ChatGPT-Next-Web/app/store.ts

359 lines
9.4 KiB
TypeScript
Raw Normal View History

2023-03-10 18:25:33 +00:00
import { create } from "zustand";
import { persist } from "zustand/middleware";
2023-03-09 17:01:40 +00:00
import { type ChatCompletionResponseMessage } from "openai";
2023-03-11 12:54:24 +00:00
import { requestChat, requestChatStream, requestWithPrompt } from "./requests";
2023-03-10 18:25:33 +00:00
import { trimTopic } from "./utils";
2023-03-09 17:01:40 +00:00
2023-03-10 18:25:33 +00:00
export type Message = ChatCompletionResponseMessage & {
date: string;
2023-03-11 12:54:24 +00:00
streaming?: boolean;
2023-03-10 18:25:33 +00:00
};
2023-03-09 17:01:40 +00:00
2023-03-11 17:14:07 +00:00
export enum SubmitKey {
Enter = "Enter",
CtrlEnter = "Ctrl + Enter",
ShiftEnter = "Shift + Enter",
AltEnter = "Alt + Enter",
}
2023-03-12 19:06:21 +00:00
export enum Theme {
Auto = "auto",
Dark = "dark",
Light = "light",
}
2023-03-19 16:09:30 +00:00
export interface ChatConfig {
maxToken?: number
2023-03-11 17:14:07 +00:00
historyMessageCount: number; // -1 means all
2023-03-19 15:13:10 +00:00
compressMessageLengthThreshold: number;
2023-03-11 17:14:07 +00:00
sendBotMessages: boolean; // send bot's message or not
submitKey: SubmitKey;
avatar: string;
2023-03-12 19:06:21 +00:00
theme: Theme;
2023-03-12 19:21:48 +00:00
tightBorder: boolean;
2023-03-09 17:01:40 +00:00
}
2023-03-12 19:21:48 +00:00
const DEFAULT_CONFIG: ChatConfig = {
2023-03-19 16:09:30 +00:00
historyMessageCount: 4,
2023-03-19 16:29:09 +00:00
compressMessageLengthThreshold: 1000,
2023-03-19 15:13:10 +00:00
sendBotMessages: true as boolean,
2023-03-12 19:21:48 +00:00
submitKey: SubmitKey.CtrlEnter as SubmitKey,
2023-03-19 15:13:10 +00:00
avatar: "1f603",
2023-03-12 19:21:48 +00:00
theme: Theme.Auto as Theme,
tightBorder: false,
};
2023-03-19 16:09:30 +00:00
export interface ChatStat {
2023-03-10 18:25:33 +00:00
tokenCount: number;
wordCount: number;
charCount: number;
}
2023-03-19 16:09:30 +00:00
export interface ChatSession {
2023-03-11 17:14:07 +00:00
id: number;
2023-03-10 18:25:33 +00:00
topic: string;
memoryPrompt: string;
messages: Message[];
stat: ChatStat;
lastUpdate: string;
2023-03-19 15:13:10 +00:00
lastSummarizeIndex: number;
2023-03-10 18:25:33 +00:00
}
const DEFAULT_TOPIC = "新的聊天";
function createEmptySession(): ChatSession {
const createDate = new Date().toLocaleString();
return {
2023-03-11 17:14:07 +00:00
id: Date.now(),
2023-03-10 18:25:33 +00:00
topic: DEFAULT_TOPIC,
memoryPrompt: "",
messages: [
{
role: "assistant",
content: "有什么可以帮你的吗",
date: createDate,
},
],
stat: {
tokenCount: 0,
wordCount: 0,
charCount: 0,
},
lastUpdate: createDate,
2023-03-19 15:13:10 +00:00
lastSummarizeIndex: 0,
2023-03-10 18:25:33 +00:00
};
2023-03-09 17:01:40 +00:00
}
2023-03-10 18:25:33 +00:00
interface ChatStore {
2023-03-11 17:14:07 +00:00
config: ChatConfig;
2023-03-10 18:25:33 +00:00
sessions: ChatSession[];
currentSessionIndex: number;
removeSession: (index: number) => void;
selectSession: (index: number) => void;
newSession: () => void;
currentSession: () => ChatSession;
onNewMessage: (message: Message) => void;
onUserInput: (content: string) => Promise<void>;
summarizeSession: () => void;
updateStat: (message: Message) => void;
updateCurrentSession: (updater: (session: ChatSession) => void) => void;
2023-03-11 12:54:24 +00:00
updateMessage: (
sessionIndex: number,
messageIndex: number,
updater: (message?: Message) => void
) => void;
2023-03-19 15:13:10 +00:00
getMessagesWithMemory: () => Message[];
2023-03-19 16:29:09 +00:00
getMemoryPrompt: () => Message,
2023-03-11 17:14:07 +00:00
getConfig: () => ChatConfig;
2023-03-12 19:21:48 +00:00
resetConfig: () => void;
2023-03-11 17:14:07 +00:00
updateConfig: (updater: (config: ChatConfig) => void) => void;
2023-03-19 15:13:10 +00:00
clearAllData: () => void;
2023-03-09 17:01:40 +00:00
}
2023-03-13 16:25:07 +00:00
const LOCAL_KEY = "chat-next-web-store";
2023-03-10 18:25:33 +00:00
export const useChatStore = create<ChatStore>()(
persist(
(set, get) => ({
sessions: [createEmptySession()],
currentSessionIndex: 0,
2023-03-11 17:14:07 +00:00
config: {
2023-03-12 19:21:48 +00:00
...DEFAULT_CONFIG,
},
resetConfig() {
set(() => ({ config: { ...DEFAULT_CONFIG } }));
2023-03-11 17:14:07 +00:00
},
getConfig() {
return get().config;
},
updateConfig(updater) {
const config = get().config;
updater(config);
set(() => ({ config }));
},
2023-03-10 18:25:33 +00:00
selectSession(index: number) {
set({
currentSessionIndex: index,
});
},
removeSession(index: number) {
set((state) => {
let nextIndex = state.currentSessionIndex;
const sessions = state.sessions;
if (sessions.length === 1) {
return {
currentSessionIndex: 0,
sessions: [createEmptySession()],
};
}
sessions.splice(index, 1);
if (nextIndex === index) {
nextIndex -= 1;
}
return {
currentSessionIndex: nextIndex,
sessions,
};
});
},
newSession() {
set((state) => ({
2023-03-11 08:24:17 +00:00
currentSessionIndex: 0,
sessions: [createEmptySession()].concat(state.sessions),
2023-03-10 18:25:33 +00:00
}));
},
currentSession() {
let index = get().currentSessionIndex;
const sessions = get().sessions;
if (index < 0 || index >= sessions.length) {
index = Math.min(sessions.length - 1, Math.max(0, index));
set(() => ({ currentSessionIndex: index }));
}
2023-03-11 17:14:07 +00:00
const session = sessions[index];
return session;
2023-03-10 18:25:33 +00:00
},
onNewMessage(message) {
2023-03-19 16:09:30 +00:00
get().updateCurrentSession(session => {
session.lastUpdate = new Date().toLocaleString()
})
2023-03-10 18:25:33 +00:00
get().updateStat(message);
get().summarizeSession();
},
async onUserInput(content) {
2023-03-19 16:09:30 +00:00
const userMessage: Message = {
2023-03-10 18:25:33 +00:00
role: "user",
content,
date: new Date().toLocaleString(),
};
2023-03-11 12:54:24 +00:00
const botMessage: Message = {
content: "",
role: "assistant",
2023-03-10 18:25:33 +00:00
date: new Date().toLocaleString(),
2023-03-11 12:54:24 +00:00
streaming: true,
};
2023-03-19 16:09:30 +00:00
// get recent messages
const recentMessages = get().getMessagesWithMemory()
const sendMessages = recentMessages.concat(userMessage)
// save user's and bot's message
2023-03-11 12:54:24 +00:00
get().updateCurrentSession((session) => {
2023-03-19 16:09:30 +00:00
session.messages.push(userMessage);
2023-03-11 12:54:24 +00:00
session.messages.push(botMessage);
});
2023-03-19 16:09:30 +00:00
console.log('[User Input] ', sendMessages)
requestChatStream(sendMessages, {
2023-03-11 12:54:24 +00:00
onMessage(content, done) {
if (done) {
2023-03-11 17:14:07 +00:00
botMessage.streaming = false;
2023-03-19 15:13:10 +00:00
get().onNewMessage(botMessage)
2023-03-11 12:54:24 +00:00
} else {
botMessage.content = content;
set(() => ({}));
}
},
2023-03-11 17:14:07 +00:00
onError(error) {
2023-03-13 16:34:52 +00:00
botMessage.content += "\n\n出错了稍后重试吧";
2023-03-11 17:14:07 +00:00
botMessage.streaming = false;
set(() => ({}));
},
filterBot: !get().config.sendBotMessages,
2023-03-10 18:25:33 +00:00
});
},
2023-03-19 16:29:09 +00:00
getMemoryPrompt() {
const session = get().currentSession()
return {
role: 'system',
content: '这是 ai 和用户的历史聊天总结作为前情提要:' + session.memoryPrompt,
date: ''
} as Message
},
2023-03-19 15:13:10 +00:00
getMessagesWithMemory() {
const session = get().currentSession()
const config = get().config
2023-03-19 16:09:30 +00:00
const n = session.messages.length
const recentMessages = session.messages.slice(n - config.historyMessageCount);
2023-03-19 15:13:10 +00:00
2023-03-19 16:29:09 +00:00
const memoryPrompt = get().getMemoryPrompt()
2023-03-19 15:13:10 +00:00
if (session.memoryPrompt) {
recentMessages.unshift(memoryPrompt)
}
return recentMessages
},
2023-03-11 12:54:24 +00:00
updateMessage(
sessionIndex: number,
messageIndex: number,
updater: (message?: Message) => void
) {
const sessions = get().sessions;
const session = sessions.at(sessionIndex);
const messages = session?.messages;
updater(messages?.at(messageIndex));
set(() => ({ sessions }));
},
2023-03-10 18:25:33 +00:00
summarizeSession() {
const session = get().currentSession();
if (session.topic === DEFAULT_TOPIC && session.messages.length >= 3) {
2023-03-13 16:25:07 +00:00
// should summarize topic
2023-03-14 17:56:39 +00:00
requestWithPrompt(
session.messages,
2023-03-19 15:13:10 +00:00
"直接返回这句话的简要主题,不要解释,如果没有主题,请直接返回“闲聊”"
2023-03-14 17:56:39 +00:00
).then((res) => {
get().updateCurrentSession(
(session) => (session.topic = trimTopic(res))
);
});
2023-03-13 16:25:07 +00:00
}
2023-03-19 15:13:10 +00:00
2023-03-19 16:09:30 +00:00
const config = get().config
2023-03-19 16:29:09 +00:00
let toBeSummarizedMsgs = session.messages.slice(session.lastSummarizeIndex)
2023-03-19 16:09:30 +00:00
const historyMsgLength = toBeSummarizedMsgs.reduce((pre, cur) => pre + cur.content.length, 0)
2023-03-19 16:29:09 +00:00
if (historyMsgLength > 4000) {
toBeSummarizedMsgs = toBeSummarizedMsgs.slice(-config.historyMessageCount)
}
// add memory prompt
toBeSummarizedMsgs.unshift(get().getMemoryPrompt())
2023-03-19 16:09:30 +00:00
const lastSummarizeIndex = session.messages.length
2023-03-19 16:29:09 +00:00
console.log('[Chat History] ', toBeSummarizedMsgs, historyMsgLength, config.compressMessageLengthThreshold)
2023-03-19 16:09:30 +00:00
if (historyMsgLength > config.compressMessageLengthThreshold) {
2023-03-19 15:13:10 +00:00
requestChatStream(toBeSummarizedMsgs.concat({
role: 'system',
2023-03-19 16:29:09 +00:00
content: '简要总结一下你和用户的对话,用作后续的上下文提示 prompt控制在 50 字以内',
2023-03-19 15:13:10 +00:00
date: ''
}), {
filterBot: false,
onMessage(message, done) {
session.memoryPrompt = message
if (done) {
console.log('[Memory] ', session.memoryPrompt)
2023-03-19 16:09:30 +00:00
session.lastSummarizeIndex = lastSummarizeIndex
2023-03-19 15:13:10 +00:00
}
},
onError(error) {
console.error('[Summarize] ', error)
},
})
}
2023-03-10 18:25:33 +00:00
},
updateStat(message) {
get().updateCurrentSession((session) => {
session.stat.charCount += message.content.length;
// TODO: should update chat count and word count
});
},
updateCurrentSession(updater) {
const sessions = get().sessions;
const index = get().currentSessionIndex;
updater(sessions[index]);
set(() => ({ sessions }));
},
2023-03-19 15:13:10 +00:00
clearAllData() {
if (confirm('确认清除所有聊天、设置数据?')) {
localStorage.clear()
location.reload()
}
},
2023-03-10 18:25:33 +00:00
}),
2023-03-13 16:25:07 +00:00
{
name: LOCAL_KEY,
}
2023-03-10 18:25:33 +00:00
)
);