store.ts 7.0 KB

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