leedom hace 1 año
padre
commit
a8a8becf96

+ 1 - 1
app/components/button.module.scss

@@ -12,7 +12,7 @@
   user-select: none;
   outline: none;
   border: none;
-  color: rgb(51, 51, 51);
+  color: var(--black);
 
   &[disabled] {
     cursor: not-allowed;

+ 2 - 4
app/components/chat-list.tsx

@@ -59,6 +59,7 @@ export function ChatList() {
       state.removeSession,
       state.moveSession,
     ]);
+  const chatStore = useChatStore();
 
   const onDragEnd: OnDragEndResponder = (result) => {
     const { destination, source } = result;
@@ -95,10 +96,7 @@ export function ChatList() {
                 index={i}
                 selected={i === selectedIndex}
                 onClick={() => selectSession(i)}
-                onDelete={() =>
-                  (!isMobileScreen() || confirm(Locale.Home.DeleteChat)) &&
-                  removeSession(i)
-                }
+                onDelete={chatStore.deleteSession}
               />
             ))}
             {provided.placeholder}

+ 6 - 10
app/components/chat.tsx

@@ -4,7 +4,7 @@ import { memo, useState, useRef, useEffect, useLayoutEffect } from "react";
 import SendWhiteIcon from "../icons/send-white.svg";
 import BrainIcon from "../icons/brain.svg";
 import ExportIcon from "../icons/export.svg";
-import MenuIcon from "../icons/menu.svg";
+import ReturnIcon from "../icons/return.svg";
 import CopyIcon from "../icons/copy.svg";
 import DownloadIcon from "../icons/download.svg";
 import LoadingIcon from "../icons/three-dots.svg";
