From c133cae04b7427723c34028803684288018374da Mon Sep 17 00:00:00 2001 From: Yifei Zhang Date: Sun, 19 Mar 2023 15:13:10 +0000 Subject: [PATCH] feat: support compress chat history --- app/components/home.tsx | 15 +++++- app/components/settings.tsx | 36 ++++++++++++-- app/components/ui-lib.module.scss | 4 +- app/icons/clear.svg | 1 + app/store.ts | 82 ++++++++++++++++++++++++------- app/styles/globals.scss | 11 +++++ 6 files changed, 125 insertions(+), 24 deletions(-) create mode 100644 app/icons/clear.svg diff --git a/app/components/home.tsx b/app/components/home.tsx index f8c1b238..1215aac8 100644 --- a/app/components/home.tsx +++ b/app/components/home.tsx @@ -208,7 +208,10 @@ export function Chat(props: { showSideBar?: () => void }) { } bordered - title="查看压缩后的历史 Prompt(开发中)" + title="查看压缩后的历史 Prompt" + onClick={() => { + showMemoryPrompt(session.memoryPrompt) + }} />
@@ -320,6 +323,16 @@ function exportMessages(messages: Message[], topic: string) { }) } +function showMemoryPrompt(prompt: string) { + showModal({ + title: "上下文记忆 Prompt", children:
+
{prompt}
+
, actions: [ + } bordered text="全部复制" onClick={() => copyToClipboard(prompt)} />, + ] + }) +} + export function Home() { const [createNewSession] = useChatStore((state) => [state.newSession]); const loading = !useChatStore?.persist?.hasHydrated(); diff --git a/app/components/settings.tsx b/app/components/settings.tsx index 206cdfad..435df4c5 100644 --- a/app/components/settings.tsx +++ b/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 }) {
} - onClick={props.closeSettings} + icon={} + onClick={clearAllData} bordered - title="重置所有选项" + title="清除所有数据" />
@@ -45,6 +47,14 @@ export function Settings(props: { closeSettings: () => void }) { title="重置所有选项" />
+
+ } + onClick={props.closeSettings} + bordered + title="关闭" + /> +
@@ -147,6 +157,24 @@ export function Settings(props: { closeSettings: () => void }) { > + + +
+ 历史消息压缩长度阈值 +
+ + updateConfig( + (config) => (config.compressMessageLengthThreshold = e.currentTarget.valueAsNumber) + ) + } + > +
+
上下文中包含机器人消息 diff --git a/app/components/ui-lib.module.scss b/app/components/ui-lib.module.scss index a86b4b9e..1a378141 100644 --- a/app/components/ui-lib.module.scss +++ b/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; } } } \ No newline at end of file diff --git a/app/icons/clear.svg b/app/icons/clear.svg new file mode 100644 index 00000000..f0430fc4 --- /dev/null +++ b/app/icons/clear.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/store.ts b/app/store.ts index 2124b23d..267e1e05 100644 --- a/app/store.ts +++ b/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; - 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()( }, onNewMessage(message) { - get().updateCurrentSession((session) => { - session.messages.push(message); - }); get().updateStat(message); get().summarizeSession(); }, @@ -200,9 +201,12 @@ export const useChatStore = create()( 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()( 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()( }); }, + 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()( set(() => ({ sessions })); }, - onBotResponse(message) { - get().onNewMessage(message); - }, - summarizeSession() { const session = get().currentSession(); @@ -260,13 +277,37 @@ export const useChatStore = create()( // 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()( updater(sessions[index]); set(() => ({ sessions })); }, + + clearAllData() { + if (confirm('确认清除所有聊天、设置数据?')) { + localStorage.clear() + location.reload() + } + }, }), { name: LOCAL_KEY, diff --git a/app/styles/globals.scss b/app/styles/globals.scss index 4d72994b..af0b092b 100644 --- a/app/styles/globals.scss +++ b/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; }