@lobehub/chat 1.33.5 → 1.34.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (203) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/changelog/v1.json +9 -0
  3. package/locales/ar/chat.json +7 -0
  4. package/locales/ar/common.json +2 -0
  5. package/locales/ar/models.json +24 -0
  6. package/locales/ar/setting.json +5 -0
  7. package/locales/ar/thread.json +5 -0
  8. package/locales/bg-BG/chat.json +7 -0
  9. package/locales/bg-BG/common.json +2 -0
  10. package/locales/bg-BG/models.json +24 -0
  11. package/locales/bg-BG/setting.json +5 -0
  12. package/locales/bg-BG/thread.json +5 -0
  13. package/locales/de-DE/chat.json +7 -0
  14. package/locales/de-DE/common.json +2 -0
  15. package/locales/de-DE/models.json +24 -0
  16. package/locales/de-DE/setting.json +5 -0
  17. package/locales/de-DE/thread.json +5 -0
  18. package/locales/en-US/chat.json +7 -0
  19. package/locales/en-US/common.json +2 -0
  20. package/locales/en-US/models.json +24 -0
  21. package/locales/en-US/setting.json +5 -0
  22. package/locales/en-US/thread.json +5 -0
  23. package/locales/es-ES/chat.json +7 -0
  24. package/locales/es-ES/common.json +2 -0
  25. package/locales/es-ES/models.json +24 -0
  26. package/locales/es-ES/setting.json +5 -0
  27. package/locales/es-ES/thread.json +5 -0
  28. package/locales/fa-IR/chat.json +7 -0
  29. package/locales/fa-IR/common.json +2 -0
  30. package/locales/fa-IR/models.json +24 -0
  31. package/locales/fa-IR/setting.json +5 -0
  32. package/locales/fa-IR/thread.json +5 -0
  33. package/locales/fr-FR/chat.json +7 -0
  34. package/locales/fr-FR/common.json +2 -0
  35. package/locales/fr-FR/models.json +24 -0
  36. package/locales/fr-FR/setting.json +5 -0
  37. package/locales/fr-FR/thread.json +5 -0
  38. package/locales/it-IT/chat.json +7 -0
  39. package/locales/it-IT/common.json +2 -0
  40. package/locales/it-IT/models.json +24 -0
  41. package/locales/it-IT/setting.json +5 -0
  42. package/locales/it-IT/thread.json +5 -0
  43. package/locales/ja-JP/chat.json +7 -0
  44. package/locales/ja-JP/common.json +2 -0
  45. package/locales/ja-JP/models.json +24 -0
  46. package/locales/ja-JP/setting.json +5 -0
  47. package/locales/ja-JP/thread.json +5 -0
  48. package/locales/ko-KR/chat.json +7 -0
  49. package/locales/ko-KR/common.json +2 -0
  50. package/locales/ko-KR/models.json +24 -0
  51. package/locales/ko-KR/setting.json +5 -0
  52. package/locales/ko-KR/thread.json +5 -0
  53. package/locales/nl-NL/chat.json +7 -0
  54. package/locales/nl-NL/common.json +2 -0
  55. package/locales/nl-NL/models.json +24 -0
  56. package/locales/nl-NL/setting.json +5 -0
  57. package/locales/nl-NL/thread.json +5 -0
  58. package/locales/pl-PL/chat.json +7 -0
  59. package/locales/pl-PL/common.json +2 -0
  60. package/locales/pl-PL/models.json +24 -0
  61. package/locales/pl-PL/setting.json +5 -0
  62. package/locales/pl-PL/thread.json +5 -0
  63. package/locales/pt-BR/chat.json +7 -0
  64. package/locales/pt-BR/common.json +2 -0
  65. package/locales/pt-BR/models.json +24 -0
  66. package/locales/pt-BR/setting.json +5 -0
  67. package/locales/pt-BR/thread.json +5 -0
  68. package/locales/ru-RU/chat.json +7 -0
  69. package/locales/ru-RU/common.json +2 -0
  70. package/locales/ru-RU/models.json +24 -0
  71. package/locales/ru-RU/setting.json +5 -0
  72. package/locales/ru-RU/thread.json +5 -0
  73. package/locales/tr-TR/chat.json +7 -0
  74. package/locales/tr-TR/common.json +2 -0
  75. package/locales/tr-TR/models.json +24 -0
  76. package/locales/tr-TR/setting.json +5 -0
  77. package/locales/tr-TR/thread.json +5 -0
  78. package/locales/vi-VN/chat.json +7 -0
  79. package/locales/vi-VN/common.json +2 -0
  80. package/locales/vi-VN/models.json +24 -0
  81. package/locales/vi-VN/setting.json +5 -0
  82. package/locales/vi-VN/thread.json +5 -0
  83. package/locales/zh-CN/chat.json +7 -0
  84. package/locales/zh-CN/common.json +2 -0
  85. package/locales/zh-CN/models.json +24 -0
  86. package/locales/zh-CN/setting.json +5 -0
  87. package/locales/zh-CN/thread.json +5 -0
  88. package/locales/zh-TW/chat.json +7 -0
  89. package/locales/zh-TW/common.json +2 -0
  90. package/locales/zh-TW/models.json +24 -0
  91. package/locales/zh-TW/setting.json +5 -0
  92. package/locales/zh-TW/thread.json +5 -0
  93. package/package.json +1 -1
  94. package/src/app/(main)/chat/(workspace)/@conversation/default.tsx +2 -0
  95. package/src/app/(main)/chat/(workspace)/@conversation/features/ChatHydration/index.tsx +11 -2
  96. package/src/{features → app/(main)/chat/(workspace)/@conversation/features}/ChatInput/Desktop/Footer/index.tsx +7 -9
  97. package/src/app/(main)/chat/(workspace)/@conversation/features/ChatInput/Desktop/index.tsx +7 -2
  98. package/src/app/(main)/chat/(workspace)/@conversation/features/ChatList/ChatItem/Thread.tsx +62 -0
  99. package/src/app/(main)/chat/(workspace)/@conversation/features/ChatList/ChatItem/ThreadItem.tsx +68 -0
  100. package/src/app/(main)/chat/(workspace)/@conversation/features/ChatList/ChatItem/index.tsx +62 -2
  101. package/src/app/(main)/chat/(workspace)/@conversation/features/ThreadHydration.tsx +47 -0
  102. package/src/app/(main)/chat/(workspace)/@portal/_layout/Desktop.tsx +3 -2
  103. package/src/app/(main)/chat/(workspace)/@portal/_layout/Mobile.tsx +47 -6
  104. package/src/app/(main)/chat/(workspace)/@topic/features/SkeletonList.tsx +3 -2
  105. package/src/app/(main)/chat/(workspace)/@topic/features/TopicListContent/ByTimeMode/index.tsx +10 -3
  106. package/src/app/(main)/chat/(workspace)/@topic/features/TopicListContent/FlatMode/index.tsx +1 -1
  107. package/src/app/(main)/chat/(workspace)/@topic/features/TopicListContent/ThreadItem/Content.tsx +164 -0
  108. package/src/app/(main)/chat/(workspace)/@topic/features/TopicListContent/ThreadItem/index.tsx +98 -0
  109. package/src/app/(main)/chat/(workspace)/@topic/features/TopicListContent/{TopicItem.tsx → TopicItem/index.tsx} +33 -22
  110. package/src/app/(main)/chat/(workspace)/_layout/Desktop/Portal.tsx +12 -5
  111. package/src/app/(main)/chat/(workspace)/_layout/Mobile/index.tsx +1 -2
  112. package/src/const/message.ts +2 -0
  113. package/src/const/settings/systemAgent.ts +1 -0
  114. package/src/database/server/migrations/0012_add_thread.sql +39 -0
  115. package/src/database/server/migrations/meta/0012_snapshot.json +3671 -0
  116. package/src/database/server/migrations/meta/_journal.json +7 -0
  117. package/src/database/server/models/_template.ts +2 -2
  118. package/src/database/server/models/message.ts +1 -0
  119. package/src/database/server/models/thread.ts +79 -0
  120. package/src/database/server/schemas/lobechat/message.ts +2 -1
  121. package/src/database/server/schemas/lobechat/relations.ts +13 -1
  122. package/src/database/server/schemas/lobechat/topic.ts +30 -1
  123. package/src/database/server/utils/idGenerator.ts +1 -0
  124. package/src/features/ChatInput/ActionBar/Token/TokenTag.tsx +6 -4
  125. package/src/features/ChatInput/ActionBar/Token/index.tsx +24 -5
  126. package/src/features/ChatInput/ActionBar/config.ts +3 -2
  127. package/src/features/ChatInput/Desktop/index.tsx +15 -7
  128. package/src/features/ChatInput/Mobile/index.tsx +4 -4
  129. package/src/features/Conversation/Actions/Assistant.tsx +24 -5
  130. package/src/features/Conversation/Actions/User.tsx +21 -4
  131. package/src/features/Conversation/Actions/index.ts +1 -66
  132. package/src/features/Conversation/Messages/{Tool → Assistant/ToolCallItem}/Inspector/index.tsx +3 -1
  133. package/src/features/Conversation/Messages/{Tool/index.tsx → Assistant/ToolCallItem/Tool.tsx} +10 -11
  134. package/src/features/Conversation/Messages/Assistant/ToolCallItem/index.tsx +5 -3
  135. package/src/features/Conversation/Messages/Assistant/index.tsx +22 -14
  136. package/src/features/Conversation/Messages/index.ts +0 -2
  137. package/src/features/Conversation/components/AutoScroll.tsx +1 -1
  138. package/src/features/Conversation/components/ChatItem/ActionsBar.tsx +79 -5
  139. package/src/features/Conversation/components/ChatItem/InPortalThreadContext.ts +3 -0
  140. package/src/features/Conversation/components/ChatItem/index.tsx +16 -5
  141. package/src/features/Conversation/components/MarkdownElements/LobeArtifact/Render/index.tsx +9 -1
  142. package/src/features/Conversation/components/ThreadDivider/index.tsx +19 -0
  143. package/src/features/Conversation/hooks/useChatListActionsBar.tsx +19 -4
  144. package/src/features/Portal/Thread/Chat/ChatInput/Footer.tsx +90 -0
  145. package/src/features/Portal/Thread/Chat/ChatInput/TextArea.tsx +30 -0
  146. package/src/features/Portal/Thread/Chat/ChatInput/index.tsx +66 -0
  147. package/src/features/Portal/Thread/Chat/ChatInput/useSend.ts +50 -0
  148. package/src/features/Portal/Thread/Chat/ChatItem.tsx +62 -0
  149. package/src/features/Portal/Thread/Chat/ChatList.tsx +49 -0
  150. package/src/features/Portal/Thread/Chat/ThreadDivider/index.tsx +19 -0
  151. package/src/features/Portal/Thread/Chat/index.tsx +28 -0
  152. package/src/features/Portal/Thread/Header/Active.tsx +35 -0
  153. package/src/features/Portal/Thread/Header/New.tsx +37 -0
  154. package/src/features/Portal/Thread/Header/Title.tsx +18 -0
  155. package/src/features/Portal/Thread/Header/index.tsx +20 -0
  156. package/src/features/Portal/Thread/hook.ts +8 -0
  157. package/src/features/Portal/Thread/index.ts +12 -0
  158. package/src/features/Portal/router.tsx +2 -1
  159. package/src/hooks/useFetchTopics.ts +7 -1
  160. package/src/locales/default/chat.ts +8 -1
  161. package/src/locales/default/common.ts +3 -0
  162. package/src/locales/default/index.ts +2 -0
  163. package/src/locales/default/setting.ts +5 -0
  164. package/src/locales/default/thread.ts +5 -0
  165. package/src/server/routers/lambda/index.ts +2 -0
  166. package/src/server/routers/lambda/thread.ts +83 -0
  167. package/src/services/thread.ts +54 -0
  168. package/src/store/chat/initialState.ts +3 -0
  169. package/src/store/chat/selectors.ts +2 -1
  170. package/src/store/chat/slices/aiChat/actions/__tests__/generateAIChat.test.ts +1 -1
  171. package/src/store/chat/slices/aiChat/actions/__tests__/rag.test.ts +1 -1
  172. package/src/store/chat/slices/aiChat/actions/generateAIChat.ts +31 -8
  173. package/src/store/chat/slices/aiChat/actions/rag.ts +1 -1
  174. package/src/store/chat/slices/message/selectors.test.ts +3 -3
  175. package/src/store/chat/slices/message/selectors.ts +50 -29
  176. package/src/store/chat/slices/plugin/action.ts +26 -8
  177. package/src/store/chat/slices/portal/action.ts +1 -0
  178. package/src/store/chat/slices/portal/initialState.ts +1 -0
  179. package/src/store/chat/slices/portal/selectors/thread.ts +17 -0
  180. package/src/store/chat/slices/portal/selectors.ts +2 -0
  181. package/src/store/chat/slices/thread/action.ts +326 -0
  182. package/src/store/chat/slices/thread/initialState.ts +34 -0
  183. package/src/store/chat/slices/thread/reducer.ts +48 -0
  184. package/src/store/chat/slices/thread/selectors/index.ts +202 -0
  185. package/src/store/chat/slices/thread/selectors/util.ts +22 -0
  186. package/src/store/chat/slices/topic/action.ts +5 -1
  187. package/src/store/chat/store.ts +5 -2
  188. package/src/store/global/initialState.ts +4 -0
  189. package/src/store/global/selectors.ts +4 -0
  190. package/src/store/user/slices/settings/selectors/systemAgent.ts +2 -0
  191. package/src/types/message/index.ts +17 -1
  192. package/src/types/topic/index.ts +1 -0
  193. package/src/types/topic/thread.ts +42 -0
  194. package/src/types/user/settings/systemAgent.ts +1 -0
  195. package/src/app/(main)/chat/(workspace)/@portal/features/Header.tsx +0 -11
  196. package/src/app/(main)/chat/(workspace)/_layout/Mobile/PortalModal.tsx +0 -35
  197. /package/src/{features → app/(main)/chat/(workspace)/@conversation/features}/ChatInput/Desktop/Footer/SendMore.tsx +0 -0
  198. /package/src/{features → app/(main)/chat/(workspace)/@conversation/features}/ChatInput/Desktop/Footer/ShortcutHint.tsx +0 -0
  199. /package/src/app/(main)/chat/(workspace)/@topic/features/TopicListContent/{DefaultContent.tsx → TopicItem/DefaultContent.tsx} +0 -0
  200. /package/src/app/(main)/chat/(workspace)/@topic/features/TopicListContent/{TopicContent.tsx → TopicItem/TopicContent.tsx} +0 -0
  201. /package/src/features/Conversation/Messages/{Tool → Assistant/ToolCallItem}/Inspector/PluginResultJSON.tsx +0 -0
  202. /package/src/features/Conversation/Messages/{Tool → Assistant/ToolCallItem}/Inspector/Settings.tsx +0 -0
  203. /package/src/features/Conversation/Messages/{Tool → Assistant/ToolCallItem}/Inspector/style.ts +0 -0
