Prechádzať zdrojové kódy

Merge branch 'Yidadaa:main' into main

Ilario Scandurra 1 rok pred
rodič
commit
502d22bd20

+ 5 - 1
README.md

@@ -35,14 +35,17 @@ One-Click to deploy your own ChatGPT web UI.
 - Awesome prompts powered by [awesome-chatgpt-prompts-zh](https://github.com/PlexPt/awesome-chatgpt-prompts-zh) and [awesome-chatgpt-prompts](https://github.com/f/awesome-chatgpt-prompts)
 - Automatically compresses chat history to support long conversations while also saving your tokens
 - One-click export all chat history with full Markdown support
+- I18n supported
 
 ## 开发计划 Roadmap
+
 - System Prompt: pin a user defined prompt as system prompt 为每个对话设置系统 Prompt [#138](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/138)
 - User Prompt: user can edit and save custom prompts to prompt list 允许用户自行编辑内置 Prompt 列表
 - Self-host Model: support llama, alpaca, ChatGLM, BELLE etc. 支持自部署的大语言模型
 - Plugins: support network search, caculator, any other apis etc. 插件机制,支持联网搜索、计算器、调用其他平台 api [#165](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/165)
 
 ### 不会开发的功能 Not in Plan
+
 - User login, accounts, cloud sync 用户登录、账号管理、消息云同步
 - UI text customize 界面文字自定义
 
@@ -179,9 +182,10 @@ docker run -d -p 3000:3000 -e OPENAI_API_KEY="" -e CODE="" yidadaa/chatgpt-next-
 
 ![更多展示 More](./static/more.png)
 
-
 ## 捐赠 Donate USDT
+
 > BNB Smart Chain (BEP 20)
+
 ```
 0x67cD02c7EB62641De576a1fA3EdB32eA0c3ffD89
 ```

+ 6 - 1
app/components/button.tsx

@@ -8,6 +8,7 @@ export function IconButton(props: {
   text?: string;
   bordered?: boolean;
   shadow?: boolean;
+  noDark?: boolean;
   className?: string;
   title?: string;
 }) {
@@ -23,7 +24,11 @@ export function IconButton(props: {
       title={props.title}
       role="button"
     >
-      <div className={styles["icon-button-icon"]}>{props.icon}</div>
+      <div
+        className={styles["icon-button-icon"] + ` ${props.noDark && "no-dark"}`}
+      >
+        {props.icon}
+      </div>
       {props.text && (
         <div className={styles["icon-button-text"]}>{props.text}</div>
       )}

+ 4 - 0
app/components/chat.module.scss

@@ -1,3 +1,5 @@
+@import "../styles/animation.scss";
+
 .prompt-toast {
   position: absolute;
   bottom: -50px;
@@ -19,6 +21,8 @@
     padding: 10px 20px;
     border-radius: 100px;
 
+    animation: slide-in-from-top ease 0.3s;
+
     .prompt-toast-content {
       margin-left: 10px;
     }

+ 33 - 16
app/components/chat.tsx

@@ -51,7 +51,7 @@ const Emoji = dynamic(async () => (await import("emoji-picker-react")).Emoji, {
 export function Avatar(props: { role: Message["role"] }) {
   const config = useChatStore((state) => state.config);
 
-  if (props.role === "assistant") {
+  if (props.role !== "user") {
     return <BotIcon className={styles["user-avtar"]} />;
   }
 
@@ -99,7 +99,8 @@ function exportMessages(messages: Message[], topic: string) {
 }
 
 function PromptToast(props: {
-  showModal: boolean;
+  showToast?: boolean;
+  showModal?: boolean;
   setShowModal: (_: boolean) => void;
 }) {
   const chatStore = useChatStore();
@@ -126,16 +127,18 @@ function PromptToast(props: {
 
   return (
     <div className={chatStyle["prompt-toast"]} key="prompt-toast">
-      <div
-        className={chatStyle["prompt-toast-inner"] + " clickable"}
-        role="button"
-        onClick={() => props.setShowModal(true)}
-      >
-        <BrainIcon />
-        <span className={chatStyle["prompt-toast-content"]}>
-          {Locale.Context.Toast(context.length)}
-        </span>
-      </div>
+      {props.showToast && (
+        <div
+          className={chatStyle["prompt-toast-inner"] + " clickable"}
+          role="button"
+          onClick={() => props.setShowModal(true)}
+        >
+          <BrainIcon />
+          <span className={chatStyle["prompt-toast-content"]}>
+            {Locale.Context.Toast(context.length)}
+          </span>
+        </div>
+      )}
       {props.showModal && (
         <div className="modal-mask">
           <Modal
@@ -187,6 +190,7 @@ function PromptToast(props: {
                       icon={<DeleteIcon />}
                       className={chatStyle["context-delete-button"]}
                       onClick={() => removeContextPrompt(i)}
+                      bordered
                     />
                   </div>
                 ))}
@@ -281,7 +285,7 @@ function useScrollToBottom() {
   useLayoutEffect(() => {
     const dom = scrollRef.current;
     if (dom && autoScroll) {
-      setTimeout(() => (dom.scrollTop = dom.scrollHeight), 500);
+      setTimeout(() => (dom.scrollTop = dom.scrollHeight), 1);
     }
   });
 
@@ -310,6 +314,12 @@ export function Chat(props: {
   const [isLoading, setIsLoading] = useState(false);
   const { submitKey, shouldSubmit } = useSubmitHandler();
   const { scrollRef, setAutoScroll } = useScrollToBottom();
+  const [hitBottom, setHitBottom] = useState(false);
+
+  const onChatBodyScroll = (e: HTMLElement) => {
+    const isTouchBottom = e.scrollTop + e.clientHeight >= e.scrollHeight - 20;
+    setHitBottom(isTouchBottom);
+  };
 
   // prompt hints
   const promptStore = usePromptStore();
@@ -441,7 +451,7 @@ export function Chat(props: {
               role: "user",
               content: userInput,
               date: new Date().toLocaleString(),
-              preview: false,
+              preview: true,
             },
           ]
         : [],
@@ -505,12 +515,17 @@ export function Chat(props: {
         </div>
 
         <PromptToast
+          showToast={!hitBottom}
           showModal={showPromptModal}
           setShowModal={setShowPromptModal}
         />
       </div>
 
-      <div className={styles["chat-body"]} ref={scrollRef}>
+      <div
+        className={styles["chat-body"]}
+        ref={scrollRef}
+        onScroll={(e) => onChatBodyScroll(e.currentTarget)}
+      >
         {messages.map((message, i) => {
           const isUser = message.role === "user";
 
@@ -533,6 +548,7 @@ export function Chat(props: {
                 <div
                   className={styles["chat-message-item"]}
                   onMouseOver={() => inputRef.current?.blur()}
+                  onTouchStart={() => inputRef.current?.blur()}
                 >
                   {!isUser &&
                     !(message.preview || message.content.length === 0) && (
@@ -612,7 +628,8 @@ export function Chat(props: {
           <IconButton
             icon={<SendWhiteIcon />}
             text={Locale.Chat.Send}
-            className={styles["chat-input-send"] + " no-dark"}
+            className={styles["chat-input-send"]}
+            noDark
             onClick={onUserSubmit}
           />
         </div>

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

@@ -1,4 +1,5 @@
 @import "./window.scss";
+@import "../styles/animation.scss";
 
 @mixin container {
   background-color: var(--white);
@@ -73,7 +74,7 @@
   .sidebar {
     position: absolute;
     left: -100%;
-    z-index: 999;
+    z-index: 1000;
     height: var(--full-height);
     transition: all ease 0.3s;
     box-shadow: none;
@@ -132,18 +133,6 @@
   overflow: hidden;
 }
 
-@keyframes slide-in {
-  from {
-    opacity: 0;
-    transform: translateY(20px);
-  }
-
-  to {
-    opacity: 1;
-    transform: translateY(0px);
-  }
-}
-
 .chat-item:hover {
   background-color: var(--hover-color);
 }
@@ -344,6 +333,7 @@
 .chat-input-panel {
   width: 100%;
   padding: 20px;
+  padding-top: 5px;
   box-sizing: border-box;
   flex-direction: column;
 }

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

@@ -1,3 +1,5 @@
+@import "../styles/animation.scss";
+
 .card {
   background-color: var(--white);
   border-radius: 10px;
@@ -24,18 +26,6 @@
   height: 100vh;
 }
 
-@keyframes slide-in {
-  from {
-    transform: translateY(10px);
-    opacity: 0;
-  }
-
-  to {
-    transform: translateY(0);
-    opacity: 1;
-  }
-}
-
 .list-item {
   display: flex;
   justify-content: space-between;

+ 1 - 1
app/locales/cn.ts

@@ -35,7 +35,7 @@ const cn = {
     Download: "下载文件",
   },
   Memory: {
-    Title: "上下文记忆 Prompt",
+    Title: "历史记忆",
     EmptyContent: "尚未记忆",
     Copy: "全部复制",
   },

+ 5 - 1
app/requests.ts

@@ -88,7 +88,11 @@ export async function requestUsage() {
     const response = (await res.json()) as {
       total_usage: number;
     };
-    return Math.round(response.total_usage) / 100;
+
+    if (response.total_usage) {
+      response.total_usage = Math.round(response.total_usage) / 100;
+    }
+    return response.total_usage;
   } catch (error) {
     console.error("[Request usage] ", error, res.body);
   }

+ 23 - 0
app/styles/animation.scss

@@ -0,0 +1,23 @@
+@keyframes slide-in {
+  from {
+    opacity: 0;
+    transform: translateY(20px);
+  }
+
+  to {
+    opacity: 1;
+    transform: translateY(0px);
+  }
+}
+
+@keyframes slide-in-from-top {
+  from {
+    opacity: 0;
+    transform: translateY(-20px);
+  }
+
+  to {
+    opacity: 1;
+    transform: translateY(0px);
+  }
+}

+ 1 - 1
app/styles/globals.scss

@@ -188,7 +188,7 @@ input[type="text"] {
   appearance: none;
   border-radius: 10px;
   border: var(--border-in-light);
-  height: 36px;
+  min-height: 36px;
   box-sizing: border-box;
   background: var(--white);
   color: var(--black);