store.ts 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274
  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. interface ChatConfig {
  22. maxToken?: number;
  23. historyMessageCount: number; // -1 means all
  24. sendBotMessages: boolean; // send bot's message or not
  25. submitKey: SubmitKey;
  26. avatar: string;
  27. theme: Theme;
  28. }
  29. interface ChatStat {
  30. tokenCount: number;
  31. wordCount: number;
  32. charCount: number;
  33. }
  34. interface ChatSession {
  35. id: number;
  36. topic: string;
  37. memoryPrompt: string;
  38. messages: Message[];
  39. stat: ChatStat;
  40. lastUpdate: string;
  41. deleted?: boolean;
  42. }
  43. const DEFAULT_TOPIC = "新的聊天";
  44. function createEmptySession(): ChatSession {
  45. const createDate = new Date().toLocaleString();
  46. return {
  47. id: Date.now(),
  48. topic: DEFAULT_TOPIC,
  49. memoryPrompt: "",
  50. messages: [
  51. {
  52. role: "assistant",
  53. content: "有什么可以帮你的吗",
  54. date: createDate,
  55. },
  56. ],
  57. stat: {
  58. tokenCount: 0,
  59. wordCount: 0,
  60. charCount: 0,
  61. },
  62. lastUpdate: createDate,
  63. };
  64. }
  65. interface ChatStore {
  66. config: ChatConfig;
  67. sessions: ChatSession[];
  68. currentSessionIndex: number;
  69. removeSession: (index: number) => void;
  70. selectSession: (index: number) => void;
  71. newSession: () => void;
  72. currentSession: () => ChatSession;
  73. onNewMessage: (message: Message) => void;
  74. onUserInput: (content: string) => Promise<void>;
  75. onBotResponse: (message: Message) => void;
  76. summarizeSession: () => void;
  77. updateStat: (message: Message) => void;
  78. updateCurrentSession: (updater: (session: ChatSession) => void) => void;
  79. updateMessage: (
  80. sessionIndex: number,
  81. messageIndex: number,
  82. updater: (message?: Message) => void
  83. ) => void;
  84. getConfig: () => ChatConfig;
  85. updateConfig: (updater: (config: ChatConfig) => void) => void;
  86. }
  87. export const useChatStore = create<ChatStore>()(
  88. persist(
  89. (set, get) => ({
  90. sessions: [createEmptySession()],
  91. currentSessionIndex: 0,
  92. config: {
  93. historyMessageCount: 5,
  94. sendBotMessages: false as boolean,
  95. submitKey: SubmitKey.CtrlEnter as SubmitKey,
  96. avatar: "1fae0",
  97. theme: Theme.Auto as Theme,
  98. },
  99. getConfig() {
  100. return get().config;
  101. },
  102. updateConfig(updater) {
  103. const config = get().config;
  104. updater(config);
  105. set(() => ({ config }));
  106. },
  107. selectSession(index: number) {
  108. set({
  109. currentSessionIndex: index,
  110. });
  111. },
  112. removeSession(index: number) {
  113. set((state) => {
  114. let nextIndex = state.currentSessionIndex;
  115. const sessions = state.sessions;
  116. if (sessions.length === 1) {
  117. return {
  118. currentSessionIndex: 0,
  119. sessions: [createEmptySession()],
  120. };
  121. }
  122. sessions.splice(index, 1);
  123. if (nextIndex === index) {
  124. nextIndex -= 1;
  125. }
  126. return {
  127. currentSessionIndex: nextIndex,
  128. sessions,
  129. };
  130. });
  131. },
  132. newSession() {
  133. set((state) => ({
  134. currentSessionIndex: 0,
  135. sessions: [createEmptySession()].concat(state.sessions),
  136. }));
  137. },
  138. currentSession() {
  139. let index = get().currentSessionIndex;
  140. const sessions = get().sessions;
  141. if (index < 0 || index >= sessions.length) {
  142. index = Math.min(sessions.length - 1, Math.max(0, index));
  143. set(() => ({ currentSessionIndex: index }));
  144. }
  145. const session = sessions[index];
  146. return session;
  147. },
  148. onNewMessage(message) {
  149. get().updateCurrentSession((session) => {
  150. session.messages.push(message);
  151. });
  152. get().updateStat(message);
  153. get().summarizeSession();
  154. },
  155. async onUserInput(content) {
  156. const message: Message = {
  157. role: "user",
  158. content,
  159. date: new Date().toLocaleString(),
  160. };
  161. // get last five messges
  162. const messages = get().currentSession().messages.concat(message);
  163. get().onNewMessage(message);
  164. const botMessage: Message = {
  165. content: "",
  166. role: "assistant",
  167. date: new Date().toLocaleString(),
  168. streaming: true,
  169. };
  170. get().updateCurrentSession((session) => {
  171. session.messages.push(botMessage);
  172. });
  173. const fiveMessages = messages.slice(-5);
  174. requestChatStream(fiveMessages, {
  175. onMessage(content, done) {
  176. if (done) {
  177. botMessage.streaming = false;
  178. get().updateStat(botMessage);
  179. get().summarizeSession();
  180. } else {
  181. botMessage.content = content;
  182. set(() => ({}));
  183. }
  184. },
  185. onError(error) {
  186. botMessage.content = "出错了,稍后重试吧";
  187. botMessage.streaming = false;
  188. set(() => ({}));
  189. },
  190. filterBot: !get().config.sendBotMessages,
  191. });
  192. },
  193. updateMessage(
  194. sessionIndex: number,
  195. messageIndex: number,
  196. updater: (message?: Message) => void
  197. ) {
  198. const sessions = get().sessions;
  199. const session = sessions.at(sessionIndex);
  200. const messages = session?.messages;
  201. updater(messages?.at(messageIndex));
  202. set(() => ({ sessions }));
  203. },
  204. onBotResponse(message) {
  205. get().onNewMessage(message);
  206. },
  207. summarizeSession() {
  208. const session = get().currentSession();
  209. if (session.topic !== DEFAULT_TOPIC) return;
  210. requestWithPrompt(
  211. session.messages,
  212. "简明扼要地 10 字以内总结主题"
  213. ).then((res) => {
  214. get().updateCurrentSession(
  215. (session) => (session.topic = trimTopic(res))
  216. );
  217. });
  218. },
  219. updateStat(message) {
  220. get().updateCurrentSession((session) => {
  221. session.stat.charCount += message.content.length;
  222. // TODO: should update chat count and word count
  223. });
  224. },
  225. updateCurrentSession(updater) {
  226. const sessions = get().sessions;
  227. const index = get().currentSessionIndex;
  228. updater(sessions[index]);
  229. set(() => ({ sessions }));
  230. },
  231. }),
  232. { name: "chat-next-web-store" }
  233. )
  234. );