@@ -0,0 +1,326 @@
1
+ /* eslint-disable sort-keys-fix/sort-keys-fix, typescript-sort-keys/interface */
2
+ // Disable the auto sort key eslint rule to make the code more logic and readable
3
+ import isEqual from 'fast-deep-equal';
4
+ import { SWRResponse, mutate } from 'swr';
5
+ import { StateCreator } from 'zustand/vanilla';
6
+
7
+ import { chainSummaryTitle } from '@/chains/summaryTitle';
8
+ import { LOADING_FLAT, THREAD_DRAFT_ID } from '@/const/message';
9
+ import { isServerMode } from '@/const/version';
10
+ import { useClientDataSWR } from '@/libs/swr';
11
+ import { chatService } from '@/services/chat';
12
+ import { threadService } from '@/services/thread';
13
+ import { threadSelectors } from '@/store/chat/selectors';
14
+ import { ChatStore } from '@/store/chat/store';
15
+ import { useSessionStore } from '@/store/session';
16
+ import { useUserStore } from '@/store/user';
17
+ import { systemAgentSelectors } from '@/store/user/selectors';
18
+ import { ChatMessage, CreateMessageParams, SendThreadMessageParams } from '@/types/message';
19
+ import { ThreadItem, ThreadType } from '@/types/topic';
20
+ import { merge } from '@/utils/merge';
21
+ import { setNamespace } from '@/utils/storeDebug';
22
+
23
+ import { ThreadDispatch, threadReducer } from './reducer';
24
+
25
+ const n = setNamespace('thd');
26
+ const SWR_USE_FETCH_THREADS = 'SWR_USE_FETCH_THREADS';
27
+
28
+ export interface ChatThreadAction {
29
+ // update
30
+ updateThreadInputMessage: (message: string) => void;
31
+ refreshThreads: () => Promise<void>;
32
+ /**
33
+ * Sends a new thread message to the AI chat system
34
+ */
35
+ sendThreadMessage: (params: SendThreadMessageParams) => Promise<void>;
36
+ resendThreadMessage: (messageId: string) => Promise<void>;
37
+ delAndResendThreadMessage: (messageId: string) => Promise<void>;
38
+ createThread: (params: {
39
+ message: CreateMessageParams;
40
+ sourceMessageId: string;
41
+ topicId: string;
42
+ type: ThreadType;
43
+ }) => Promise<{ threadId: string; messageId: string }>;
44
+ openThreadCreator: (messageId: string) => void;
45
+ openThreadInPortal: (threadId: string, sourceMessageId: string) => void;
46
+ closeThreadPortal: () => void;
47
+ useFetchThreads: (topicId?: string) => SWRResponse<ThreadItem[]>;
48
+ summaryThreadTitle: (threadId: string, messages: ChatMessage[]) => Promise<void>;
49
+ updateThreadTitle: (id: string, title: string) => Promise<void>;
50
+ removeThread: (id: string) => Promise<void>;
51
+ switchThread: (id: string) => void;
52
+
53
+ internal_updateThreadTitleInSummary: (id: string, title: string) => void;
54
+ internal_updateThreadLoading: (id: string, loading: boolean) => void;
55
+ internal_updateThread: (id: string, data: Partial<ThreadItem>) => Promise<void>;
56
+ internal_dispatchThread: (payload: ThreadDispatch, action?: any) => void;
57
+ }
58
+
59
+ export const chatThreadMessage: StateCreator<
60
+ ChatStore,
61
+ [['zustand/devtools', never]],
62
+ [],
63
+ ChatThreadAction
64
+ > = (set, get) => ({
65
+ updateThreadInputMessage: (message) => {
66
+ if (isEqual(message, get().threadInputMessage)) return;
67
+
68
+ set({ threadInputMessage: message }, false, n(`updateThreadInputMessage`, message));
69
+ },
70
+
71
+ openThreadCreator: (messageId) => {
72
+ set(
73
+ { threadStartMessageId: messageId, portalThreadId: undefined, startToForkThread: true },
74
+ false,
75
+ 'openThreadCreator',
76
+ );
77
+ get().togglePortal(true);
78
+ },
79
+ openThreadInPortal: (threadId, sourceMessageId) => {
80
+ set(
81
+ { portalThreadId: threadId, threadStartMessageId: sourceMessageId, startToForkThread: false },
82
+ false,
83
+ 'openThreadInPortal',
84
+ );
85
+ get().togglePortal(true);
86
+ },
87
+
88
+ closeThreadPortal: () => {
89
+ set(
90
+ { threadStartMessageId: undefined, portalThreadId: undefined, startToForkThread: undefined },
91
+ false,
92
+ 'closeThreadPortal',
93
+ );
94
+ get().togglePortal(false);
95
+ },
96
+ sendThreadMessage: async ({ message }) => {
97
+ const {
98
+ internal_coreProcessMessage,
99
+ activeTopicId,
100
+ activeId,
101
+ threadStartMessageId,
102
+ newThreadMode,
103
+ portalThreadId,
104
+ } = get();
105
+ if (!activeId || !activeTopicId) return;
106
+
107
+ // if message is empty or no files, then stop
108
+ if (!message) return;
109
+
110
+ set({ isCreatingThreadMessage: true }, false, n('creatingThreadMessage/start'));
111
+
112
+ const newMessage: CreateMessageParams = {
113
+ content: message,
114
+ // if message has attached with files, then add files to message and the agent
115
+ // files: fileIdList,
116
+ role: 'user',
117
+ sessionId: activeId,
118
+ // if there is activeTopicId,then add topicId to message
119
+ topicId: activeTopicId,
120
+ threadId: portalThreadId,
121
+ };
122
+
123
+ let parentMessageId: string | undefined = undefined;
124
+ let tempMessageId: string | undefined = undefined;
125
+
126
+ // if there is no portalThreadId, then create a thread and then append message
127
+ if (!portalThreadId) {
128
+ if (!threadStartMessageId) return;
129
+ // we need to create a temp message for optimistic update
130
+ tempMessageId = get().internal_createTmpMessage({
131
+ ...newMessage,
132
+ threadId: THREAD_DRAFT_ID,
133
+ });
134
+ get().internal_toggleMessageLoading(true, tempMessageId);
135
+
136
+ const { threadId, messageId } = await get().createThread({
137
+ message: newMessage,
138
+ sourceMessageId: threadStartMessageId,
139
+ topicId: activeTopicId,
140
+ type: newThreadMode,
141
+ });
142
+
143
+ parentMessageId = messageId;
144
+
145
+ // mark the portal in thread mode
146
+ await get().refreshThreads();
147
+ await get().refreshMessages();
148
+
149
+ get().openThreadInPortal(threadId, threadStartMessageId);
150
+ } else {
151
+ // if there is a thread, just append message
152
+ // we need to create a temp message for optimistic update
153
+ tempMessageId = get().internal_createTmpMessage(newMessage);
154
+ get().internal_toggleMessageLoading(true, tempMessageId);
155
+
156
+ parentMessageId = await get().internal_createMessage(newMessage, { tempMessageId });
157
+ }
158
+
159
+ get().internal_toggleMessageLoading(false, tempMessageId);
160
+
161
+ // update assistant update to make it rerank
162
+ useSessionStore.getState().triggerSessionUpdate(get().activeId);
163
+
164
+ // Get the current messages to generate AI response
165
+ const messages = threadSelectors.portalAIChats(get());
166
+
167
+ await internal_coreProcessMessage(messages, parentMessageId, {
168
+ ragQuery: get().internal_shouldUseRAG() ? message : undefined,
169
+ threadId: get().portalThreadId,
170
+ inPortalThread: true,
171
+ });
172
+
173
+ set({ isCreatingThreadMessage: false }, false, n('creatingThreadMessage/stop'));
174
+
175
+ // 说明是在新建 thread,需要自动总结标题
176
+ if (!portalThreadId) {
177
+ const portalThread = threadSelectors.currentPortalThread(get());
178
+
179
+ if (!portalThread) return;
180
+
181
+ const chats = threadSelectors.portalAIChats(get());
182
+ await get().summaryThreadTitle(portalThread.id, chats);
183
+ }
184
+ },
185
+ resendThreadMessage: async (messageId) => {
186
+ const chats = threadSelectors.portalAIChats(get());
187
+
188
+ await get().internal_resendMessage(messageId, {
189
+ messages: chats,
190
+ threadId: get().portalThreadId,
191
+ inPortalThread: true,
192
+ });
193
+ },
194
+ delAndResendThreadMessage: async (id) => {
195
+ get().resendThreadMessage(id);
196
+ get().deleteMessage(id);
197
+ },
198
+ createThread: async ({ message, sourceMessageId, topicId, type }) => {
199
+ set({ isCreatingThread: true }, false, n('creatingThread/start'));
200
+
201
+ const data = await threadService.createThreadWithMessage({
202
+ topicId,
203
+ sourceMessageId,
204
+ type,
205
+ message,
206
+ });
207
+ set({ isCreatingThread: false }, false, n('creatingThread/end'));
208
+
209
+ return data;
210
+ },
211
+
212
+ useFetchThreads: (topicId) =>
213
+ useClientDataSWR<ThreadItem[]>(
214
+ !!topicId && isServerMode ? [SWR_USE_FETCH_THREADS, topicId] : null,
215
+ async ([, topicId]: [string, string]) => threadService.getThreads(topicId),
216
+ {
217
+ suspense: true,
218
+ fallbackData: [],
219
+ onSuccess: (threads) => {
220
+ const nextMap = { ...get().threadMaps, [topicId!]: threads };
221
+
222
+ // no need to update map if the topics have been init and the map is the same
223
+ if (get().topicsInit && isEqual(nextMap, get().topicMaps)) return;
224
+
225
+ set(
226
+ { threadMaps: nextMap, threadsInit: true },
227
+ false,
228
+ n('useFetchThreads(success)', { topicId }),
229
+ );
230
+ },
231
+ },
232
+ ),
233
+
234
+ refreshThreads: async () => {
235
+ const topicId = get().activeTopicId;
236
+ if (!topicId) return;
237
+
238
+ return mutate([SWR_USE_FETCH_THREADS, topicId]);
239
+ },
240
+ removeThread: async (id) => {
241
+ await threadService.removeThread(id);
242
+ await get().refreshThreads();
243
+
244
+ if (get().activeThreadId === id) {
245
+ set({ activeThreadId: undefined });
246
+ }
247
+ },
248
+ switchThread: async (id) => {
249
+ set({ activeThreadId: id }, false, n('toggleTopic'));
250
+ },
251
+ updateThreadTitle: async (id, title) => {
252
+ await get().internal_updateThread(id, { title });
253
+ },
254
+
255
+ summaryThreadTitle: async (threadId, messages) => {
256
+ const { internal_updateThreadTitleInSummary, internal_updateThreadLoading } = get();
257
+ const portalThread = threadSelectors.currentPortalThread(get());
258
+ if (!portalThread) return;
259
+
260
+ internal_updateThreadTitleInSummary(threadId, LOADING_FLAT);
261
+
262
+ let output = '';
263
+ const threadConfig = systemAgentSelectors.thread(useUserStore.getState());
264
+
265
+ await chatService.fetchPresetTaskResult({
266
+ onError: () => {
267
+ internal_updateThreadTitleInSummary(threadId, portalThread.title);
268
+ },
269
+ onFinish: async (text) => {
270
+ await get().internal_updateThread(threadId, { title: text });
271
+ },
272
+ onLoadingChange: (loading) => {
273
+ internal_updateThreadLoading(threadId, loading);
274
+ },
275
+ onMessageHandle: (chunk) => {
276
+ switch (chunk.type) {
277
+ case 'text': {
278
+ output += chunk.text;
279
+ }
280
+ }
281
+
282
+ internal_updateThreadTitleInSummary(threadId, output);
283
+ },
284
+ params: merge(threadConfig, chainSummaryTitle(messages)),
285
+ });
286
+ },
287
+
288
+ // Internal process method of the topics
289
+ internal_updateThreadTitleInSummary: (id, title) => {
290
+ get().internal_dispatchThread(
291
+ { type: 'updateThread', id, value: { title } },
292
+ 'updateThreadTitleInSummary',
293
+ );
294
+ },
295
+
296
+ internal_updateThreadLoading: (id, loading) => {
297
+ set(
298
+ (state) => {
299
+ if (loading) return { threadLoadingIds: [...state.threadLoadingIds, id] };
300
+
301
+ return { threadLoadingIds: state.threadLoadingIds.filter((i) => i !== id) };
302
+ },
303
+ false,
304
+ n('updateThreadLoading'),
305
+ );
306
+ },
307
+
308
+ internal_updateThread: async (id, data) => {
309
+ get().internal_dispatchThread({ type: 'updateThread', id, value: data });
310
+
311
+ get().internal_updateThreadLoading(id, true);
312
+ await threadService.updateThread(id, data);
313
+ await get().refreshThreads();
314
+ get().internal_updateThreadLoading(id, false);
315
+ },
316
+
317
+ internal_dispatchThread: (payload, action) => {
318
+ const nextThreads = threadReducer(threadSelectors.currentTopicThreads(get()), payload);
319
+ const nextMap = { ...get().threadMaps, [get().activeTopicId!]: nextThreads };
320
+
321
+ // no need to update map if is the same
322
+ if (isEqual(nextMap, get().threadMaps)) return;
323
+
324
+ set({ threadMaps: nextMap }, false, action ?? n(`dispatchThread/${payload.type}`));
325
+ },
326
+ });
@@ -0,0 +1,34 @@
1
+ import { ThreadItem, ThreadType } from '@/types/topic';
2
+
3
+ export interface ChatThreadState {
4
+ activeThreadId?: string;
5
+ /**
6
+ * is creating thread with service call
7
+ */
8
+ isCreatingThread?: boolean;
9
+ isCreatingThreadMessage?: boolean;
10
+ newThreadMode: ThreadType;
11
+ /**
12
+ * if true it mean to start to fork a new thread
13
+ */
14
+ startToForkThread?: boolean;
15
+
16
+ threadInputMessage: string;
17
+ threadLoadingIds: string[];
18
+ threadMaps: Record<string, ThreadItem[]>;
19
+ threadRenamingId?: string;
20
+ /**
21
+ * when open thread creator, set the message id to it
22
+ */
23
+ threadStartMessageId?: string;
24
+ threadsInit?: boolean;
25
+ }
26
+
27
+ export const initialThreadState: ChatThreadState = {
28
+ isCreatingThread: false,
29
+ newThreadMode: ThreadType.Continuation,
30
+ threadInputMessage: '',
31
+ threadLoadingIds: [],
32
+ threadMaps: {},
33
+ threadsInit: false,
34
+ };
@@ -0,0 +1,48 @@
1
+ import { produce } from 'immer';
2
+
3
+ import { ThreadItem } from '@/types/topic';
4
+
5
+ interface UpdateThreadAction {
6
+ id: string;
7
+ type: 'updateThread';
8
+ value: Partial<ThreadItem>;
9
+ }
10
+
11
+ interface DeleteThreadAction {
12
+ id: string;
13
+ type: 'deleteThread';
14
+ }
15
+
16
+ export type ThreadDispatch = UpdateThreadAction | DeleteThreadAction;
17
+
18
+ export const threadReducer = (state: ThreadItem[] = [], payload: ThreadDispatch): ThreadItem[] => {
19
+ switch (payload.type) {
20
+ case 'updateThread': {
21
+ return produce(state, (draftState) => {
22
+ const { value, id } = payload;
23
+ const threadIndex = draftState.findIndex((thread) => thread.id === id);
24
+
25
+ if (threadIndex !== -1) {
26
+ draftState[threadIndex] = {
27
+ ...draftState[threadIndex],
28
+ ...value,
29
+ updatedAt: new Date(),
30
+ };
31
+ }
32
+ });
33
+ }
34
+
35
+ case 'deleteThread': {
36
+ return produce(state, (draftState) => {
37
+ const threadIndex = draftState.findIndex((thread) => thread.id === payload.id);
38
+ if (threadIndex !== -1) {
39
+ draftState.splice(threadIndex, 1);
40
+ }
41
+ });
42
+ }
43
+
44
+ default: {
45
+ return state;
46
+ }
47
+ }
48
+ };
@@ -0,0 +1,202 @@
1
+ import { THREAD_DRAFT_ID } from '@/const/message';
2
+ import { useAgentStore } from '@/store/agent';
3
+ import { agentSelectors } from '@/store/agent/selectors';
4
+ import type { ChatStoreState } from '@/store/chat';
5
+ import { chatHelpers } from '@/store/chat/helpers';
6
+ import { ChatMessage } from '@/types/message';
7
+ import { ThreadItem } from '@/types/topic';
8
+
9
+ import { chatSelectors } from '../../message/selectors';
10
+ import { genMessage } from './util';
11
+
12
+ const currentTopicThreads = (s: ChatStoreState) => {
13
+ if (!s.activeTopicId) return [];
14
+
15
+ return s.threadMaps[s.activeTopicId] || [];
16
+ };
17
+
18
+ const currentPortalThread = (s: ChatStoreState): ThreadItem | undefined => {
19
+ if (!s.portalThreadId) return undefined;
20
+
21
+ const threads = currentTopicThreads(s);
22
+
23
+ return threads.find((t) => t.id === s.portalThreadId);
24
+ };
25
+
26
+ const threadStartMessageId = (s: ChatStoreState) => s.threadStartMessageId;
27
+
28
+ const threadSourceMessageId = (s: ChatStoreState) => {
29
+ if (s.startToForkThread) return threadStartMessageId(s);
30
+
31
+ const portalThread = currentPortalThread(s);
32
+ return portalThread?.sourceMessageId;
33
+ };
34
+
35
+ const getTheadParentMessages = (s: ChatStoreState, data: ChatMessage[]) => {
36
+ if (s.startToForkThread) {
37
+ const startMessageId = threadStartMessageId(s)!;
38
+
39
+ // 存在 threadId 的消息是子消息,在创建付消息时需要忽略
40
+ const messages = data.filter((m) => !m.threadId);
41
+ return genMessage(messages, startMessageId, s.newThreadMode);
42
+ }
43
+
44
+ const portalThread = currentPortalThread(s);
45
+ return genMessage(data, portalThread?.sourceMessageId, portalThread?.type);
46
+ };
47
+
48
+ // ======= Portal Thread Display Chats ======= //
49
+ // =========================================== //
50
+
51
+ /**
52
+ * 获取当前 thread 的父级消息
53
+ */
54
+ const portalDisplayParentMessages = (s: ChatStoreState): ChatMessage[] => {
55
+ const data = chatSelectors.activeBaseChatsWithoutTool(s);
56
+
57
+ return getTheadParentMessages(s, data);
58
+ };
59
+
60
+ /**
61
+ * these messages are the messages that are in the thread
62
+ *
63
+ */
64
+ const portalDisplayChildChatsByThreadId =
65
+ (id?: string) =>
66
+ (s: ChatStoreState): ChatMessage[] => {
67
+ // skip tool message
68
+ const data = chatSelectors.activeBaseChatsWithoutTool(s);
69
+
70
+ return data.filter((m) => !!id && m.threadId === id);
71
+ };
72
+
73
+ const portalDisplayChats = (s: ChatStoreState) => {
74
+ const parentMessages = portalDisplayParentMessages(s);
75
+ const afterMessages = portalDisplayChildChatsByThreadId(s.portalThreadId)(s);
76
+ // use for optimistic update
77
+ const draftMessage = chatSelectors.activeBaseChats(s).find((m) => m.threadId === THREAD_DRAFT_ID);
78
+
79
+ return [...parentMessages, draftMessage, ...afterMessages].filter(Boolean) as ChatMessage[];
80
+ };
81
+
82
+ const portalDisplayChatsLength = (s: ChatStoreState) => {
83
+ // history length include a thread divider
84
+ return portalDisplayChats(s).length;
85
+ };
86
+ const portalDisplayChatsString = (s: ChatStoreState) => {
87
+ const messages = portalDisplayChats(s);
88
+
89
+ return messages.map((m) => m.content).join('');
90
+ };
91
+
92
+ const portalDisplayChatIDs = (s: ChatStoreState): string[] =>
93
+ portalDisplayChats(s).map((i) => i.id);
94
+
95
+ // ========= Portal Thread AI Chats ========= //
96
+ // ========================================== //
97
+
98
+ const portalAIParentMessages = (s: ChatStoreState): ChatMessage[] => {
99
+ const data = chatSelectors.activeBaseChats(s);
100
+
101
+ return getTheadParentMessages(s, data);
102
+ };
103
+
104
+ const portalAIChildChatsByThreadId =
105
+ (id?: string) =>
106
+ (s: ChatStoreState): ChatMessage[] => {
107
+ // skip tool message
108
+ const data = chatSelectors.activeBaseChats(s);
109
+
110
+ return data.filter((m) => !!id && m.threadId === id);
111
+ };
112
+
113
+ const portalAIChats = (s: ChatStoreState) => {
114
+ const parentMessages = portalAIParentMessages(s);
115
+ const afterMessages = portalAIChildChatsByThreadId(s.portalThreadId)(s);
116
+
117
+ return [...parentMessages, ...afterMessages].filter(Boolean) as ChatMessage[];
118
+ };
119
+
120
+ const portalAIChatsWithHistoryConfig = (s: ChatStoreState) => {
121
+ const parentMessages = portalAIParentMessages(s);
122
+ const afterMessages = portalAIChildChatsByThreadId(s.portalThreadId)(s);
123
+
124
+ const messages = [...parentMessages, ...afterMessages].filter(Boolean) as ChatMessage[];
125
+
126
+ const config = agentSelectors.currentAgentChatConfig(useAgentStore.getState());
127
+
128
+ return chatHelpers.getSlicedMessagesWithConfig(messages, config);
129
+ };
130
+
131
+ const threadSourceMessageIndex = (s: ChatStoreState) => {
132
+ const theadMessageId = threadSourceMessageId(s);
133
+ const data = portalDisplayChats(s);
134
+
135
+ return !theadMessageId ? -1 : data.findIndex((d) => d.id === theadMessageId);
136
+ };
137
+ const getThreadsByTopic = (topicId?: string) => (s: ChatStoreState) => {
138
+ if (!topicId) return;
139
+
140
+ return s.threadMaps[topicId];
141
+ };
142
+
143
+ const getFirstThreadBySourceMsgId = (id: string) => (s: ChatStoreState) => {
144
+ const threads = currentTopicThreads(s);
145
+
146
+ return threads.find((t) => t.sourceMessageId === id);
147
+ };
148
+
149
+ const getThreadsBySourceMsgId = (id: string) => (s: ChatStoreState) => {
150
+ const threads = currentTopicThreads(s);
151
+
152
+ return threads.filter((t) => t.sourceMessageId === id);
153
+ };
154
+
155
+ const hasThreadBySourceMsgId = (id: string) => (s: ChatStoreState) => {
156
+ const threads = currentTopicThreads(s);
157
+
158
+ return threads.some((t) => t.sourceMessageId === id);
159
+ };
160
+
161
+ const isThreadAIGenerating = (s: ChatStoreState) =>
162
+ s.chatLoadingIds.some((id) => portalDisplayChatIDs(s).includes(id));
163
+
164
+ const isInRAGFlow = (s: ChatStoreState) =>
165
+ s.messageRAGLoadingIds.some((id) => portalDisplayChatIDs(s).includes(id));
166
+ const isCreatingMessage = (s: ChatStoreState) => s.isCreatingThreadMessage;
167
+ const isHasMessageLoading = (s: ChatStoreState) =>
168
+ s.messageLoadingIds.some((id) => portalDisplayChatIDs(s).includes(id));
169
+
170
+ /**
171
+ * this function is used to determine whether the send button should be disabled
172
+ */
173
+ const isSendButtonDisabledByMessage = (s: ChatStoreState) =>
174
+ // 1. when there is message loading
175
+ isHasMessageLoading(s) ||
176
+ // 2. when is creating the topic
177
+ s.isCreatingThread ||
178
+ // 3. when is creating the message
179
+ isCreatingMessage(s) ||
180
+ // 4. when the message is in RAG flow
181
+ isInRAGFlow(s);
182
+
183
+ export const threadSelectors = {
184
+ currentPortalThread,
185
+ currentTopicThreads,
186
+ getFirstThreadBySourceMsgId,
187
+ getThreadsBySourceMsgId,
188
+ getThreadsByTopic,
189
+ hasThreadBySourceMsgId,
190
+ isSendButtonDisabledByMessage,
191
+ isThreadAIGenerating,
192
+ portalAIChats,
193
+ portalAIChatsWithHistoryConfig,
194
+ portalDisplayChatIDs,
195
+ portalDisplayChats,
196
+ portalDisplayChatsLength,
197
+ portalDisplayChatsString,
198
+ portalDisplayChildChatsByThreadId,
199
+ threadSourceMessageId,
200
+ threadSourceMessageIndex,
201
+ threadStartMessageId,
202
+ };
@@ -0,0 +1,22 @@
1
+ import { ChatMessage } from '@/types/message';
2
+ import { ThreadType } from '@/types/topic';
3
+
4
+ export const genMessage = (
5
+ messages: ChatMessage[],
6
+ startMessageId: string | undefined,
7
+ threadMode?: ThreadType,
8
+ ) => {
9
+ if (!startMessageId) return [];
10
+
11
+ // 如果是独立话题模式,则只显示话题开始消息
12
+ if (threadMode === ThreadType.Standalone) {
13
+ return messages.filter((m) => m.id === startMessageId);
14
+ }
15
+
16
+ // 如果是连续模式下,那么只显示话题开始消息和话题分割线
17
+ const targetIndex = messages.findIndex((item) => item.id === startMessageId);
18
+
19
+ if (targetIndex < 0) return [];
20
+
21
+ return messages.slice(0, targetIndex + 1);
22
+ };
@@ -223,7 +223,11 @@ export const chatTopic: StateCreator<
223
223
  },
