Browse Source

Merge pull request #1361 from Yidadaa/bugfix-0509

feat: #1055, #444 and fix #1359 hot key to switch chat and allow to disable gpt-4
Yifei Zhang 1 year ago
parent
commit
dae7da0e4e

+ 7 - 1
README.md

@@ -168,7 +168,13 @@ Specify OpenAI organization ID.
 
 > Default: Empty
 
-If you do not want users to input their own API key, set this environment variable to 1.
+If you do not want users to input their own API key, set this value to 1.
+
+### `DISABLE_GPT4` (optional)
+
+> Default: Empty
+
+If you do not want users to use GPT-4, set this value to 1.
 
 ## Development
 

+ 5 - 1
README_CN.md

@@ -64,7 +64,7 @@ code1,code2,code3
 
 ## 环境变量
 
-> 本项目大多数配置项都通过环境变量来设置。
+> 本项目大多数配置项都通过环境变量来设置,教程:[如何修改 Vercel 环境变量](./docs/vercel-cn.md)
 
 ### `OPENAI_API_KEY` (必填项)
 
@@ -94,6 +94,10 @@ OpenAI 接口代理 URL,如果你手动配置了 openai 接口代理,请填
 
 如果你不想让用户自行填入 API Key,将此环境变量设置为 1 即可。
 
+### `DISABLE_GPT4` (可选)
+
+如果你不想让用户使用 GPT-4,将此环境变量设置为 1 即可。
+
 ## 开发
 
 > 强烈不建议在本地进行开发或者部署,由于一些技术原因,很难在本地配置好 OpenAI API 代理,除非你能保证可以直连 OpenAI 服务器。

+ 2 - 1
app/api/config/route.ts

@@ -1,4 +1,4 @@
-import { NextRequest, NextResponse } from "next/server";
+import { NextResponse } from "next/server";
 
 import { getServerSideConfig } from "../../config/server";
 