@@ -404,6 +404,7 @@ export function Chat(props: {
 
   // submit user input
   const onUserSubmit = () => {
+    if (userInput.length <= 0) return;
     setIsLoading(true);
     chatStore.onUserInput(userInput).then(() => setIsLoading(false));
     setUserInput("");
@@ -420,7 +421,6 @@ export function Chat(props: {
   // check if should send message
   const onInputKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
     if (shouldSubmit(e)) {
-      setAutoScroll(true);
       onUserSubmit();
       e.preventDefault();
     }
@@ -507,13 +507,10 @@ export function Chat(props: {
   return (
     <div className={styles.chat} key={session.id}>
       <div className={styles["window-header"]}>
-        <div
-          className={styles["window-header-title"]}
-          onClick={props?.showSideBar}
-        >
+        <div className={styles["window-header-title"]}>
           <div
             className={`${styles["window-header-main-title"]} ${styles["chat-body-title"]}`}
-            onClick={() => {
+            onClickCapture={() => {
               const newTopic = prompt(Locale.Chat.Rename, session.topic);
               if (newTopic && newTopic !== session.topic) {
                 chatStore.updateCurrentSession(
@@ -531,7 +528,7 @@ export function Chat(props: {
         <div className={styles["window-actions"]}>
           <div className={styles["window-action-button"] + " " + styles.mobile}>
             <IconButton
-              icon={<MenuIcon />}
+              icon={<ReturnIcon />}
               bordered
               title={Locale.Chat.Actions.ChatList}
               onClick={props?.showSideBar}
@@ -667,7 +664,7 @@ export function Chat(props: {
             onInput={(e) => onInput(e.currentTarget.value)}
             value={userInput}
             onKeyDown={onInputKeyDown}
-            onFocus={() => setAutoScroll(isMobileScreen())}
+            onFocus={() => setAutoScroll(true)}
             onBlur={() => {
               setAutoScroll(false);
               setTimeout(() => setPromptHints([]), 500);
@@ -679,7 +676,6 @@ export function Chat(props: {
             text={Locale.Chat.Send}
             className={styles["chat-input-send"]}
             noDark
-            disabled={!userInput}
             onClick={onUserSubmit}
           />
         </div>

+ 2 - 5
app/components/home.tsx

@@ -95,6 +95,7 @@ function _Home() {
       state.removeSession,
     ],
   );
+  const chatStore = useChatStore();
   const loading = !useHasHydrated();
   const [showSideBar, setShowSideBar] = useState(true);
 
@@ -144,11 +145,7 @@ function _Home() {
             <div className={styles["sidebar-action"] + " " + styles.mobile}>
               <IconButton
                 icon={<CloseIcon />}
-                onClick={() => {
-                  if (confirm(Locale.Home.DeleteChat)) {
-                    removeSession(currentIndex);
-                  }
-                }}
+                onClick={chatStore.deleteSession}
               />
             </div>
             <div className={styles["sidebar-action"]}>

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

@@ -135,9 +135,25 @@
     box-shadow: var(--card-shadow);
     border: var(--border-in-light);
     color: var(--black);
-    padding: 10px 30px;
+    padding: 10px 20px;
     border-radius: 50px;
     margin-bottom: 20px;
+    display: flex;
+    align-items: center;
+
+    .toast-action {
+      padding-left: 20px;
+      color: var(--primary);
+      opacity: 0.8;
+      border: 0;
+      background: none;
+      cursor: pointer;
+      font-family: inherit;
+
+      &:hover {
+        opacity: 1;
+      }
+    }
   }
 }
 
@@ -160,4 +176,4 @@
       max-height: 50vh;
     }
   }
-}
+}

+ 24 - 4
app/components/ui-lib.tsx

@@ -110,17 +110,37 @@ export function showModal(props: ModalProps) {
   root.render(<Modal {...props} onClose={closeModal}></Modal>);
 }
 
-export type ToastProps = { content: string };
+export type ToastProps = {
+  content: string;
+  action?: {
+    text: string;
+    onClick: () => void;
+  };
+};
 
 export function Toast(props: ToastProps) {
   return (
     <div className={styles["toast-container"]}>
-      <div className={styles["toast-content"]}>{props.content}</div>
+      <div className={styles["toast-content"]}>
+        <span>{props.content}</span>
+        {props.action && (
+          <button
+            onClick={props.action.onClick}
+            className={styles["toast-action"]}
+          >
+            {props.action.text}
+          </button>
+        )}
+      </div>
     </div>
   );
 }
 
-export function showToast(content: string, delay = 3000) {
+export function showToast(
+  content: string,
+  action?: ToastProps["action"],
+  delay = 3000,
+) {
   const div = document.createElement("div");
   div.className = styles.show;
   document.body.appendChild(div);
@@ -139,7 +159,7 @@ export function showToast(content: string, delay = 3000) {
     close();
   }, delay);
 
-  root.render(<Toast content={content} />);
+  root.render(<Toast content={content} action={action} />);
 }
 
 export type InputProps = React.HTMLProps<HTMLTextAreaElement> & {

+ 21 - 0
app/icons/return.svg

@@ -0,0 +1,21 @@
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="16"
+  height="16" viewBox="0 0 16 16" fill="none">
+  <defs>
+    <rect id="path_0" x="0" y="0" width="16" height="16" />
+  </defs>
+  <g opacity="1" transform="translate(0 0)  rotate(0 8 8)">
+    <mask id="bg-mask-0" fill="white">
+      <use xlink:href="#path_0"></use>
+    </mask>
+    <g mask="url(#bg-mask-0)">
+      <path id="路径 1"
+        style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0"
+        transform="translate(2 2.6666666666666665)  rotate(0 1.1666333333333334 2.1666666666666665)"
+        d="M2.33,0L0,2L2.33,4.33 " />
+      <path id="路径 2"
+        style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0"
+        transform="translate(2 4.666666666666666)  rotate(0 6.000006859869576 4.333333333333333)"
+        d="M0,0L7.66,0C9.96,0 11.91,1.87 12,4.17C12.09,6.59 10.09,8.67 7.66,8.67L2,8.67 " />
+    </g>
+  </g>
+</svg>

+ 2 - 0
app/locales/cn.ts

@@ -47,6 +47,8 @@ const cn = {
   Home: {
     NewChat: "新的聊天",
     DeleteChat: "确认删除选中的对话?",
+    DeleteToast: "已删除会话",
+    Revert: "撤销",
   },
   Settings: {
     Title: "设置",

+ 2 - 0
app/locales/en.ts

@@ -50,6 +50,8 @@ const en: LocaleType = {
   Home: {
     NewChat: "New Chat",
     DeleteChat: "Confirm to delete the selected conversation?",
+    DeleteToast: "Chat Deleted",
+    Revert: "Revert",
   },
   Settings: {
     Title: "Settings",

+ 2 - 0
app/locales/es.ts

@@ -50,6 +50,8 @@ const es: LocaleType = {
   Home: {
     NewChat: "Nuevo chat",
     DeleteChat: "¿Confirmar eliminación de la conversación seleccionada?",
+    DeleteToast: "Chat Deleted",
+    Revert: "Revert",
   },
   Settings: {
     Title: "Configuración",

+ 2 - 0
app/locales/it.ts

@@ -50,6 +50,8 @@ const it: LocaleType = {
   Home: {
     NewChat: "Nuova Chat",
     DeleteChat: "Confermare la cancellazione della conversazione selezionata?",
+    DeleteToast: "Chat Deleted",
+    Revert: "Revert",
   },
   Settings: {
     Title: "Impostazioni",

+ 2 - 0
app/locales/tw.ts

@@ -48,6 +48,8 @@ const tw: LocaleType = {
   Home: {
     NewChat: "新的對話",
     DeleteChat: "確定要刪除選取的對話嗎?",
+    DeleteToast: "已刪除對話",
+    Revert: "撤銷",
   },
   Settings: {
     Title: "設定",

+ 23 - 1
app/store/app.ts

@@ -7,9 +7,10 @@ import {
   requestChatStream,
   requestWithPrompt,
 } from "../requests";
-import { trimTopic } from "../utils";
+import { isMobileScreen, trimTopic } from "../utils";
 
 import Locale from "../locales";
+import { showToast } from "../components/ui-lib";
 
 export type Message = ChatCompletionResponseMessage & {
   date: string;
@@ -204,6 +205,7 @@ interface ChatStore {
   moveSession: (from: number, to: number) => void;
   selectSession: (index: number) => void;
   newSession: () => void;
+  deleteSession: () => void;
   currentSession: () => ChatSession;
   onNewMessage: (message: Message) => void;
   onUserInput: (content: string) => Promise<void>;
@@ -324,6 +326,26 @@ export const useChatStore = create<ChatStore>()(
         }));
       },
 
+      deleteSession() {
+        const deletedSession = get().currentSession();
+        const index = get().currentSessionIndex;
+        const isLastSession = get().sessions.length === 1;
+        if (!isMobileScreen() || confirm(Locale.Home.DeleteChat)) {
+          get().removeSession(index);
+        }
+        showToast(Locale.Home.DeleteToast, {
+          text: Locale.Home.Revert,
+          onClick() {
+            set((state) => ({
+              sessions: state.sessions
+                .slice(0, index)
+                .concat([deletedSession])
+                .concat(state.sessions.slice(index + Number(isLastSession))),
+            }));
+          },
+        });
+      },
+
       currentSession() {
         let index = get().currentSessionIndex;
         const sessions = get().sessions;

+ 7 - 9
app/utils.ts

@@ -7,11 +7,9 @@ export function trimTopic(topic: string) {
 }
 
 export async function copyToClipboard(text: string) {
-  if (navigator.clipboard) {
-    navigator.clipboard.writeText(text).catch((err) => {
-      console.error("Failed to copy: ", err);
-    });
-  } else {
+  try {
+    await navigator.clipboard.writeText(text);
+  } catch (error) {
     const textArea = document.createElement("textarea");
     textArea.value = text;
     document.body.appendChild(textArea);
@@ -19,11 +17,11 @@ export async function copyToClipboard(text: string) {
     textArea.select();
     try {
       document.execCommand("copy");
-      console.log("Text copied to clipboard");
-    } catch (err) {
-      console.error("Failed to copy: ", err);
+    } catch (error) {
+      showToast(Locale.Copy.Failed);
     }
-    document.body.removeChild(textArea);
+  } finally {
+    showToast(Locale.Copy.Success);
   }
 }
 

+ 10 - 0
docs/faq-cn.md

@@ -126,3 +126,13 @@ OpenAI只接受指定地区的信用卡(中国信用卡无法使用)。一
 
 ## 如何使用 Azure OpenAI 接口
 请参考:[#371](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/371)
+
+## 为什么我的 Token 消耗得这么快?
+> 相关讨论:[#518](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/518)
+- 如果你有 GPT 4 的权限,并且日常在使用 GPT 4 api,那么由于 GPT 4 价格是 GPT 3.5 的 15 倍左右,你的账单金额会急速膨胀;
+- 如果你在使用 GPT 3.5,并且使用频率并不高,仍然发现自己的账单金额在飞快增加,那么请马上按照以下步骤排查:
+  - 去 openai 官网查看你的 api key 消费记录,如果你的 token 每小时都有消费,并且每次都消耗了上万 token,那你的 key 一定是泄露了,请立即删除重新生成。**不要在乱七八糟的网站上查余额。**
+  - 如果你的密码设置很短,比如 5 位以内的字母,那么爆破成本是非常低的,建议你搜索一下 docker 的日志记录,确认是否有人大量尝试了密码组合,关键字:got access code
+- 通过上述两个方法就可以定位到你的 token 被快速消耗的原因:
+  - 如果 openai 消费记录异常,但是 docker 日志没有问题,那么说明是 api key 泄露;
+  - 如果 docker 日志发现大量 got access code 爆破记录,那么就是密码被爆破了。