Browse Source

Merge pull request #1333 from Yidadaa/0508-bugfix

feat: #1303 and fix #1294 1307, new modal style
Yifei Zhang 1 year ago
parent
commit
8f21e736dd

+ 28 - 0
app/command.ts

@@ -0,0 +1,28 @@
+import { useSearchParams } from "react-router-dom";
+
+type Command = (param: string) => void;
+interface Commands {
+  fill?: Command;
+  submit?: Command;
+  mask?: Command;
+}
+
+export function useCommand(commands: Commands = {}) {
+  const [searchParams, setSearchParams] = useSearchParams();
+
+  if (commands === undefined) return;
+
+  let shouldUpdate = false;
+  searchParams.forEach((param, name) => {
+    const commandName = name as keyof Commands;
+    if (typeof commands[commandName] === "function") {
+      commands[commandName]!(param);
+      searchParams.delete(name);
+      shouldUpdate = true;
+    }
+  });
+
+  if (shouldUpdate) {
+    setSearchParams(searchParams);
+  }
+}

+ 12 - 11
app/components/chat.tsx

@@ -26,12 +26,10 @@ import {
   SubmitKey,
   useChatStore,
   BOT_HELLO,
-  ROLES,
   createMessage,
   useAccessStore,
   Theme,
   useAppConfig,
-  ModelConfig,
   DEFAULT_TOPIC,
 } from "../store";
 
@@ -58,11 +56,8 @@ import { useLocation, useNavigate } from "react-router-dom";
 import { Path } from "../constant";
 import { Avatar } from "./emoji";
 import { MaskAvatar, MaskConfig } from "./mask";
