forked from XiaoMo/ChatGPT-Next-Web
feat: support compress chat history
This commit is contained in:
parent
9398b34b5c
commit
c133cae04b
@ -208,7 +208,10 @@ export function Chat(props: { showSideBar?: () => void }) {
|
|||||||
<IconButton
|
<IconButton
|
||||||
icon={<BrainIcon />}
|
icon={<BrainIcon />}
|
||||||
bordered
|
bordered
|
||||||
title="查看压缩后的历史 Prompt(开发中)"
|
title="查看压缩后的历史 Prompt"
|
||||||
|
onClick={() => {
|
||||||
|
showMemoryPrompt(session.memoryPrompt)
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles["window-action-button"]}>
|
<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() {
|
export function Home() {
|
||||||
const [createNewSession] = useChatStore((state) => [state.newSession]);
|
const [createNewSession] = useChatStore((state) => [state.newSession]);
|
||||||
const loading = !useChatStore?.persist?.hasHydrated();
|
const loading = !useChatStore?.persist?.hasHydrated();
|
||||||
|
@ -6,6 +6,7 @@ import styles from "./settings.module.scss";
|
|||||||
|
|
||||||
import ResetIcon from "../icons/reload.svg";
|
import ResetIcon from "../icons/reload.svg";
|
||||||
import CloseIcon from "../icons/close.svg";
|
import CloseIcon from "../icons/close.svg";
|
||||||
|
import ClearIcon from "../icons/clear.svg";
|
||||||
|
|
||||||
import { List, ListItem, Popover } from "./ui-lib";
|
import { List, ListItem, Popover } from "./ui-lib";
|
||||||
|
|
||||||
@ -15,10 +16,11 @@ import { Avatar } from "./home";
|
|||||||
|
|
||||||
export function Settings(props: { closeSettings: () => void }) {
|
export function Settings(props: { closeSettings: () => void }) {
|
||||||
const [showEmojiPicker, setShowEmojiPicker] = useState(false);
|
const [showEmojiPicker, setShowEmojiPicker] = useState(false);
|
||||||
const [config, updateConfig, resetConfig] = useChatStore((state) => [
|
const [config, updateConfig, resetConfig, clearAllData] = useChatStore((state) => [
|
||||||
state.config,
|
state.config,
|
||||||
state.updateConfig,
|
state.updateConfig,
|
||||||
state.resetConfig,
|
state.resetConfig,
|
||||||
|
state.clearAllData,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -31,10 +33,10 @@ export function Settings(props: { closeSettings: () => void }) {
|
|||||||
<div className={styles["window-actions"]}>
|
<div className={styles["window-actions"]}>
|
||||||
<div className={styles["window-action-button"]}>
|
<div className={styles["window-action-button"]}>
|
||||||
<IconButton
|
<IconButton
|
||||||
icon={<CloseIcon />}
|
icon={<ClearIcon />}
|
||||||
onClick={props.closeSettings}
|
onClick={clearAllData}
|
||||||
bordered
|
bordered
|
||||||
title="重置所有选项"
|
title="清除所有数据"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles["window-action-button"]}>
|
<div className={styles["window-action-button"]}>
|
||||||
@ -45,6 +47,14 @@ export function Settings(props: { closeSettings: () => void }) {
|
|||||||
title="重置所有选项"
|
title="重置所有选项"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div className={styles["window-action-button"]}>
|
||||||
|
<IconButton
|
||||||
|
icon={<CloseIcon />}
|
||||||
|
onClick={props.closeSettings}
|
||||||
|
bordered
|
||||||
|
title="关闭"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles["settings"]}>
|
<div className={styles["settings"]}>
|
||||||
@ -147,6 +157,24 @@ export function Settings(props: { closeSettings: () => void }) {
|
|||||||
></input>
|
></input>
|
||||||
</ListItem>
|
</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>
|
<ListItem>
|
||||||
<div className={styles["settings-title"]}>
|
<div className={styles["settings-title"]}>
|
||||||
上下文中包含机器人消息
|
上下文中包含机器人消息
|
||||||
|
@ -90,7 +90,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.modal-content {
|
.modal-content {
|
||||||
height: 40vh;
|
max-height: 40vh;
|
||||||
padding: var(--modal-padding);
|
padding: var(--modal-padding);
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
}
|
}
|
||||||
@ -118,7 +118,7 @@
|
|||||||
width: 90vw;
|
width: 90vw;
|
||||||
|
|
||||||
.modal-content {
|
.modal-content {
|
||||||
height: 50vh;
|
max-height: 50vh;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
1
app/icons/clear.svg
Normal file
1
app/icons/clear.svg
Normal file
@ -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>
|
After Width: | Height: | Size: 1.2 KiB |
82
app/store.ts
82
app/store.ts
@ -26,6 +26,7 @@ export enum Theme {
|
|||||||
interface ChatConfig {
|
interface ChatConfig {
|
||||||
maxToken?: number;
|
maxToken?: number;
|
||||||
historyMessageCount: number; // -1 means all
|
historyMessageCount: number; // -1 means all
|
||||||
|
compressMessageLengthThreshold: number;
|
||||||
sendBotMessages: boolean; // send bot's message or not
|
sendBotMessages: boolean; // send bot's message or not
|
||||||
submitKey: SubmitKey;
|
submitKey: SubmitKey;
|
||||||
avatar: string;
|
avatar: string;
|
||||||
@ -35,9 +36,10 @@ interface ChatConfig {
|
|||||||
|
|
||||||
const DEFAULT_CONFIG: ChatConfig = {
|
const DEFAULT_CONFIG: ChatConfig = {
|
||||||
historyMessageCount: 5,
|
historyMessageCount: 5,
|
||||||
sendBotMessages: false as boolean,
|
compressMessageLengthThreshold: 500,
|
||||||
|
sendBotMessages: true as boolean,
|
||||||
submitKey: SubmitKey.CtrlEnter as SubmitKey,
|
submitKey: SubmitKey.CtrlEnter as SubmitKey,
|
||||||
avatar: "1fae0",
|
avatar: "1f603",
|
||||||
theme: Theme.Auto as Theme,
|
theme: Theme.Auto as Theme,
|
||||||
tightBorder: false,
|
tightBorder: false,
|
||||||
};
|
};
|
||||||
@ -55,7 +57,7 @@ interface ChatSession {
|
|||||||
messages: Message[];
|
messages: Message[];
|
||||||
stat: ChatStat;
|
stat: ChatStat;
|
||||||
lastUpdate: string;
|
lastUpdate: string;
|
||||||
deleted?: boolean;
|
lastSummarizeIndex: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const DEFAULT_TOPIC = "新的聊天";
|
const DEFAULT_TOPIC = "新的聊天";
|
||||||
@ -80,6 +82,7 @@ function createEmptySession(): ChatSession {
|
|||||||
charCount: 0,
|
charCount: 0,
|
||||||
},
|
},
|
||||||
lastUpdate: createDate,
|
lastUpdate: createDate,
|
||||||
|
lastSummarizeIndex: 0,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -93,7 +96,6 @@ interface ChatStore {
|
|||||||
currentSession: () => ChatSession;
|
currentSession: () => ChatSession;
|
||||||
onNewMessage: (message: Message) => void;
|
onNewMessage: (message: Message) => void;
|
||||||
onUserInput: (content: string) => Promise<void>;
|
onUserInput: (content: string) => Promise<void>;
|
||||||
onBotResponse: (message: Message) => void;
|
|
||||||
summarizeSession: () => void;
|
summarizeSession: () => void;
|
||||||
updateStat: (message: Message) => void;
|
updateStat: (message: Message) => void;
|
||||||
updateCurrentSession: (updater: (session: ChatSession) => void) => void;
|
updateCurrentSession: (updater: (session: ChatSession) => void) => void;
|
||||||
@ -102,10 +104,12 @@ interface ChatStore {
|
|||||||
messageIndex: number,
|
messageIndex: number,
|
||||||
updater: (message?: Message) => void
|
updater: (message?: Message) => void
|
||||||
) => void;
|
) => void;
|
||||||
|
getMessagesWithMemory: () => Message[];
|
||||||
|
|
||||||
getConfig: () => ChatConfig;
|
getConfig: () => ChatConfig;
|
||||||
resetConfig: () => void;
|
resetConfig: () => void;
|
||||||
updateConfig: (updater: (config: ChatConfig) => void) => void;
|
updateConfig: (updater: (config: ChatConfig) => void) => void;
|
||||||
|
clearAllData: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const LOCAL_KEY = "chat-next-web-store";
|
const LOCAL_KEY = "chat-next-web-store";
|
||||||
@ -186,9 +190,6 @@ export const useChatStore = create<ChatStore>()(
|
|||||||
},
|
},
|
||||||
|
|
||||||
onNewMessage(message) {
|
onNewMessage(message) {
|
||||||
get().updateCurrentSession((session) => {
|
|
||||||
session.messages.push(message);
|
|
||||||
});
|
|
||||||
get().updateStat(message);
|
get().updateStat(message);
|
||||||
get().summarizeSession();
|
get().summarizeSession();
|
||||||
},
|
},
|
||||||
@ -200,9 +201,12 @@ export const useChatStore = create<ChatStore>()(
|
|||||||
date: new Date().toLocaleString(),
|
date: new Date().toLocaleString(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
get().updateCurrentSession((session) => {
|
||||||
|
session.messages.push(message);
|
||||||
|
});
|
||||||
|
|
||||||
// get last five messges
|
// get last five messges
|
||||||
const messages = get().currentSession().messages.concat(message);
|
const messages = get().currentSession().messages.concat(message);
|
||||||
get().onNewMessage(message);
|
|
||||||
|
|
||||||
const botMessage: Message = {
|
const botMessage: Message = {
|
||||||
content: "",
|
content: "",
|
||||||
@ -215,14 +219,13 @@ export const useChatStore = create<ChatStore>()(
|
|||||||
session.messages.push(botMessage);
|
session.messages.push(botMessage);
|
||||||
});
|
});
|
||||||
|
|
||||||
const fiveMessages = messages.slice(-5);
|
const recentMessages = get().getMessagesWithMemory()
|
||||||
|
|
||||||
requestChatStream(fiveMessages, {
|
requestChatStream(recentMessages, {
|
||||||
onMessage(content, done) {
|
onMessage(content, done) {
|
||||||
if (done) {
|
if (done) {
|
||||||
botMessage.streaming = false;
|
botMessage.streaming = false;
|
||||||
get().updateStat(botMessage);
|
get().onNewMessage(botMessage)
|
||||||
get().summarizeSession();
|
|
||||||
} else {
|
} else {
|
||||||
botMessage.content = content;
|
botMessage.content = content;
|
||||||
set(() => ({}));
|
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(
|
updateMessage(
|
||||||
sessionIndex: number,
|
sessionIndex: number,
|
||||||
messageIndex: number,
|
messageIndex: number,
|
||||||
@ -249,10 +270,6 @@ export const useChatStore = create<ChatStore>()(
|
|||||||
set(() => ({ sessions }));
|
set(() => ({ sessions }));
|
||||||
},
|
},
|
||||||
|
|
||||||
onBotResponse(message) {
|
|
||||||
get().onNewMessage(message);
|
|
||||||
},
|
|
||||||
|
|
||||||
summarizeSession() {
|
summarizeSession() {
|
||||||
const session = get().currentSession();
|
const session = get().currentSession();
|
||||||
|
|
||||||
@ -260,13 +277,37 @@ export const useChatStore = create<ChatStore>()(
|
|||||||
// should summarize topic
|
// should summarize topic
|
||||||
requestWithPrompt(
|
requestWithPrompt(
|
||||||
session.messages,
|
session.messages,
|
||||||
"直接返回这句话的简要主题,不要解释"
|
"直接返回这句话的简要主题,不要解释,如果没有主题,请直接返回“闲聊”"
|
||||||
).then((res) => {
|
).then((res) => {
|
||||||
get().updateCurrentSession(
|
get().updateCurrentSession(
|
||||||
(session) => (session.topic = trimTopic(res))
|
(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) {
|
updateStat(message) {
|
||||||
@ -282,6 +323,13 @@ export const useChatStore = create<ChatStore>()(
|
|||||||
updater(sessions[index]);
|
updater(sessions[index]);
|
||||||
set(() => ({ sessions }));
|
set(() => ({ sessions }));
|
||||||
},
|
},
|
||||||
|
|
||||||
|
clearAllData() {
|
||||||
|
if (confirm('确认清除所有聊天、设置数据?')) {
|
||||||
|
localStorage.clear()
|
||||||
|
location.reload()
|
||||||
|
}
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
name: LOCAL_KEY,
|
name: LOCAL_KEY,
|
||||||
|
@ -161,6 +161,17 @@ input[type="range"]::-webkit-slider-thumb:hover {
|
|||||||
width: 24px;
|
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 {
|
div.math {
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user