store.ts 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358
  1. import { create } from "zustand";
  2. import { persist } from "zustand/middleware";
  3. import { type ChatCompletionResponseMessage } from "openai";
  4. import { requestChat, requestChatStream, requestWithPrompt } from "./requests";
  5. import { trimTopic } from "./utils";
  6. export type Message = ChatCompletionResponseMessage & {
  7. date: string;
  8. streaming?: boolean;
  9. };
  10. export enum SubmitKey {
  11. Enter = "Enter",
  12. CtrlEnter = "Ctrl + Enter",
  13. ShiftEnter = "Shift + Enter",
  14. AltEnter = "Alt + Enter",
  15. }
  16. export enum Theme {
  17. Auto = "auto",
  18. Dark = "dark",
  19. Light = "light",
  20. }
  21. export interface ChatConfig {
  22. maxToken?: number;
  23. historyMessageCount: number; // -1 means all
  24. compressMessageLengthThreshold: number;
  25. sendBotMessages: boolean; // send bot's message or not
  26. submitKey: SubmitKey;
  27. avatar: string;
  28. theme: Theme;
  29. tightBorder: boolean;
  30. }
  31. const DEFAULT_CONFIG: ChatConfig = {
  32. historyMessageCount: 4,
  33. compressMessageLengthThreshold: 1000,
  34. sendBotMessages: true as boolean,
  35. submitKey: SubmitKey.CtrlEnter as SubmitKey,
  36. avatar: "1f603",
  37. theme: Theme.Auto as Theme,
  38. tightBorder: false,
  39. };
  40. export interface ChatStat {
  41. tokenCount: number;
  42. wordCount: number;
  43. charCount: number;
  44. }
  45. export interface ChatSession {
  46. id: number;
  47. topic: string;
  48. memoryPrompt: string;
  49. messages: Message[];
  50. stat: ChatStat;
  51. lastUpdate: string;
  52. lastSummarizeIndex: number;
  53. }
  54. const DEFAULT_TOPIC = "新的聊天";
  55. function createEmptySession(): ChatSession {
  56. const createDate = new Date().toLocaleString();
  57. return {
  58. id: Date.now(),
  59. topic: DEFAULT_TOPIC,
  60. memoryPrompt: "",
  61. messages: [
  62. {
  63. role: "assistant",
  64. content: "有什么可以帮你的吗",
  65. date: createDate,
  66. },
  67. ],
  68. stat: {
  69. tokenCount: 0,
  70. wordCount: 0,
  71. charCount: 0,
  72. },
  73. lastUpdate: createDate,
  74. lastSummarizeIndex: 0,
  75. };
  76. }
  77. interface ChatStore {
  78. config: ChatConfig;
  79. sessions: ChatSession[];
  80. currentSessionIndex: number;
  81. removeSession: (index: number) => void;
  82. selectSession: (index: number) => void;
  83. newSession: () => void;
  84. currentSession: () => ChatSession;
  85. onNewMessage: (message: Message) => void;
  86. onUserInput: (content: string) => Promise<void>;
  87. summarizeSession: () => void;
  88. updateStat: (message: Message) => void;
  89. updateCurrentSession: (updater: (session: ChatSession) => void) => void;
  90. updateMessage: (
  91. sessionIndex: number,
  92. messageIndex: number,
  93. updater: (message?: Message) => void
  94. ) => void;
  95. getMessagesWithMemory: () => Message[];
  96. getMemoryPrompt: () => Message,
  97. getConfig: () => ChatConfig;
  98. resetConfig: () => void;
  99. updateConfig: (updater: (config: ChatConfig) => void) => void;
  100. clearAllData: () => void;
  101. }
  102. const LOCAL_KEY = "chat-next-web-store";
  103. export const useChatStore = create<ChatStore>()(
  104. persist(
  105. (set, get) => ({
  106. sessions: [createEmptySession()],
  107. currentSessionIndex: 0,
  108. config: {
  109. ...DEFAULT_CONFIG,
  110. },
  111. resetConfig() {
  112. set(() => ({ config: { ...DEFAULT_CONFIG } }));
  113. },
  114. getConfig() {
  115. return get().config;
  116. },
  117. updateConfig(updater) {
  118. const config = get().config;
  119. updater(config);
  120. set(() => ({ config }));
  121. },
  122. selectSession(index: number) {
  123. set({
  124. currentSessionIndex: index,
  125. });
  126. },
  127. removeSession(index: number) {
  128. set((state) => {
  129. let nextIndex = state.currentSessionIndex;
  130. const sessions = state.sessions;
  131. if (sessions.length === 1) {
  132. return {
  133. currentSessionIndex: 0,
  134. sessions: [createEmptySession()],
  135. };
  136. }
  137. sessions.splice(index, 1);
  138. if (nextIndex === index) {
  139. nextIndex -= 1;
  140. }
  141. return {
  142. currentSessionIndex: nextIndex,
  143. sessions,
  144. };
  145. });
  146. },
  147. newSession() {
  148. set((state) => ({
  149. currentSessionIndex: 0,
  150. sessions: [createEmptySession()].concat(state.sessions),
  151. }));
  152. },
  153. currentSession() {
  154. let index = get().currentSessionIndex;
  155. const sessions = get().sessions;
  156. if (index < 0 || index >= sessions.length) {
  157. index = Math.min(sessions.length - 1, Math.max(0, index));
  158. set(() => ({ currentSessionIndex: index }));
  159. }
  160. const session = sessions[index];
  161. return session;
  162. },
  163. onNewMessage(message) {
  164. get().updateCurrentSession(session => {
  165. session.lastUpdate = new Date().toLocaleString()
  166. })
  167. get().updateStat(message);
  168. get().summarizeSession();
  169. },
  170. async onUserInput(content) {
  171. const userMessage: Message = {
  172. role: "user",
  173. content,
  174. date: new Date().toLocaleString(),
  175. };
  176. const botMessage: Message = {
  177. content: "",
  178. role: "assistant",
  179. date: new Date().toLocaleString(),
  180. streaming: true,
  181. };
  182. // get recent messages
  183. const recentMessages = get().getMessagesWithMemory()
  184. const sendMessages = recentMessages.concat(userMessage)
  185. // save user's and bot's message
  186. get().updateCurrentSession((session) => {
  187. session.messages.push(userMessage);
  188. session.messages.push(botMessage);
  189. });
  190. console.log('[User Input] ', sendMessages)
  191. requestChatStream(sendMessages, {
  192. onMessage(content, done) {
  193. if (done) {
  194. botMessage.streaming = false;
  195. get().onNewMessage(botMessage)
  196. } else {
  197. botMessage.content = content;
  198. set(() => ({}));
  199. }
  200. },
  201. onError(error) {
  202. botMessage.content += "\n\n出错了,稍后重试吧";
  203. botMessage.streaming = false;
  204. set(() => ({}));
  205. },
  206. filterBot: !get().config.sendBotMessages,
  207. });
  208. },
  209. getMemoryPrompt() {
  210. const session = get().currentSession()
  211. return {
  212. role: 'system',
  213. content: '这是 ai 和用户的历史聊天总结作为前情提要:' + session.memoryPrompt,
  214. date: ''
  215. } as Message
  216. },
  217. getMessagesWithMemory() {
  218. const session = get().currentSession()
  219. const config = get().config
  220. const n = session.messages.length
  221. const recentMessages = session.messages.slice(n - config.historyMessageCount);
  222. const memoryPrompt = get().getMemoryPrompt()
  223. if (session.memoryPrompt) {
  224. recentMessages.unshift(memoryPrompt)
  225. }
  226. return recentMessages
  227. },
  228. updateMessage(
  229. sessionIndex: number,
  230. messageIndex: number,
  231. updater: (message?: Message) => void
  232. ) {
  233. const sessions = get().sessions;
  234. const session = sessions.at(sessionIndex);
  235. const messages = session?.messages;
  236. updater(messages?.at(messageIndex));
  237. set(() => ({ sessions }));
  238. },
  239. summarizeSession() {
  240. const session = get().currentSession();
  241. if (session.topic === DEFAULT_TOPIC) {
  242. // should summarize topic
  243. requestWithPrompt(
  244. session.messages,
  245. "直接返回这句话的简要主题,不要解释,如果没有主题,请直接返回“闲聊”"
  246. ).then((res) => {
  247. get().updateCurrentSession(
  248. (session) => (session.topic = trimTopic(res))
  249. );
  250. });
  251. }
  252. const config = get().config
  253. let toBeSummarizedMsgs = session.messages.slice(session.lastSummarizeIndex)
  254. const historyMsgLength = toBeSummarizedMsgs.reduce((pre, cur) => pre + cur.content.length, 0)
  255. if (historyMsgLength > 4000) {
  256. toBeSummarizedMsgs = toBeSummarizedMsgs.slice(-config.historyMessageCount)
  257. }
  258. // add memory prompt
  259. toBeSummarizedMsgs.unshift(get().getMemoryPrompt())
  260. const lastSummarizeIndex = session.messages.length
  261. console.log('[Chat History] ', toBeSummarizedMsgs, historyMsgLength, config.compressMessageLengthThreshold)
  262. if (historyMsgLength > config.compressMessageLengthThreshold) {
  263. requestChatStream(toBeSummarizedMsgs.concat({
  264. role: 'system',
  265. content: '简要总结一下你和用户的对话,用作后续的上下文提示 prompt,控制在 50 字以内',
  266. date: ''
  267. }), {
  268. filterBot: false,
  269. onMessage(message, done) {
  270. session.memoryPrompt = message
  271. if (done) {
  272. console.log('[Memory] ', session.memoryPrompt)
  273. session.lastSummarizeIndex = lastSummarizeIndex
  274. }
  275. },
  276. onError(error) {
  277. console.error('[Summarize] ', error)
  278. },
  279. })
  280. }
  281. },
  282. updateStat(message) {
  283. get().updateCurrentSession((session) => {
  284. session.stat.charCount += message.content.length;
  285. // TODO: should update chat count and word count
  286. });
  287. },
  288. updateCurrentSession(updater) {
  289. const sessions = get().sessions;
  290. const index = get().currentSessionIndex;
  291. updater(sessions[index]);
  292. set(() => ({ sessions }));
  293. },
  294. clearAllData() {
  295. if (confirm('确认清除所有聊天、设置数据?')) {
  296. localStorage.clear()
  297. location.reload()
  298. }
  299. },
  300. }),
  301. {
  302. name: LOCAL_KEY,
  303. }
  304. )
  305. );