-import {
-  DEFAULT_MASK_AVATAR,
-  DEFAULT_MASK_ID,
-  useMaskStore,
-} from "../store/mask";
+import { useMaskStore } from "../store/mask";
+import { useCommand } from "../command";
 
 const Markdown = dynamic(async () => (await import("./markdown")).Markdown, {
   loading: () => <LoadingIcon />,
@@ -478,8 +473,7 @@ export function Chat() {
     }
   };
 
-  // submit user input
-  const onUserSubmit = () => {
+  const doSubmit = (userInput: string) => {
     if (userInput.trim() === "") return;
     setIsLoading(true);
     chatStore.onUserInput(userInput).then(() => setIsLoading(false));
@@ -504,7 +498,7 @@ export function Chat() {
       return;
     }
     if (shouldSubmit(e)) {
-      onUserSubmit();
+      doSubmit(userInput);
       e.preventDefault();
     }
   };
@@ -618,6 +612,13 @@ export function Chat() {
   const isChat = location.pathname === Path.Chat;
   const autoFocus = !isMobileScreen || isChat; // only focus in chat page
 
+  useCommand({
+    fill: setUserInput,
+    submit: (text) => {
+      doSubmit(text);
+    },
+  });
+
   return (
     <div className={styles.chat} key={session.id}>
       <div className="window-header">
@@ -816,7 +817,7 @@ export function Chat() {
             text={Locale.Chat.Send}
             className={styles["chat-input-send"]}
             type="primary"
-            onClick={onUserSubmit}
+            onClick={() => doSubmit(userInput)}
           />
         </div>
       </div>

+ 1 - 0
app/components/home.tsx

@@ -23,6 +23,7 @@ import {
 } from "react-router-dom";
 import { SideBar } from "./sidebar";
 import { useAppConfig } from "../store/config";
+import { useMaskStore } from "../store/mask";
 
 export function Loading(props: { noLogo?: boolean }) {
   return (

+ 13 - 7
app/components/markdown.tsx

@@ -12,15 +12,21 @@ import mermaid from "mermaid";
 import LoadingIcon from "../icons/three-dots.svg";
 import React from "react";
 
-export function Mermaid(props: { code: string }) {
+export function Mermaid(props: { code: string; onError: () => void }) {
   const ref = useRef<HTMLDivElement>(null);
 
   useEffect(() => {
     if (props.code && ref.current) {
-      mermaid.run({
-        nodes: [ref.current],
-      });
+      mermaid
+        .run({
+          nodes: [ref.current],
+        })
+        .catch((e) => {
+          props.onError();
+          console.error("[Mermaid] ", e.message);
+        });
     }
+    // eslint-disable-next-line react-hooks/exhaustive-deps
   }, [props.code]);
 
   function viewSvgInNewWindow() {
@@ -38,7 +44,7 @@ export function Mermaid(props: { code: string }) {
   return (
     <div
       className="no-dark"
-      style={{ cursor: "pointer" }}
+      style={{ cursor: "pointer", overflow: "auto" }}
       ref={ref}
       onClick={() => viewSvgInNewWindow()}
     >
@@ -60,7 +66,7 @@ export function PreCode(props: { children: any }) {
   }, [props.children]);
 
   if (mermaidCode) {
-    return <Mermaid code={mermaidCode} />;
+    return <Mermaid code={mermaidCode} onError={() => setMermaidCode("")} />;
   }
 
   return (
@@ -147,7 +153,7 @@ export function Markdown(
     }
   };
 
-  checkInView();
+  setTimeout(() => checkInView(), 1);
 
   return (
     <div

+ 61 - 38
app/components/mask.tsx

@@ -20,7 +20,7 @@ import Locale, { AllLangs, Lang } from "../locales";
 import { useNavigate } from "react-router-dom";
 
 import chatStyle from "./chat.module.scss";
-import { useEffect, useState } from "react";
+import { useState } from "react";
 import { downloadAs, readFromFile } from "../utils";
 import { Updater } from "../api/openai/typing";
 import { ModelConfigList } from "./model-config";
@@ -106,6 +106,59 @@ export function MaskConfig(props: {
   );
 }
 
+function ContextPromptItem(props: {
+  prompt: Message;
+  update: (prompt: Message) => void;
+  remove: () => void;
+}) {
+  const [focusingInput, setFocusingInput] = useState(false);
+
+  return (
+    <div className={chatStyle["context-prompt-row"]}>
+      {!focusingInput && (
+        <select
+          value={props.prompt.role}
+          className={chatStyle["context-role"]}
+          onChange={(e) =>
+            props.update({
+              ...props.prompt,
+              role: e.target.value as any,
+            })
+          }
+        >
+          {ROLES.map((r) => (
+            <option key={r} value={r}>
+              {r}
+            </option>
+          ))}
+        </select>
+      )}
+      <Input
+        value={props.prompt.content}
+        type="text"
+        className={chatStyle["context-content"]}
+        rows={focusingInput ? 5 : 1}
+        onFocus={() => setFocusingInput(true)}
+        onBlur={() => setFocusingInput(false)}
+        onInput={(e) =>
+          props.update({
+            ...props.prompt,
+            content: e.currentTarget.value as any,
+          })
+        }
+      />
+      {!focusingInput && (
+        <IconButton
+          icon={<DeleteIcon />}
+          className={chatStyle["context-delete-button"]}
+          onClick={() => props.remove()}
+          bordered
+        />
+      )}
+    </div>
+  );
+}
+
 export function ContextPrompts(props: {
   context: Message[];
   updateContext: (updater: (context: Message[]) => void) => void;
@@ -128,42 +181,12 @@ export function ContextPrompts(props: {
     <>
       <div className={chatStyle["context-prompt"]} style={{ marginBottom: 20 }}>
         {context.map((c, i) => (
-          <div className={chatStyle["context-prompt-row"]} key={i}>
-            <select
-              value={c.role}
-              className={chatStyle["context-role"]}
-              onChange={(e) =>
-                updateContextPrompt(i, {
-                  ...c,
-                  role: e.target.value as any,
-                })
-              }
-            >
-              {ROLES.map((r) => (
-                <option key={r} value={r}>
-                  {r}
-                </option>
-              ))}
-            </select>
-            <Input
-              value={c.content}
-              type="text"
-              className={chatStyle["context-content"]}
-              rows={1}
-              onInput={(e) =>
-                updateContextPrompt(i, {
-                  ...c,
-                  content: e.currentTarget.value as any,
-                })
-              }
-            />
-            <IconButton
-              icon={<DeleteIcon />}
-              className={chatStyle["context-delete-button"]}
-              onClick={() => removeContextPrompt(i)}
-              bordered
-            />
-          </div>
+          <ContextPromptItem
+            key={i}
+            prompt={c}
+            update={(prompt) => updateContextPrompt(i, prompt)}
+            remove={() => removeContextPrompt(i)}
+          />
         ))}
 
         <div className={chatStyle["context-prompt-row"]}>
@@ -174,7 +197,7 @@ export function ContextPrompts(props: {
             className={chatStyle["context-prompt-button"]}
             onClick={() =>
               addContextPrompt({
-                role: "system",
+                role: "user",
                 content: "",
                 date: "",
               })

+ 0 - 1
app/components/model-config.tsx

@@ -1,4 +1,3 @@
-import styles from "./settings.module.scss";
 import { ALL_MODELS, ModalConfigValidator, ModelConfig } from "../store";
 
 import Locale from "../locales";

+ 13 - 1
app/components/new-chat.tsx

@@ -13,6 +13,7 @@ import { Mask, useMaskStore } from "../store/mask";
 import Locale from "../locales";
 import { useAppConfig, useChatStore } from "../store";
 import { MaskAvatar } from "./mask";
+import { useCommand } from "../command";
 
 function getIntersectionArea(aRect: DOMRect, bRect: DOMRect) {
   const xmin = Math.max(aRect.x, bRect.x);
@@ -108,9 +109,20 @@ export function NewChat() {
 
   const startChat = (mask?: Mask) => {
     chatStore.newSession(mask);
-    navigate(Path.Chat);
+    setTimeout(() => navigate(Path.Chat), 1);
   };
 
+  useCommand({
+    mask: (id) => {
+      try {
+        const mask = maskStore.get(parseInt(id));
+        startChat(mask ?? undefined);
+      } catch {
+        console.error("[New Chat] failed to create chat from mask id=", id);
+      }
+    },
+  });
+
   return (
     <div className={styles["new-chat"]}>
       <div className={styles["mask-header"]}>

+ 12 - 10
app/components/ui-lib.module.scss

@@ -124,6 +124,18 @@
   }
 }
 
+@media screen and (max-width: 600px) {
+  .modal-container {
+    width: 100vw;
+    border-bottom-left-radius: 0;
+    border-bottom-right-radius: 0;
+
+    .modal-content {
+      max-height: 50vh;
+    }
+  }
+}
+
 .show {
   opacity: 1;
   transition: all ease 0.3s;
@@ -191,13 +203,3 @@
   resize: none;
   min-width: 50px;
 }
-
-@media only screen and (max-width: 600px) {
-  .modal-container {
-    width: 90vw;
-
-    .modal-content {
-      max-height: 50vh;
-    }
-  }
-}

+ 2 - 2
app/locales/cn.ts

@@ -4,7 +4,7 @@ const cn = {
   WIP: "该功能仍在开发中……",
   Error: {
     Unauthorized:
-      "现在是未授权状态,请点击左下角[设置](/#/settings)按钮输入访问密码。",
+      "访问密码不正确或为空,请前往[设置](/#/settings)页输入正确的访问密码,或者填入你自己的 OpenAI API Key。",
   },
   ChatItem: {
     ChatItemCount: (count: number) => `${count} 条对话`,
@@ -149,7 +149,7 @@ const cn = {
     },
     AccessCode: {
       Title: "访问密码",
-      SubTitle: "已开启加密访问",
+      SubTitle: "管理员已开启加密访问",
       Placeholder: "请输入访问密码",
     },
     Model: "模型 (model)",

+ 2 - 2
app/store/chat.ts

@@ -7,11 +7,11 @@ import {
   requestChatStream,
   requestWithPrompt,
 } from "../requests";
-import { isMobileScreen, trimTopic } from "../utils";
+import { trimTopic } from "../utils";
 
 import Locale from "../locales";
 import { showToast } from "../components/ui-lib";
-import { DEFAULT_CONFIG, ModelConfig, ModelType, useAppConfig } from "./config";
+import { ModelType } from "./config";
 import { createEmptyMask, Mask } from "./mask";
 import { StoreKey } from "../constant";
 

+ 1 - 1
app/store/config.ts

@@ -31,7 +31,7 @@ export const DEFAULT_CONFIG = {
 
   modelConfig: {
     model: "gpt-3.5-turbo" as ModelType,
-    temperature: 1,
+    temperature: 0.5,
     max_tokens: 2000,
     presence_penalty: 0,
     sendMemory: true,

+ 4 - 0
app/styles/globals.scss

@@ -248,6 +248,10 @@ div.math {
   display: flex;
   align-items: center;
   justify-content: center;
+
+  @media screen and (max-width: 600px) {
+    align-items: flex-end;
+  }
 }
 
 .link {

+ 6 - 1
next.config.mjs

@@ -5,7 +5,12 @@ const nextConfig = {
     appDir: true,
   },
   async rewrites() {
-    const ret = [];
+    const ret = [
+      {
+        source: "/api/proxy/:path*",
+        destination: "https://api.openai.com/:path*",
+      },
+    ];
 
     const apiUrl = process.env.API_URL;
     if (apiUrl) {