Browse Source

Merge pull request #955 from Yidadaa/bugfix-0420

Bugfix 0420
Yifei Zhang 1 year ago
parent
commit
ee0f847827
9 changed files with 155 additions and 69 deletions
  1. 4 0
      README.md
  2. 4 0
      README_CN.md
  3. 0 1
      app/components/chat-list.tsx
  4. 51 40
      app/components/chat.tsx
  5. 9 11
      app/components/home.tsx
  6. 22 0
      app/icons/black-bot.svg
  7. 32 6
      app/requests.ts
  8. 15 11
      app/store/app.ts
  9. 18 0
      app/utils.ts

+ 4 - 0
README.md

@@ -146,6 +146,10 @@ Access passsword, separated by comma.
 
 Override openai api request base url.
 
+### `OPENAI_ORG_ID` (optional)
+
+Specify OpenAI organization ID.
+
 ## Development
 
 > [简体中文 > 如何进行二次开发](./README_CN.md#开发)

+ 4 - 0
README_CN.md

@@ -94,6 +94,10 @@ OpenAI 接口代理 URL,如果你手动配置了 openai 接口代理,请填
 
 > 如果遇到 ssl 证书问题,请将 `BASE_URL` 的协议设置为 http。
 
+### `OPENAI_ORG_ID` (可选)
+
+指定 OpenAI 中的组织 ID。
+
 ## 开发
 
 > 强烈不建议在本地进行开发或者部署,由于一些技术原因,很难在本地配置好 OpenAI API 代理,除非你能保证可以直连 OpenAI 服务器。

+ 0 - 1
app/components/chat-list.tsx

@@ -10,7 +10,6 @@ import {
 import { useChatStore } from "../store";
 
 import Locale from "../locales";
-import { isMobileScreen } from "../utils";
 
 export function ChatItem(props: {
   onClick?: () => void;

+ 51 - 40
app/components/chat.tsx

@@ -10,6 +10,7 @@ import CopyIcon from "../icons/copy.svg";
 import DownloadIcon from "../icons/download.svg";
 import LoadingIcon from "../icons/three-dots.svg";
 import BotIcon from "../icons/bot.svg";
+import BlackBotIcon from "../icons/black-bot.svg";
 import AddIcon from "../icons/add.svg";
 import DeleteIcon from "../icons/delete.svg";
 import MaxIcon from "../icons/max.svg";
@@ -30,15 +31,16 @@ import {
   createMessage,
   useAccessStore,
   Theme,
+  ModelType,
 } from "../store";
 
 import {
   copyToClipboard,
   downloadAs,
   getEmojiUrl,
-  isMobileScreen,
   selectOrCopy,
   autoGrowTextArea,
+  useMobileScreen,
 } from "../utils";
 
 import dynamic from "next/dynamic";
@@ -64,13 +66,17 @@ const Emoji = dynamic(async () => (await import("emoji-picker-react")).Emoji, {
   loading: () => <LoadingIcon />,
 });
 
-export function Avatar(props: { role: Message["role"] }) {
+export function Avatar(props: { role: Message["role"]; model?: ModelType }) {
   const config = useChatStore((state) => state.config);
 
   if (props.role !== "user") {
     return (
       <div className="no-dark">
-        <BotIcon className={styles["user-avtar"]} />
+        {props.model?.startsWith("gpt-4") ? (
+          <BlackBotIcon className={styles["user-avtar"]} />
+        ) : (
+          <BotIcon className={styles["user-avtar"]} />
+        )}
       </div>
     );
   }
@@ -432,6 +438,7 @@ export function Chat(props: {
   const { submitKey, shouldSubmit } = useSubmitHandler();
   const { scrollRef, setAutoScroll, scrollToBottom } = useScrollToBottom();
   const [hitBottom, setHitBottom] = useState(false);
+  const isMobileScreen = useMobileScreen();
 
   const onChatBodyScroll = (e: HTMLElement) => {
     const isTouchBottom = e.scrollTop + e.clientHeight >= e.scrollHeight - 20;
@@ -462,7 +469,7 @@ export function Chat(props: {
       const rows = inputRef.current ? autoGrowTextArea(inputRef.current) : 1;
       const inputRows = Math.min(
         5,
-        Math.max(2 + Number(!isMobileScreen()), rows),
+        Math.max(2 + Number(!isMobileScreen), rows),
       );
       setInputRows(inputRows);
     },
@@ -502,7 +509,7 @@ export function Chat(props: {
     setBeforeInput(userInput);
     setUserInput("");
     setPromptHints([]);
-    if (!isMobileScreen()) inputRef.current?.focus();
+    if (!isMobileScreen) inputRef.current?.focus();
     setAutoScroll(true);
   };
 
@@ -634,7 +641,7 @@ export function Chat(props: {
 
   // Auto focus
   useEffect(() => {
-    if (props.sideBarShowing && isMobileScreen()) return;
+    if (props.sideBarShowing && isMobileScreen) return;
     inputRef.current?.focus();
     // eslint-disable-next-line react-hooks/exhaustive-deps
   }, []);
@@ -682,7 +689,7 @@ export function Chat(props: {
               }}
             />
           </div>
-          {!isMobileScreen() && (
+          {!isMobileScreen && (
             <div className={styles["window-action-button"]}>
               <IconButton
                 icon={chatStore.config.tightBorder ? <MinIcon /> : <MaxIcon />}
@@ -717,6 +724,11 @@ export function Chat(props: {
       >
         {messages.map((message, i) => {
           const isUser = message.role === "user";
+          const showActions =
+            !isUser &&
+            i > 0 &&
+            !(message.preview || message.content.length === 0);
+          const showTyping = message.preview || message.streaming;
 
           return (
             <div
@@ -727,49 +739,48 @@ export function Chat(props: {
             >
               <div className={styles["chat-message-container"]}>
                 <div className={styles["chat-message-avatar"]}>
-                  <Avatar role={message.role} />
+                  <Avatar role={message.role} model={message.model} />
                 </div>
-                {(message.preview || message.streaming) && (
+                {showTyping && (
                   <div className={styles["chat-message-status"]}>
                     {Locale.Chat.Typing}
                   </div>
                 )}
                 <div className={styles["chat-message-item"]}>
-                  {!isUser &&
-                    !(message.preview || message.content.length === 0) && (
-                      <div className={styles["chat-message-top-actions"]}>
-                        {message.streaming ? (
-                          <div
-                            className={styles["chat-message-top-action"]}
-                            onClick={() => onUserStop(message.id ?? i)}
-                          >
-                            {Locale.Chat.Actions.Stop}
-                          </div>
-                        ) : (
-                          <>
-                            <div
-                              className={styles["chat-message-top-action"]}
-                              onClick={() => onDelete(message.id ?? i)}
-                            >
-                              {Locale.Chat.Actions.Delete}
-                            </div>
-                            <div
-                              className={styles["chat-message-top-action"]}
-                              onClick={() => onResend(message.id ?? i)}
-                            >
-                              {Locale.Chat.Actions.Retry}
-                            </div>
-                          </>
-                        )}
-
+                  {showActions && (
+                    <div className={styles["chat-message-top-actions"]}>
+                      {message.streaming ? (
                         <div
                           className={styles["chat-message-top-action"]}
-                          onClick={() => copyToClipboard(message.content)}
+                          onClick={() => onUserStop(message.id ?? i)}
                         >
-                          {Locale.Chat.Actions.Copy}
+                          {Locale.Chat.Actions.Stop}
                         </div>
+                      ) : (
+                        <>
+                          <div
+                            className={styles["chat-message-top-action"]}
+                            onClick={() => onDelete(message.id ?? i)}
+                          >
+                            {Locale.Chat.Actions.Delete}
+                          </div>
+                          <div
+                            className={styles["chat-message-top-action"]}
+                            onClick={() => onResend(message.id ?? i)}
+                          >
+                            {Locale.Chat.Actions.Retry}
+                          </div>
+                        </>
+                      )}
+
+                      <div
+                        className={styles["chat-message-top-action"]}
+                        onClick={() => copyToClipboard(message.content)}
+                      >
+                        {Locale.Chat.Actions.Copy}
                       </div>
-                    )}
+                    </div>
+                  )}
                   <Markdown
                     content={message.content}
                     loading={
@@ -778,7 +789,7 @@ export function Chat(props: {
                     }
                     onContextMenu={(e) => onRightClick(e, message)}
                     onDoubleClickCapture={() => {
-                      if (!isMobileScreen()) return;
+                      if (!isMobileScreen) return;
                       setUserInput(message.content);
                     }}
                     fontSize={fontSize}

+ 9 - 11
app/components/home.tsx

@@ -17,7 +17,7 @@ import LoadingIcon from "../icons/three-dots.svg";
 import CloseIcon from "../icons/close.svg";
 
 import { useChatStore } from "../store";
-import { getCSSVar, isMobileScreen } from "../utils";
+import { getCSSVar, useMobileScreen } from "../utils";
 import Locale from "../locales";
 import { Chat } from "./chat";
 
@@ -103,17 +103,14 @@ function useDragSideBar() {
     window.addEventListener("mousemove", handleMouseMove.current);
     window.addEventListener("mouseup", handleMouseUp.current);
   };
+  const isMobileScreen = useMobileScreen();
 
   useEffect(() => {
-    if (isMobileScreen()) {
-      return;
-    }
-
-    document.documentElement.style.setProperty(
-      "--sidebar-width",
-      `${limit(chatStore.config.sidebarWidth ?? 300)}px`,
-    );
-  }, [chatStore.config.sidebarWidth]);
+    const sideBarWidth = isMobileScreen
+      ? "100vw"
+      : `${limit(chatStore.config.sidebarWidth ?? 300)}px`;
+    document.documentElement.style.setProperty("--sidebar-width", sideBarWidth);
+  }, [chatStore.config.sidebarWidth, isMobileScreen]);
 
   return {
     onDragMouseDown,
@@ -148,6 +145,7 @@ function _Home() {
 
   // drag side bar
   const { onDragMouseDown } = useDragSideBar();
+  const isMobileScreen = useMobileScreen();
 
   useSwitchTheme();
 
@@ -158,7 +156,7 @@ function _Home() {
   return (
     <div
       className={`${
-        config.tightBorder && !isMobileScreen()
+        config.tightBorder && !isMobileScreen
           ? styles["tight-container"]
           : styles.container
       }`}

File diff suppressed because it is too large
+ 22 - 0
app/icons/black-bot.svg


+ 32 - 6
app/requests.ts

@@ -1,5 +1,11 @@
 import type { ChatRequest, ChatResponse } from "./api/openai/typing";
-import { Message, ModelConfig, useAccessStore, useChatStore } from "./store";
+import {
+  Message,
+  ModelConfig,
+  ModelType,
+  useAccessStore,
+  useChatStore,
+} from "./store";
 import { showToast } from "./components/ui-lib";
 
 const TIME_OUT_MS = 60000;
@@ -9,6 +15,7 @@ const makeRequestParam = (
   options?: {
     filterBot?: boolean;
     stream?: boolean;
+    model?: ModelType;
   },
 ): ChatRequest => {
   let sendMessages = messages.map((v) => ({
@@ -26,6 +33,11 @@ const makeRequestParam = (
   // @ts-expect-error
   delete modelConfig.max_tokens;
 
+  // override model config
+  if (options?.model) {
+    modelConfig.model = options.model;
+  }
+
   return {
     messages: sendMessages,
     stream: options?.stream,
@@ -50,7 +62,7 @@ function getHeaders() {
 
 export function requestOpenaiClient(path: string) {
   return (body: any, method = "POST") =>
-    fetch("/api/openai?_vercel_no_cache=1", {
+    fetch("/api/openai", {
       method,
       headers: {
         "Content-Type": "application/json",
@@ -61,8 +73,16 @@ export function requestOpenaiClient(path: string) {
     });
 }
 
-export async function requestChat(messages: Message[]) {
-  const req: ChatRequest = makeRequestParam(messages, { filterBot: true });
+export async function requestChat(
+  messages: Message[],
+  options?: {
+    model?: ModelType;
+  },
+) {
+  const req: ChatRequest = makeRequestParam(messages, {
+    filterBot: true,
+    model: options?.model,
+  });
 
   const res = await requestOpenaiClient("v1/chat/completions")(req);
 
@@ -204,7 +224,13 @@ export async function requestChatStream(
   }
 }
 
-export async function requestWithPrompt(messages: Message[], prompt: string) {
+export async function requestWithPrompt(
+  messages: Message[],
+  prompt: string,
+  options?: {
+    model?: ModelType;
+  },
+) {
   messages = messages.concat([
     {
       role: "user",
@@ -213,7 +239,7 @@ export async function requestWithPrompt(messages: Message[], prompt: string) {
     },
   ]);
 
-  const res = await requestChat(messages);
+  const res = await requestChat(messages, options);
 
   return res?.choices?.at(0)?.message?.content ?? "";
 }

+ 15 - 11
app/store/app.ts

@@ -17,6 +17,7 @@ export type Message = ChatCompletionResponseMessage & {
   streaming?: boolean;
   isError?: boolean;
   id?: number;
+  model?: ModelType;
 };
 
 export function createMessage(override: Partial<Message>): Message {
@@ -58,7 +59,7 @@ export interface ChatConfig {
   disablePromptHint: boolean;
 
   modelConfig: {
-    model: string;
+    model: ModelType;
     temperature: number;
     max_tokens: number;
     presence_penalty: number;
@@ -96,7 +97,9 @@ export const ALL_MODELS = [
     name: "gpt-3.5-turbo-0301",
     available: true,
   },
-];
+] as const;
+
+export type ModelType = (typeof ALL_MODELS)[number]["name"];
 
 export function limitNumber(
   x: number,
@@ -119,7 +122,7 @@ export function limitModel(name: string) {
 
 export const ModalConfigValidator = {
   model(x: string) {
-    return limitModel(x);
+    return limitModel(x) as ModelType;
   },
   max_tokens(x: number) {
     return limitNumber(x, 0, 32000, 2000);
@@ -387,6 +390,7 @@ export const useChatStore = create<ChatStore>()(
           role: "assistant",
           streaming: true,
           id: userMessage.id! + 1,
+          model: get().config.modelConfig.model,
         });
 
         // get recent messages
@@ -531,14 +535,14 @@ export const useChatStore = create<ChatStore>()(
           session.topic === DEFAULT_TOPIC &&
           countMessages(session.messages) >= SUMMARIZE_MIN_LEN
         ) {
-          requestWithPrompt(session.messages, Locale.Store.Prompt.Topic).then(
-            (res) => {
-              get().updateCurrentSession(
-                (session) =>
-                  (session.topic = res ? trimTopic(res) : DEFAULT_TOPIC),
-              );
-            },
-          );
+          requestWithPrompt(session.messages, Locale.Store.Prompt.Topic, {
+            model: "gpt-3.5-turbo",
+          }).then((res) => {
+            get().updateCurrentSession(
+              (session) =>
+                (session.topic = res ? trimTopic(res) : DEFAULT_TOPIC),
+            );
+          });
         }
 
         const config = get().config;

+ 18 - 0
app/utils.ts

@@ -1,4 +1,5 @@
 import { EmojiStyle } from "emoji-picker-react";
+import { useEffect, useState } from "react";
 import { showToast } from "./components/ui-lib";
 import Locale from "./locales";
 
@@ -47,6 +48,23 @@ export function isIOS() {
   return /iphone|ipad|ipod/.test(userAgent);
 }
 
+export function useMobileScreen() {
+  const [isMobileScreen_, setIsMobileScreen] = useState(false);
+  useEffect(() => {
+    const onResize = () => {
+      setIsMobileScreen(isMobileScreen());
+    };
+
+    window.addEventListener("resize", onResize);
+
+    return () => {
+      window.removeEventListener("resize", onResize);
+    };
+  }, []);
+
+  return isMobileScreen_;
+}
+
 export function isMobileScreen() {
   return window.innerWidth <= 600;
 }

Some files were not shown because too many files changed in this diff