224
224
  ),
225
225
  switchTopic: async (id, skipRefreshMessage) => {
226
- set({ activeTopicId: !id ? (null as any) : id }, false, n('toggleTopic'));
226
+ set(
227
+ { activeTopicId: !id ? (null as any) : id, activeThreadId: undefined },
228
+ false,
229
+ n('toggleTopic'),
230
+ );
227
231
 
228
232
  if (skipRefreshMessage) return;
229
233
  await get().refreshMessages();
@@ -13,11 +13,13 @@ import { ChatMessageAction, chatMessage } from './slices/message/action';
13
13
  import { ChatPluginAction, chatPlugin } from './slices/plugin/action';
14
14
  import { ShareAction, chatShare } from './slices/share/action';
15
15
  import { ChatTopicAction, chatTopic } from './slices/topic/action';
16
- import { chatAiChat, ChatAIChatAction } from './slices/aiChat/actions';
17
- import { chatTTS, ChatTTSAction } from './slices/tts/action';
16
+ import { ChatAIChatAction, chatAiChat } from './slices/aiChat/actions';
17
+ import { ChatTTSAction, chatTTS } from './slices/tts/action';
18
+ import { ChatThreadAction, chatThreadMessage } from './slices/thread/action';
18
19
 
19
20
  export interface ChatStoreAction
20
21
  extends ChatMessageAction,
22
+ ChatThreadAction,
21
23
  ChatAIChatAction,
22
24
  ChatTopicAction,
23
25
  ShareAction,
@@ -35,6 +37,7 @@ const createStore: StateCreator<ChatStore, [['zustand/devtools', never]]> = (...
35
37
  ...initialState,
36
38
 
37
39
  ...chatMessage(...params),
40
+ ...chatThreadMessage(...params),
38
41
  ...chatAiChat(...params),
39
42
  ...chatTopic(...params),
40
43
  ...chatShare(...params),