Browse Source

feat: support compress chat history

Yifei Zhang 2 years ago
parent
commit
c133cae04b
6 changed files with 125 additions and 24 deletions
  1. 14 1
      app/components/home.tsx
  2. 32 4
      app/components/settings.tsx
  3. 2 2
      app/components/ui-lib.module.scss
  4. 1 0
      app/icons/clear.svg
  5. 65 17
      app/store.ts
  6. 11 0
      app/styles/globals.scss

+ 14 - 1
app/components/home.tsx

@@ -208,7 +208,10 @@ export function Chat(props: { showSideBar?: () => void }) {
             <IconButton
               icon={<BrainIcon />}
               bordered
-              title="查看压缩后的历史 Prompt(开发中)"
+              title="查看压缩后的历史 Prompt"
+              onClick={() => {
+                showMemoryPrompt(session.memoryPrompt)
+              }}
             />
           </div>
           <div className={styles["window-action-button"]}>
@@ -320,6 +323,16 @@ function exportMessages(messages: Message[], topic: string) {
   })
 }
 
+function showMemoryPrompt(prompt: string) {
+  showModal({
+    title: "上下文记忆 Prompt", children: <div className="markdown-body">
+      <pre className={styles['export-content']}>{prompt}</pre>
+    </div>, actions: [
+      <IconButton key="copy" icon={<CopyIcon />} bordered text="全部复制" onClick={() => copyToClipboard(prompt)} />,
+    ]
+  })
+}
+
 export function Home() {
   const [createNewSession] = useChatStore((state) => [state.newSession]);
   const loading = !useChatStore?.persist?.hasHydrated();

+ 32 - 4
app/components/settings.tsx

@@ -6,6 +6,7 @@ import styles from "./settings.module.scss";
 
 import ResetIcon from "../icons/reload.svg";
 import CloseIcon from "../icons/close.svg";
+import ClearIcon from "../icons/clear.svg";
 
 import { List, ListItem, Popover } from "./ui-lib";
 
@@ -15,10 +16,11 @@ import { Avatar } from "./home";
 
 export function Settings(props: { closeSettings: () => void }) {
   const [showEmojiPicker, setShowEmojiPicker] = useState(false);
-  const [config, updateConfig, resetConfig] = useChatStore((state) => [
+  const [config, updateConfig, resetConfig, clearAllData] = useChatStore((state) => [
     state.config,
     state.updateConfig,
     state.resetConfig,
+    state.clearAllData,
   ]);
 
   return (
@@ -31,10 +33,10 @@ export function Settings(props: { closeSettings: () => void }) {
         <div className={styles["window-actions"]}>
           <div className={styles["window-action-button"]}>
             <IconButton
-              icon={<CloseIcon />}
-              onClick={props.closeSettings}
+              icon={<ClearIcon />}
+              onClick={clearAllData}
               bordered
-              title="重置所有选项"
+              title="清除所有数据"
             />
           </div>
           <div className={styles["window-action-button"]}>
@@ -45,6 +47,14 @@ export function Settings(props: { closeSettings: () => void }) {
               title="重置所有选项"
             />
           </div>
+          <div className={styles["window-action-button"]}>
+            <IconButton
+              icon={<CloseIcon />}
+              onClick={props.closeSettings}
+              bordered
+              title="关闭"
+            />
+          </div>
         </div>
       </div>
       <div className={styles["settings"]}>
@@ -147,6 +157,24 @@ export function Settings(props: { closeSettings: () => void }) {
             ></input>
           </ListItem>
 
+
+          <ListItem>
+            <div className={styles["settings-title"]}>
+              历史消息压缩长度阈值
+            </div>
+            <input
+              type="number"
+              min={500}
+              max={4000}
+              value={config.compressMessageLengthThreshold}
+              onChange={(e) =>
+                updateConfig(
+                  (config) => (config.compressMessageLengthThreshold = e.currentTarget.valueAsNumber)
+                )
+              }
+            ></input>
+          </ListItem>
+
           <ListItem>
             <div className={styles["settings-title"]}>
               上下文中包含机器人消息

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

@@ -90,7 +90,7 @@
   }
 
   .modal-content {
-    height: 40vh;
+    max-height: 40vh;
     padding: var(--modal-padding);
     overflow: auto;
   }
@@ -118,7 +118,7 @@
     width: 90vw;
 
     .modal-content {
-      height: 50vh;
+      max-height: 50vh;
     }
   }
 }

+ 1 - 0
app/icons/clear.svg

@@ -0,0 +1 @@
+<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.6666666666666665 5)  rotate(0 5.333333333333333 4.833333333333333)" d="M1,9.67L9.67,9.67L10.67,0L0,0L1,9.67Z " /><path  id="路径 2" style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0" transform="translate(6.667333333333333 8.334133333333334)  rotate(0 0 1.6666999999999998)" d="M0,0L0,3.33 " /><path  id="路径 3" style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0" transform="translate(9.334133333333334 8.333166666666667)  rotate(0 0 1.666283333333333)" d="M0,0L0,3.33 " /><path  id="路径 4" style="stroke:#333333; stroke-width:1.3333333333333333; stroke-opacity:1; stroke-dasharray:0 0" transform="translate(4 1)  rotate(0 4 2)" d="M0,4L5.44,0L8,4 " /></g></g></svg>

+ 65 - 17
app/store.ts

@@ -26,6 +26,7 @@ export enum Theme {
 interface ChatConfig {
   maxToken?: number;
   historyMessageCount: number; // -1 means all
+  compressMessageLengthThreshold: number;
   sendBotMessages: boolean; // send bot's message or not
   submitKey: SubmitKey;
   avatar: string;
@@ -35,9 +36,10 @@ interface ChatConfig {
 
 const DEFAULT_CONFIG: ChatConfig = {
   historyMessageCount: 5,
-  sendBotMessages: false as boolean,
+  compressMessageLengthThreshold: 500,
+  sendBotMessages: true as boolean,
   submitKey: SubmitKey.CtrlEnter as SubmitKey,
-  avatar: "1fae0",
+  avatar: "1f603",
   theme: Theme.Auto as Theme,
   tightBorder: false,
 };
@@ -55,7 +57,7 @@ interface ChatSession {
   messages: Message[];
   stat: ChatStat;
   lastUpdate: string;
-  deleted?: boolean;
+  lastSummarizeIndex: number;
 }
 
 const DEFAULT_TOPIC = "新的聊天";
@@ -80,6 +82,7 @@ function createEmptySession(): ChatSession {
       charCount: 0,
     },
     lastUpdate: createDate,
+    lastSummarizeIndex: 0,
   };
 }
 
@@ -93,7 +96,6 @@ interface ChatStore {
   currentSession: () => ChatSession;
   onNewMessage: (message: Message) => void;
   onUserInput: (content: string) => Promise<void>;
-  onBotResponse: (message: Message) => void;
   summarizeSession: () => void;
   updateStat: (message: Message) => void;
   updateCurrentSession: (updater: (session: ChatSession) => void) => void;
@@ -102,10 +104,12 @@ interface ChatStore {
     messageIndex: number,
     updater: (message?: Message) => void
   ) => void;
+  getMessagesWithMemory: () => Message[];
 
   getConfig: () => ChatConfig;
   resetConfig: () => void;
   updateConfig: (updater: (config: ChatConfig) => void) => void;
+  clearAllData: () => void;
 }
 
 const LOCAL_KEY = "chat-next-web-store";
@@ -186,9 +190,6 @@ export const useChatStore = create<ChatStore>()(
       },
 
       onNewMessage(message) {
-        get().updateCurrentSession((session) => {
-          session.messages.push(message);
-        });
         get().updateStat(message);
         get().summarizeSession();
       },
@@ -200,9 +201,12 @@ export const useChatStore = create<ChatStore>()(
           date: new Date().toLocaleString(),
         };
 
+        get().updateCurrentSession((session) => {
+          session.messages.push(message);
+        });
+
         // get last five messges
         const messages = get().currentSession().messages.concat(message);
-        get().onNewMessage(message);
 
         const botMessage: Message = {
           content: "",
@@ -215,14 +219,13 @@ export const useChatStore = create<ChatStore>()(
           session.messages.push(botMessage);
         });
 
-        const fiveMessages = messages.slice(-5);
+        const recentMessages = get().getMessagesWithMemory()
 
-        requestChatStream(fiveMessages, {
+        requestChatStream(recentMessages, {
           onMessage(content, done) {
             if (done) {
               botMessage.streaming = false;
-              get().updateStat(botMessage);
-              get().summarizeSession();
+              get().onNewMessage(botMessage)
             } else {
               botMessage.content = content;
               set(() => ({}));
@@ -237,6 +240,24 @@ export const useChatStore = create<ChatStore>()(
         });
       },
 
+      getMessagesWithMemory() {
+        const session = get().currentSession()
+        const config = get().config
+        const recentMessages = session.messages.slice(-config.historyMessageCount);
+
+        const memoryPrompt: Message = {
+          role: 'system',
+          content: '这是你和用户的历史聊天总结:' + session.memoryPrompt,
+          date: ''
+        }
+
+        if (session.memoryPrompt) {
+          recentMessages.unshift(memoryPrompt)
+        }
+
+        return recentMessages
+      },
+
       updateMessage(
         sessionIndex: number,
         messageIndex: number,
@@ -249,10 +270,6 @@ export const useChatStore = create<ChatStore>()(
         set(() => ({ sessions }));
       },
 
-      onBotResponse(message) {
-        get().onNewMessage(message);
-      },
-
       summarizeSession() {
         const session = get().currentSession();
 
@@ -260,13 +277,37 @@ export const useChatStore = create<ChatStore>()(
           // should summarize topic
           requestWithPrompt(
             session.messages,
-            "直接返回这句话的简要主题,不要解释"
+            "直接返回这句话的简要主题,不要解释,如果没有主题,请直接返回“闲聊”"
           ).then((res) => {
             get().updateCurrentSession(
               (session) => (session.topic = trimTopic(res))
             );
           });
         }
+
+        const messages = get().getMessagesWithMemory()
+        const toBeSummarizedMsgs = messages.slice(session.lastSummarizeIndex)
+        const historyMsgLength = session.memoryPrompt.length + toBeSummarizedMsgs.reduce((pre, cur) => pre + cur.content.length, 0)
+        const lastSummarizeIndex = messages.length
+        if (historyMsgLength > 500) {
+          requestChatStream(toBeSummarizedMsgs.concat({
+            role: 'system',
+            content: '总结一下你和用户的对话,用作后续的上下文提示 prompt,控制在 50 字以内',
+            date: ''
+          }), {
+            filterBot: false,
+            onMessage(message, done) {
+              session.memoryPrompt = message
+              session.lastSummarizeIndex = lastSummarizeIndex
+              if (done) {
+                console.log('[Memory] ', session.memoryPrompt)
+              }
+            },
+            onError(error) {
+              console.error('[Summarize] ', error)
+            },
+          })
+        }
       },
 
       updateStat(message) {
@@ -282,6 +323,13 @@ export const useChatStore = create<ChatStore>()(
         updater(sessions[index]);
         set(() => ({ sessions }));
       },
+
+      clearAllData() {
+        if (confirm('确认清除所有聊天、设置数据?')) {
+          localStorage.clear()
+          location.reload()
+        }
+      },
     }),
     {
       name: LOCAL_KEY,

+ 11 - 0
app/styles/globals.scss

@@ -161,6 +161,17 @@ input[type="range"]::-webkit-slider-thumb:hover {
   width: 24px;
 }
 
+input[type="number"] {
+  appearance: none;
+  border-radius: 10px;
+  border: var(--border-in-light);
+  height: 32px;
+  box-sizing: border-box;
+  background: var(--white);
+  color: var(--black);
+  padding: 0 10px;
+}
+
 div.math {
   overflow-x: auto;
 }