@@ -9,6 +9,7 @@ const serverConfig = getServerSideConfig();
 const DANGER_CONFIG = {
   needCode: serverConfig.needCode,
   hideUserApiKey: serverConfig.hideUserApiKey,
+  enableGPT4: serverConfig.enableGPT4,
 };
 
 declare global {

+ 4 - 10
app/components/chat.tsx

@@ -53,7 +53,7 @@ import chatStyle from "./chat.module.scss";
 
 import { ListItem, Modal, showModal } from "./ui-lib";
 import { useLocation, useNavigate } from "react-router-dom";
-import { Path } from "../constant";
+import { LAST_INPUT_KEY, Path } from "../constant";
 import { Avatar } from "./emoji";
 import { MaskAvatar, MaskConfig } from "./mask";
 import { useMaskStore } from "../store/mask";
@@ -404,7 +404,6 @@ export function Chat() {
 
   const inputRef = useRef<HTMLTextAreaElement>(null);
   const [userInput, setUserInput] = useState("");
-  const [beforeInput, setBeforeInput] = useState("");
   const [isLoading, setIsLoading] = useState(false);
   const { submitKey, shouldSubmit } = useSubmitHandler();
   const { scrollRef, setAutoScroll, scrollToBottom } = useScrollToBottom();
@@ -477,7 +476,7 @@ export function Chat() {
     if (userInput.trim() === "") return;
     setIsLoading(true);
     chatStore.onUserInput(userInput).then(() => setIsLoading(false));
-    setBeforeInput(userInput);
+    localStorage.setItem(LAST_INPUT_KEY, userInput);
     setUserInput("");
     setPromptHints([]);
     if (!isMobileScreen) inputRef.current?.focus();
@@ -491,9 +490,9 @@ export function Chat() {
 
   // check if should send message
   const onInputKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
-    // if ArrowUp and no userInput
+    // if ArrowUp and no userInput, fill with last input
     if (e.key === "ArrowUp" && userInput.length <= 0) {
-      setUserInput(beforeInput);
+      setUserInput(localStorage.getItem(LAST_INPUT_KEY) ?? "");
       e.preventDefault();
       return;
     }
@@ -503,11 +502,6 @@ export function Chat() {
     }
   };
   const onRightClick = (e: any, message: Message) => {
-    // auto fill user input
-    if (message.role === "user") {
-      setUserInput(message.content);
-    }
-
     // copy to clipboard
     if (selectOrCopy(e.currentTarget, message.content)) {
       e.preventDefault();

+ 1 - 1
app/components/mask.tsx

@@ -14,7 +14,7 @@ import CopyIcon from "../icons/copy.svg";
 
 import { DEFAULT_MASK_AVATAR, Mask, useMaskStore } from "../store/mask";
 import { Message, ModelConfig, ROLES, useChatStore } from "../store";
-import { Input, List, ListItem, Modal, Popover, showToast } from "./ui-lib";
+import { Input, List, ListItem, Modal, Popover } from "./ui-lib";
 import { Avatar, AvatarPicker } from "./emoji";
 import Locale, { AllLangs, Lang } from "../locales";
 import { useNavigate } from "react-router-dom";

+ 24 - 1
app/components/sidebar.tsx

@@ -32,6 +32,28 @@ const ChatList = dynamic(async () => (await import("./chat-list")).ChatList, {
   loading: () => null,
 });
 
+function useHotKey() {
+  const chatStore = useChatStore();
+
+  useEffect(() => {
+    const onKeyDown = (e: KeyboardEvent) => {
+      if (e.metaKey || e.altKey || e.ctrlKey) {
+        const n = chatStore.sessions.length;
+        const limit = (x: number) => (x + n) % n;
+        const i = chatStore.currentSessionIndex;
+        if (e.key === "ArrowUp") {
+          chatStore.selectSession(limit(i - 1));
+        } else if (e.key === "ArrowDown") {
+          chatStore.selectSession(limit(i + 1));
+        }
+      }
+    };
+
+    window.addEventListener("keydown", onKeyDown);
+    return () => window.removeEventListener("keydown", onKeyDown);
+  });
+}
+
 function useDragSideBar() {
   const limit = (x: number) => Math.min(MAX_SIDEBAR_WIDTH, x);
 
@@ -86,9 +108,10 @@ export function SideBar(props: { className?: string }) {
   // drag side bar
   const { onDragMouseDown, shouldNarrow } = useDragSideBar();
   const navigate = useNavigate();
-
   const config = useAppConfig();
 
+  useHotKey();
+
   return (
     <div
       className={`${styles.sidebar} ${props.className} ${

+ 2 - 0
app/config/server.ts

@@ -8,6 +8,7 @@ declare global {
       PROXY_URL?: string;
       VERCEL?: string;
       HIDE_USER_API_KEY?: string; // disable user's api key input
+      DISABLE_GPT4?: string; // allow user to use gpt-4 or not
     }
   }
 }
@@ -40,5 +41,6 @@ export const getServerSideConfig = () => {
     proxyUrl: process.env.PROXY_URL,
     isVercel: !!process.env.VERCEL,
     hideUserApiKey: !!process.env.HIDE_USER_API_KEY,
+    enableGPT4: !process.env.DISABLE_GPT4,
   };
 };

+ 2 - 0
app/constant.ts

@@ -38,3 +38,5 @@ export const MIN_SIDEBAR_WIDTH = 230;
 export const NARROW_SIDEBAR_WIDTH = 100;
 
 export const ACCESS_CODE_PREFIX = "ak-";
+
+export const LAST_INPUT_KEY = "last-input";

+ 1 - 1
app/masks/en.ts

@@ -31,7 +31,7 @@ export const EN_MASKS: BuiltinMask[] = [
     ],
     modelConfig: {
       model: "gpt-4",
-      temperature: 1,
+      temperature: 0.5,
       max_tokens: 2000,
       presence_penalty: 0,
       sendMemory: true,

+ 9 - 0
app/store/access.ts

@@ -2,6 +2,7 @@ import { create } from "zustand";
 import { persist } from "zustand/middleware";
 import { StoreKey } from "../constant";
 import { BOT_HELLO } from "./chat";
+import { ALL_MODELS } from "./config";
 
 export interface AccessControlStore {
   accessCode: string;
@@ -60,6 +61,14 @@ export const useAccessStore = create<AccessControlStore>()(
             console.log("[Config] got config from server", res);
             set(() => ({ ...res }));
 
+            if (!res.enableGPT4) {
+              ALL_MODELS.forEach((model) => {
+                if (model.name.startsWith("gpt-4")) {
+                  (model as any).available = false;
+                }
+              });
+            }
+
             if ((res as any).botHello) {
               BOT_HELLO.content = (res as any).botHello;
             }

+ 2 - 1
app/store/chat.ts

@@ -180,8 +180,9 @@ export const useChatStore = create<ChatStore>()(
         const sessions = get().sessions.slice();
         sessions.splice(index, 1);
 
+        const currentIndex = get().currentSessionIndex;
         let nextIndex = Math.min(
-          get().currentSessionIndex,
+          currentIndex - Number(index < currentIndex),
           sessions.length - 1,
         );
 

+ 20 - 0
app/store/config.ts

@@ -76,6 +76,26 @@ export const ALL_MODELS = [
     name: "gpt-3.5-turbo-0301",
     available: true,
   },
+  {
+    name: "qwen-v1", // 通义千问
+    available: false,
+  },
+  {
+    name: "ernie", // 文心一言
+    available: false,
+  },
+  {
+    name: "spark", // 讯飞星火
+    available: false,
+  },
+  {
+    name: "llama", // llama
+    available: false,
+  },
+  {
+    name: "chatglm", // chatglm-6b
+    available: false,
+  },
 ] as const;
 
 export type ModelType = (typeof ALL_MODELS)[number]["name"];

+ 2 - 2
app/utils.ts

@@ -160,13 +160,13 @@ export function autoGrowTextArea(dom: HTMLTextAreaElement) {
   measureDom.style.width = width + "px";
   measureDom.innerText = dom.value.trim().length > 0 ? dom.value : "1";
 
-  const lineWrapCount = Math.max(0, dom.value.split("\n").length - 1);
+  const emptyLineWrap = Math.max(0, dom.value.split("\n\n").length - 1);
   const height = parseFloat(window.getComputedStyle(measureDom).height);
   const singleLineHeight = parseFloat(
     window.getComputedStyle(singleLineDom).height,
   );
 
-  const rows = Math.round(height / singleLineHeight) + lineWrapCount;
+  const rows = Math.round(height / singleLineHeight) + emptyLineWrap;
 
   return rows;
 }