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 {
|
2023-03-20 06:06:29 +00:00
|
|
|
|
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();
|
|
|
|
|
|
2023-03-20 06:06:29 +00:00
|
|
|
|
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
|
|
|
|
)
|
|
|
|
|
);
|