@lobehub/lobehub 2.0.0-next.34 → 2.0.0-next.36

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 (282) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/changelog/v1.json +18 -0
  3. package/next.config.ts +5 -6
  4. package/package.json +2 -2
  5. package/packages/agent-runtime/src/core/__tests__/runtime.test.ts +112 -77
  6. package/packages/agent-runtime/src/core/runtime.ts +63 -18
  7. package/packages/agent-runtime/src/types/generalAgent.ts +55 -0
  8. package/packages/agent-runtime/src/types/index.ts +1 -0
  9. package/packages/agent-runtime/src/types/instruction.ts +10 -3
  10. package/packages/const/src/user.ts +0 -1
  11. package/packages/context-engine/src/processors/GroupMessageFlatten.ts +8 -6
  12. package/packages/context-engine/src/processors/__tests__/GroupMessageFlatten.test.ts +12 -12
  13. package/packages/conversation-flow/src/__tests__/fixtures/inputs/branch/assistant-group-branches.json +249 -0
  14. package/packages/conversation-flow/src/__tests__/fixtures/inputs/branch/index.ts +4 -0
  15. package/packages/conversation-flow/src/__tests__/fixtures/inputs/branch/multi-assistant-group.json +260 -0
  16. package/packages/conversation-flow/src/__tests__/fixtures/outputs/branch/active-index-1.json +4 -0
  17. package/packages/conversation-flow/src/__tests__/fixtures/outputs/branch/assistant-group-branches.json +481 -0
  18. package/packages/conversation-flow/src/__tests__/fixtures/outputs/branch/conversation.json +5 -1
  19. package/packages/conversation-flow/src/__tests__/fixtures/outputs/branch/index.ts +4 -0
  20. package/packages/conversation-flow/src/__tests__/fixtures/outputs/branch/multi-assistant-group.json +407 -0
  21. package/packages/conversation-flow/src/__tests__/fixtures/outputs/branch/nested.json +18 -2
  22. package/packages/conversation-flow/src/__tests__/fixtures/outputs/complex-scenario.json +25 -3
  23. package/packages/conversation-flow/src/__tests__/parse.test.ts +12 -0
  24. package/packages/conversation-flow/src/index.ts +1 -1
  25. package/packages/conversation-flow/src/transformation/FlatListBuilder.ts +112 -34
  26. package/packages/conversation-flow/src/types/flatMessageList.ts +0 -12
  27. package/packages/conversation-flow/src/{types.ts → types/index.ts} +3 -14
  28. package/packages/database/src/models/message.ts +18 -19
  29. package/packages/types/src/aiChat.ts +2 -0
  30. package/packages/types/src/importer.ts +2 -2
  31. package/packages/types/src/message/ui/chat.ts +17 -1
  32. package/packages/types/src/message/ui/extra.ts +2 -2
  33. package/packages/types/src/message/ui/params.ts +2 -2
  34. package/packages/types/src/user/preference.ts +0 -4
  35. package/packages/utils/src/tokenizer/index.ts +3 -11
  36. package/src/app/[variants]/(main)/chat/ChatRouter.tsx +83 -0
  37. package/src/app/[variants]/(main)/chat/_layout/ChatLayout.tsx +22 -0
  38. package/src/app/[variants]/(main)/chat/_layout/Desktop/SessionPanel.tsx +12 -7
  39. package/src/app/[variants]/(main)/chat/_layout/Desktop/index.tsx +2 -2
  40. package/src/app/[variants]/(main)/chat/_layout/FeatureFlagsProvider.tsx +24 -0
  41. package/src/app/[variants]/(main)/chat/_layout/Mobile.tsx +3 -2
  42. package/src/app/[variants]/(main)/chat/_layout/type.ts +0 -1
  43. package/src/app/[variants]/(main)/chat/components/ConversationArea.tsx +29 -0
  44. package/src/app/[variants]/(main)/chat/components/MainChatPage.tsx +25 -0
  45. package/src/app/[variants]/(main)/chat/components/PortalPanel.tsx +28 -0
  46. package/src/app/[variants]/(main)/chat/components/SessionPanel.tsx +33 -0
  47. package/src/app/[variants]/(main)/chat/{settings/page.tsx → components/SettingsPage.tsx} +35 -3
  48. package/src/app/[variants]/(main)/chat/components/TopicSidebar.tsx +30 -0
  49. package/src/app/[variants]/(main)/chat/components/WorkspaceLayout.tsx +73 -0
  50. package/src/app/[variants]/(main)/chat/{(workspace)/@conversation → components/conversation}/features/ChatInput/Desktop/MessageFromUrl.tsx +3 -3
  51. package/src/app/[variants]/(main)/chat/{(workspace)/@conversation → components/conversation}/features/ChatInput/V1Mobile/index.tsx +1 -1
  52. package/src/app/[variants]/(main)/chat/{(workspace)/@conversation → components/conversation}/features/ChatInput/V1Mobile/useSend.ts +3 -3
  53. package/src/app/[variants]/(main)/chat/{(workspace)/@conversation → components/conversation}/features/ChatInput/useSend.ts +6 -6
  54. package/src/app/[variants]/(main)/chat/{(workspace)/@conversation → components/conversation}/features/ChatList/ChatItem/index.tsx +1 -1
  55. package/src/app/[variants]/(main)/chat/{(workspace)/@conversation → components/conversation}/features/ChatList/Content.tsx +5 -3
  56. package/src/app/[variants]/(main)/chat/{(workspace)/@conversation → components/conversation}/features/ChatList/WelcomeChatItem/AgentWelcome/OpeningQuestions.tsx +2 -2
  57. package/src/app/[variants]/(main)/chat/{(workspace)/@conversation → components/conversation}/features/ChatList/WelcomeChatItem/GroupWelcome/GroupUsageSuggest.tsx +2 -2
  58. package/src/app/[variants]/(main)/chat/{layout.ts → layout.tsx} +0 -1
  59. package/src/app/[variants]/(main)/chat/page.tsx +12 -0
  60. package/src/app/[variants]/(main)/labs/page.tsx +0 -9
  61. package/src/features/ChatInput/ActionBar/STT/browser.tsx +3 -3
  62. package/src/features/ChatInput/ActionBar/STT/openai.tsx +3 -3
  63. package/src/features/Conversation/Error/AccessCodeForm.tsx +1 -1
  64. package/src/features/Conversation/Error/ChatInvalidApiKey.tsx +1 -1
  65. package/src/features/Conversation/Error/ClerkLogin/index.tsx +1 -1
  66. package/src/features/Conversation/Error/OAuthForm.tsx +1 -1
  67. package/src/features/Conversation/Error/index.tsx +0 -5
  68. package/src/features/Conversation/Messages/Assistant/Actions/index.tsx +13 -10
  69. package/src/features/Conversation/Messages/Assistant/Extra/index.test.tsx +3 -8
  70. package/src/features/Conversation/Messages/Assistant/Extra/index.tsx +2 -6
  71. package/src/features/Conversation/Messages/Assistant/MessageContent.tsx +7 -9
  72. package/src/features/Conversation/Messages/Assistant/Tool/Inspector/PluginResult.tsx +2 -2
  73. package/src/features/Conversation/Messages/Assistant/Tool/Inspector/PluginState.tsx +2 -2
  74. package/src/features/Conversation/Messages/Assistant/Tool/Render/PluginSettings.tsx +4 -1
  75. package/src/features/Conversation/Messages/Assistant/Tool/Render/index.tsx +2 -3
  76. package/src/features/Conversation/Messages/Assistant/index.tsx +57 -60
  77. package/src/features/Conversation/Messages/Default.tsx +1 -0
  78. package/src/features/Conversation/Messages/Group/Actions/WithContentId.tsx +38 -10
  79. package/src/features/Conversation/Messages/Group/Actions/index.tsx +1 -1
  80. package/src/features/Conversation/Messages/Group/ContentBlock.tsx +1 -3
  81. package/src/features/Conversation/Messages/Group/GroupChildren.tsx +12 -12
  82. package/src/features/Conversation/Messages/Group/MessageContent.tsx +7 -1
  83. package/src/features/Conversation/Messages/Group/Tool/Render/PluginSettings.tsx +1 -1
  84. package/src/features/Conversation/Messages/Group/index.tsx +2 -1
  85. package/src/features/Conversation/Messages/Supervisor/index.tsx +2 -2
  86. package/src/features/Conversation/Messages/User/{Actions.tsx → Actions/ActionsBar.tsx} +26 -25
  87. package/src/features/Conversation/Messages/User/Actions/MessageBranch.tsx +107 -0
  88. package/src/features/Conversation/Messages/User/Actions/index.tsx +42 -0
  89. package/src/features/Conversation/Messages/User/index.tsx +43 -44
  90. package/src/features/Conversation/Messages/index.tsx +3 -3
  91. package/src/features/Conversation/components/AutoScroll.tsx +3 -3
  92. package/src/features/Conversation/components/Extras/Usage/UsageDetail/AnimatedNumber.tsx +55 -0
  93. package/src/features/Conversation/components/Extras/Usage/UsageDetail/index.tsx +5 -2
  94. package/src/features/Conversation/components/VirtualizedList/index.tsx +29 -20
  95. package/src/features/Conversation/hooks/useChatListActionsBar.tsx +8 -10
  96. package/src/features/Portal/GroupThread/Body/index.tsx +1 -1
  97. package/src/features/Portal/Thread/Chat/ChatInput/useSend.ts +3 -3
  98. package/src/hooks/useHotkeys/chatScope.ts +16 -8
  99. package/src/server/routers/lambda/__tests__/aiChat.test.ts +1 -1
  100. package/src/server/routers/lambda/__tests__/integration/message.integration.test.ts +0 -26
  101. package/src/server/routers/lambda/aiChat.ts +3 -2
  102. package/src/server/routers/lambda/message.ts +8 -16
  103. package/src/server/services/message/__tests__/index.test.ts +29 -39
  104. package/src/server/services/message/index.ts +41 -36
  105. package/src/services/electron/desktopNotification.ts +6 -6
  106. package/src/services/electron/file.ts +6 -6
  107. package/src/services/file/ClientS3/index.ts +8 -8
  108. package/src/services/message/__tests__/metadata-race-condition.test.ts +157 -0
  109. package/src/services/message/index.ts +21 -15
  110. package/src/services/upload.ts +11 -11
  111. package/src/services/utils/abortableRequest.test.ts +161 -0
  112. package/src/services/utils/abortableRequest.ts +67 -0
  113. package/src/store/chat/agents/GeneralChatAgent.ts +137 -0
  114. package/src/store/chat/agents/createAgentExecutors.ts +395 -0
  115. package/src/store/chat/helpers.test.ts +0 -99
  116. package/src/store/chat/helpers.ts +0 -11
  117. package/src/store/chat/slices/aiChat/actions/__tests__/conversationControl.test.ts +332 -0
  118. package/src/store/chat/slices/aiChat/actions/__tests__/conversationLifecycle.test.ts +257 -0
  119. package/src/store/chat/slices/aiChat/actions/__tests__/helpers.ts +11 -2
  120. package/src/store/chat/slices/aiChat/actions/__tests__/rag.test.ts +6 -6
  121. package/src/store/chat/slices/aiChat/actions/__tests__/streamingExecutor.test.ts +391 -0
  122. package/src/store/chat/slices/aiChat/actions/__tests__/streamingStates.test.ts +179 -0
  123. package/src/store/chat/slices/aiChat/actions/conversationControl.ts +157 -0
  124. package/src/store/chat/slices/aiChat/actions/conversationLifecycle.ts +329 -0
  125. package/src/store/chat/slices/aiChat/actions/generateAIGroupChat.ts +14 -14
  126. package/src/store/chat/slices/aiChat/actions/index.ts +12 -6
  127. package/src/store/chat/slices/aiChat/actions/rag.ts +9 -6
  128. package/src/store/chat/slices/aiChat/actions/streamingExecutor.ts +604 -0
  129. package/src/store/chat/slices/aiChat/actions/streamingStates.ts +84 -0
  130. package/src/store/chat/slices/builtinTool/actions/__tests__/localSystem.test.ts +4 -4
  131. package/src/store/chat/slices/builtinTool/actions/__tests__/search.test.ts +11 -11
  132. package/src/store/chat/slices/builtinTool/actions/interpreter.ts +8 -8
  133. package/src/store/chat/slices/builtinTool/actions/localSystem.ts +2 -2
  134. package/src/store/chat/slices/builtinTool/actions/search.ts +8 -8
  135. package/src/store/chat/slices/message/action.test.ts +79 -68
  136. package/src/store/chat/slices/message/actions/index.ts +39 -0
  137. package/src/store/chat/slices/message/actions/internals.ts +77 -0
  138. package/src/store/chat/slices/message/actions/optimisticUpdate.ts +260 -0
  139. package/src/store/chat/slices/message/actions/publicApi.ts +224 -0
  140. package/src/store/chat/slices/message/actions/query.ts +120 -0
  141. package/src/store/chat/slices/message/actions/runtimeState.ts +108 -0
  142. package/src/store/chat/slices/message/initialState.ts +13 -0
  143. package/src/store/chat/slices/message/reducer.test.ts +48 -370
  144. package/src/store/chat/slices/message/reducer.ts +17 -81
  145. package/src/store/chat/slices/message/selectors/chat.test.ts +13 -50
  146. package/src/store/chat/slices/message/selectors/chat.ts +78 -242
  147. package/src/store/chat/slices/message/selectors/dbMessage.ts +140 -0
  148. package/src/store/chat/slices/message/selectors/displayMessage.ts +301 -0
  149. package/src/store/chat/slices/message/selectors/messageState.ts +5 -2
  150. package/src/store/chat/slices/plugin/action.test.ts +62 -64
  151. package/src/store/chat/slices/plugin/action.ts +34 -28
  152. package/src/store/chat/slices/thread/action.test.ts +28 -31
  153. package/src/store/chat/slices/thread/action.ts +13 -10
  154. package/src/store/chat/slices/thread/selectors/index.ts +8 -6
  155. package/src/store/chat/slices/topic/reducer.ts +11 -3
  156. package/src/store/chat/store.ts +1 -1
  157. package/src/store/user/slices/preference/selectors/labPrefer.ts +0 -3
  158. package/packages/database/src/models/__tests__/message.grouping.test.ts +0 -812
  159. package/packages/database/src/utils/__tests__/groupMessages.test.ts +0 -1132
  160. package/packages/database/src/utils/groupMessages.ts +0 -361
  161. package/packages/utils/src/tokenizer/client.ts +0 -35
  162. package/packages/utils/src/tokenizer/estimated.ts +0 -4
  163. package/packages/utils/src/tokenizer/server.ts +0 -11
  164. package/packages/utils/src/tokenizer/tokenizer.worker.ts +0 -12
  165. package/src/app/(backend)/webapi/tokenizer/index.test.ts +0 -32
  166. package/src/app/(backend)/webapi/tokenizer/route.ts +0 -8
  167. package/src/app/[variants]/(main)/chat/(workspace)/layout.ts +0 -11
  168. package/src/app/[variants]/(main)/chat/(workspace)/page.tsx +0 -53
  169. package/src/app/[variants]/(main)/chat/@session/default.tsx +0 -31
  170. package/src/app/[variants]/(main)/chat/settings/layout.tsx +0 -21
  171. package/src/features/Conversation/Error/InvalidAccessCode.tsx +0 -79
  172. package/src/store/chat/slices/aiChat/actions/__tests__/generateAIChat.test.ts +0 -975
  173. package/src/store/chat/slices/aiChat/actions/__tests__/generateAIChatV2.test.ts +0 -1050
  174. package/src/store/chat/slices/aiChat/actions/generateAIChat.ts +0 -720
  175. package/src/store/chat/slices/aiChat/actions/generateAIChatV2.ts +0 -849
  176. package/src/store/chat/slices/message/action.ts +0 -629
  177. /package/src/app/[variants]/(main)/chat/{(workspace)/@conversation → components/conversation}/default.tsx +0 -0
  178. /package/src/app/[variants]/(main)/chat/{(workspace)/@conversation → components/conversation}/features/ChatHydration/index.tsx +0 -0
  179. /package/src/app/[variants]/(main)/chat/{(workspace)/@conversation → components/conversation}/features/ChatInput/Desktop/ClassicChat.tsx +0 -0
  180. /package/src/app/[variants]/(main)/chat/{(workspace)/@conversation → components/conversation}/features/ChatInput/Desktop/GroupChat.tsx +0 -0
  181. /package/src/app/[variants]/(main)/chat/{(workspace)/@conversation → components/conversation}/features/ChatInput/Desktop/index.tsx +0 -0
  182. /package/src/app/[variants]/(main)/chat/{(workspace)/@conversation → components/conversation}/features/ChatInput/Desktop/useSendMenuItems.tsx +0 -0
  183. /package/src/app/[variants]/(main)/chat/{(workspace)/@conversation → components/conversation}/features/ChatInput/Mobile/MentionedUsers/MentionedUserItem.tsx +0 -0
  184. /package/src/app/[variants]/(main)/chat/{(workspace)/@conversation → components/conversation}/features/ChatInput/Mobile/MentionedUsers/index.tsx +0 -0
  185. /package/src/app/[variants]/(main)/chat/{(workspace)/@conversation → components/conversation}/features/ChatInput/Mobile/index.tsx +0 -0
  186. /package/src/app/[variants]/(main)/chat/{(workspace)/@conversation → components/conversation}/features/ChatInput/V1Mobile/ActionBar.tsx +0 -0
  187. /package/src/app/[variants]/(main)/chat/{(workspace)/@conversation → components/conversation}/features/ChatInput/V1Mobile/Files/index.tsx +0 -0
  188. /package/src/app/[variants]/(main)/chat/{(workspace)/@conversation → components/conversation}/features/ChatInput/V1Mobile/InputArea/Container.tsx +0 -0
  189. /package/src/app/[variants]/(main)/chat/{(workspace)/@conversation → components/conversation}/features/ChatInput/V1Mobile/InputArea/index.tsx +0 -0
  190. /package/src/app/[variants]/(main)/chat/{(workspace)/@conversation → components/conversation}/features/ChatInput/V1Mobile/Send.tsx +0 -0
  191. /package/src/app/[variants]/(main)/chat/{(workspace)/@conversation → components/conversation}/features/ChatInput/index.tsx +0 -0
  192. /package/src/app/[variants]/(main)/chat/{(workspace)/@conversation → components/conversation}/features/ChatList/ChatItem/OrchestratorThinking.tsx +0 -0
  193. /package/src/app/[variants]/(main)/chat/{(workspace)/@conversation → components/conversation}/features/ChatList/ChatItem/Thread.tsx +0 -0
  194. /package/src/app/[variants]/(main)/chat/{(workspace)/@conversation → components/conversation}/features/ChatList/ChatItem/ThreadItem.tsx +0 -0
  195. /package/src/app/[variants]/(main)/chat/{(workspace)/@conversation → components/conversation}/features/ChatList/WelcomeChatItem/AgentWelcome/AddButton.tsx +0 -0
  196. /package/src/app/[variants]/(main)/chat/{(workspace)/@conversation → components/conversation}/features/ChatList/WelcomeChatItem/AgentWelcome/index.tsx +0 -0
  197. /package/src/app/[variants]/(main)/chat/{(workspace)/@conversation → components/conversation}/features/ChatList/WelcomeChatItem/GroupWelcome/index.tsx +0 -0
  198. /package/src/app/[variants]/(main)/chat/{(workspace)/@conversation → components/conversation}/features/ChatList/WelcomeChatItem/GroupWelcome/useTemplateMatching.ts +0 -0
  199. /package/src/app/[variants]/(main)/chat/{(workspace)/@conversation → components/conversation}/features/ChatList/WelcomeChatItem/index.tsx +0 -0
  200. /package/src/app/[variants]/(main)/chat/{(workspace)/@conversation → components/conversation}/features/ChatList/index.tsx +0 -0
  201. /package/src/app/[variants]/(main)/chat/{(workspace)/@conversation → components/conversation}/features/ChatMinimap/index.tsx +0 -0
  202. /package/src/app/[variants]/(main)/chat/{(workspace)/@conversation → components/conversation}/features/ThreadHydration.tsx +0 -0
  203. /package/src/app/[variants]/(main)/chat/{(workspace)/@conversation → components/conversation}/features/ZenModeToast/Toast.tsx +0 -0
  204. /package/src/app/[variants]/(main)/chat/{(workspace)/@conversation → components/conversation}/features/ZenModeToast/index.tsx +0 -0
  205. /package/src/app/[variants]/(main)/chat/{(workspace) → components}/features/AgentSettings/index.tsx +0 -0
  206. /package/src/app/[variants]/(main)/chat/{(workspace) → components}/features/AgentTeamSettings/index.tsx +0 -0
  207. /package/src/app/[variants]/(main)/chat/{(workspace) → components}/features/ChangelogModal.tsx +0 -0
  208. /package/src/app/[variants]/(main)/chat/{(workspace) → components}/features/SettingButton.tsx +0 -0
  209. /package/src/app/[variants]/(main)/chat/{(workspace) → components}/features/ShareButton/index.tsx +0 -0
  210. /package/src/app/[variants]/(main)/chat/{(workspace) → components}/features/TelemetryNotification.tsx +0 -0
  211. /package/src/app/[variants]/(main)/chat/{(workspace)/_layout → components/layout}/Desktop/ChatHeader/HeaderAction.tsx +0 -0
  212. /package/src/app/[variants]/(main)/chat/{(workspace)/_layout → components/layout}/Desktop/ChatHeader/Main.tsx +0 -0
  213. /package/src/app/[variants]/(main)/chat/{(workspace)/_layout → components/layout}/Desktop/ChatHeader/Tags/HistoryLimitTags.tsx +0 -0
  214. /package/src/app/[variants]/(main)/chat/{(workspace)/_layout → components/layout}/Desktop/ChatHeader/Tags/KnowledgeTag.tsx +0 -0
  215. /package/src/app/[variants]/(main)/chat/{(workspace)/_layout → components/layout}/Desktop/ChatHeader/Tags/MemberCountTag.tsx +0 -0
  216. /package/src/app/[variants]/(main)/chat/{(workspace)/_layout → components/layout}/Desktop/ChatHeader/Tags/SearchTags.tsx +0 -0
  217. /package/src/app/[variants]/(main)/chat/{(workspace)/_layout → components/layout}/Desktop/ChatHeader/Tags/index.tsx +0 -0
  218. /package/src/app/[variants]/(main)/chat/{(workspace)/_layout → components/layout}/Desktop/ChatHeader/index.tsx +0 -0
  219. /package/src/app/[variants]/(main)/chat/{(workspace)/_layout → components/layout}/Desktop/Portal.tsx +0 -0
  220. /package/src/app/[variants]/(main)/chat/{(workspace)/_layout → components/layout}/Desktop/TopicPanel.tsx +0 -0
  221. /package/src/app/[variants]/(main)/chat/{(workspace)/_layout → components/layout}/Desktop/index.tsx +0 -0
  222. /package/src/app/[variants]/(main)/chat/{(workspace)/_layout → components/layout}/Mobile/ChatHeader/ChatHeaderTitle.tsx +0 -0
  223. /package/src/app/[variants]/(main)/chat/{(workspace)/_layout → components/layout}/Mobile/ChatHeader/index.tsx +0 -0
  224. /package/src/app/[variants]/(main)/chat/{(workspace)/_layout → components/layout}/Mobile/TopicModal.tsx +0 -0
  225. /package/src/app/[variants]/(main)/chat/{(workspace)/_layout → components/layout}/Mobile/index.tsx +0 -0
  226. /package/src/app/[variants]/(main)/chat/{(workspace)/_layout → components/layout}/type.ts +0 -0
  227. /package/src/app/[variants]/(main)/chat/{(workspace)/@portal → components/portal}/_layout/Desktop.tsx +0 -0
  228. /package/src/app/[variants]/(main)/chat/{(workspace)/@portal → components/portal}/_layout/Mobile.tsx +0 -0
  229. /package/src/app/[variants]/(main)/chat/{(workspace)/@portal → components/portal}/default.tsx +0 -0
  230. /package/src/app/[variants]/(main)/chat/{(workspace)/@portal → components/portal}/error.tsx +0 -0
  231. /package/src/app/[variants]/(main)/chat/{(workspace)/@portal → components/portal}/features/Body.tsx +0 -0
  232. /package/src/app/[variants]/(main)/chat/{(workspace)/@portal → components/portal}/loading.tsx +0 -0
  233. /package/src/app/[variants]/(main)/chat/{(workspace)/@topic → components/topic}/_layout/Desktop.tsx +0 -0
  234. /package/src/app/[variants]/(main)/chat/{(workspace)/@topic → components/topic}/_layout/Mobile.tsx +0 -0
  235. /package/src/app/[variants]/(main)/chat/{(workspace)/@topic → components/topic}/default.tsx +0 -0
  236. /package/src/app/[variants]/(main)/chat/{(workspace)/@topic → components/topic}/features/AgentConfig/SystemRole.tsx +0 -0
  237. /package/src/app/[variants]/(main)/chat/{(workspace)/@topic → components/topic}/features/AgentConfig/index.tsx +0 -0
  238. /package/src/app/[variants]/(main)/chat/{(workspace)/@topic → components/topic}/features/ConfigLayout.tsx +0 -0
  239. /package/src/app/[variants]/(main)/chat/{(workspace)/@topic → components/topic}/features/ConfigSwitcher.tsx +0 -0
  240. /package/src/app/[variants]/(main)/chat/{(workspace)/@topic → components/topic}/features/GroupConfig/GroupMember.tsx +0 -0
  241. /package/src/app/[variants]/(main)/chat/{(workspace)/@topic → components/topic}/features/GroupConfig/GroupMemberItem.tsx +0 -0
  242. /package/src/app/[variants]/(main)/chat/{(workspace)/@topic → components/topic}/features/GroupConfig/GroupRole.tsx +0 -0
  243. /package/src/app/[variants]/(main)/chat/{(workspace)/@topic → components/topic}/features/GroupConfig/index.tsx +0 -0
  244. /package/src/app/[variants]/(main)/chat/{(workspace)/@topic → components/topic}/features/GroupConfig/style.ts +0 -0
  245. /package/src/app/[variants]/(main)/chat/{(workspace)/@topic → components/topic}/features/SkeletonList.tsx +0 -0
  246. /package/src/app/[variants]/(main)/chat/{(workspace)/@topic → components/topic}/features/Topic/Header.tsx +0 -0
  247. /package/src/app/[variants]/(main)/chat/{(workspace)/@topic → components/topic}/features/Topic/TopicListContent/ByTimeMode/GroupItem.tsx +0 -0
  248. /package/src/app/[variants]/(main)/chat/{(workspace)/@topic → components/topic}/features/Topic/TopicListContent/ByTimeMode/index.tsx +0 -0
  249. /package/src/app/[variants]/(main)/chat/{(workspace)/@topic → components/topic}/features/Topic/TopicListContent/FlatMode/index.tsx +0 -0
  250. /package/src/app/[variants]/(main)/chat/{(workspace)/@topic → components/topic}/features/Topic/TopicListContent/SearchResult/index.tsx +0 -0
  251. /package/src/app/[variants]/(main)/chat/{(workspace)/@topic → components/topic}/features/Topic/TopicListContent/ThreadItem/Content.tsx +0 -0
  252. /package/src/app/[variants]/(main)/chat/{(workspace)/@topic → components/topic}/features/Topic/TopicListContent/ThreadItem/index.tsx +0 -0
  253. /package/src/app/[variants]/(main)/chat/{(workspace)/@topic → components/topic}/features/Topic/TopicListContent/ThreadList/index.tsx +0 -0
  254. /package/src/app/[variants]/(main)/chat/{(workspace)/@topic → components/topic}/features/Topic/TopicListContent/TopicItem/DefaultContent.tsx +0 -0
  255. /package/src/app/[variants]/(main)/chat/{(workspace)/@topic → components/topic}/features/Topic/TopicListContent/TopicItem/TopicContent.tsx +0 -0
  256. /package/src/app/[variants]/(main)/chat/{(workspace)/@topic → components/topic}/features/Topic/TopicListContent/TopicItem/index.tsx +0 -0
  257. /package/src/app/[variants]/(main)/chat/{(workspace)/@topic → components/topic}/features/Topic/TopicListContent/index.tsx +0 -0
  258. /package/src/app/[variants]/(main)/chat/{(workspace)/@topic → components/topic}/features/Topic/TopicSearchBar/index.tsx +0 -0
  259. /package/src/app/[variants]/(main)/chat/{(workspace)/@topic → components/topic}/features/Topic/index.tsx +0 -0
  260. /package/src/app/[variants]/(main)/chat/{@session → session}/features/SessionHydration.tsx +0 -0
  261. /package/src/app/[variants]/(main)/chat/{@session → session}/features/SessionListContent/CollapseGroup/Actions.tsx +0 -0
  262. /package/src/app/[variants]/(main)/chat/{@session → session}/features/SessionListContent/CollapseGroup/index.tsx +0 -0
  263. /package/src/app/[variants]/(main)/chat/{@session → session}/features/SessionListContent/DefaultMode.tsx +0 -0
  264. /package/src/app/[variants]/(main)/chat/{@session → session}/features/SessionListContent/Inbox/index.tsx +0 -0
  265. /package/src/app/[variants]/(main)/chat/{@session → session}/features/SessionListContent/List/AddButton.tsx +0 -0
  266. /package/src/app/[variants]/(main)/chat/{@session → session}/features/SessionListContent/List/Item/Actions.tsx +0 -0
  267. /package/src/app/[variants]/(main)/chat/{@session → session}/features/SessionListContent/List/Item/index.tsx +0 -0
  268. /package/src/app/[variants]/(main)/chat/{@session → session}/features/SessionListContent/List/index.tsx +0 -0
  269. /package/src/app/[variants]/(main)/chat/{@session → session}/features/SessionListContent/ListItem/index.tsx +0 -0
  270. /package/src/app/[variants]/(main)/chat/{@session → session}/features/SessionListContent/Modals/ConfigGroupModal/GroupItem.tsx +0 -0
  271. /package/src/app/[variants]/(main)/chat/{@session → session}/features/SessionListContent/Modals/ConfigGroupModal/index.tsx +0 -0
  272. /package/src/app/[variants]/(main)/chat/{@session → session}/features/SessionListContent/Modals/CreateGroupModal.tsx +0 -0
  273. /package/src/app/[variants]/(main)/chat/{@session → session}/features/SessionListContent/Modals/RenameGroupModal.tsx +0 -0
  274. /package/src/app/[variants]/(main)/chat/{@session → session}/features/SessionListContent/SearchMode.tsx +0 -0
  275. /package/src/app/[variants]/(main)/chat/{@session → session}/features/SessionListContent/index.tsx +0 -0
  276. /package/src/app/[variants]/(main)/chat/{@session → session}/features/SessionSearchBar.tsx +0 -0
  277. /package/src/app/[variants]/(main)/chat/{@session → session}/features/SkeletonList.tsx +0 -0
  278. /package/src/app/[variants]/(main)/chat/{@session/_layout → session/layout}/Desktop/PanelBody.tsx +0 -0
  279. /package/src/app/[variants]/(main)/chat/{@session/_layout → session/layout}/Desktop/SessionHeader.tsx +0 -0
  280. /package/src/app/[variants]/(main)/chat/{@session/_layout → session/layout}/Desktop/index.tsx +0 -0
  281. /package/src/app/[variants]/(main)/chat/{@session/_layout → session/layout}/Mobile/SessionHeader.tsx +0 -0
  282. /package/src/app/[variants]/(main)/chat/{@session/_layout → session/layout}/Mobile/index.tsx +0 -0
@@ -1,975 +0,0 @@
1
- import { act, renderHook } from '@testing-library/react';
2
- import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
3
-
4
- import { LOADING_FLAT } from '@/const/message';
5
- import { chatService } from '@/services/chat';
6
- import { messageService } from '@/services/message';
7
- import { chatSelectors } from '@/store/chat/selectors';
8
- import { messageMapKey } from '@/store/chat/utils/messageMapKey';
9
- import { UploadFileItem } from '@/types/files/upload';
10
-
11
- import { useChatStore } from '../../../../store';
12
- import { TEST_CONTENT, TEST_IDS, createMockMessage, createMockMessages } from './fixtures';
13
- import {
14
- resetTestEnvironment,
15
- setupMockSelectors,
16
- setupStoreWithMessages,
17
- spyOnChatService,
18
- spyOnMessageService,
19
- } from './helpers';
20
-
21
- // Keep zustand mock as it's needed globally
22
- vi.mock('zustand/traditional');
23
-
24
- const realCoreProcessMessage = useChatStore.getState().internal_coreProcessMessage;
25
-
26
- beforeEach(() => {
27
- resetTestEnvironment();
28
- setupMockSelectors();
29
-
30
- // Setup default spies that most tests need
31
- spyOnMessageService();
32
- // ✅ Removed spyOnChatService() - tests should spy chatService only when needed
33
-
34
- // Setup common mock methods that most tests need
35
- act(() => {
36
- useChatStore.setState({
37
- refreshMessages: vi.fn(),
38
- refreshTopic: vi.fn(),
39
- internal_coreProcessMessage: vi.fn(),
40
- });
41
- });
42
- });
43
-
44
- afterEach(() => {
45
- process.env.NEXT_PUBLIC_BASE_PATH = undefined;
46
-
47
- vi.restoreAllMocks();
48
- });
49
-
50
- describe('chatMessage actions', () => {
51
- describe('sendMessage', () => {
52
- describe('validation', () => {
53
- it('should not send when there is no active session', async () => {
54
- act(() => {
55
- useChatStore.setState({ activeId: undefined });
56
- });
57
-
58
- const { result } = renderHook(() => useChatStore());
59
-
60
- await act(async () => {
61
- await result.current.sendMessage({ message: TEST_CONTENT.USER_MESSAGE });
62
- });
63
-
64
- expect(messageService.createMessage).not.toHaveBeenCalled();
65
- expect(result.current.internal_coreProcessMessage).not.toHaveBeenCalled();
66
- });
67
-
68
- it('should not send when message is empty and no files are provided', async () => {
69
- const { result } = renderHook(() => useChatStore());
70
-
71
- await act(async () => {
72
- await result.current.sendMessage({ message: TEST_CONTENT.EMPTY });
73
- });
74
-
75
- expect(messageService.createMessage).not.toHaveBeenCalled();
76
- });
77
-
78
- it('should not send when message is empty with empty files array', async () => {
79
- const { result } = renderHook(() => useChatStore());
80
-
81
- await act(async () => {
82
- await result.current.sendMessage({ message: TEST_CONTENT.EMPTY, files: [] });
83
- });
84
-
85
- expect(messageService.createMessage).not.toHaveBeenCalled();
86
- });
87
- });
88
-
89
- describe('message creation', () => {
90
- it('should not process AI when onlyAddUserMessage is true', async () => {
91
- const { result } = renderHook(() => useChatStore());
92
-
93
- await act(async () => {
94
- await result.current.sendMessage({
95
- message: TEST_CONTENT.USER_MESSAGE,
96
- onlyAddUserMessage: true,
97
- });
98
- });
99
-
100
- expect(messageService.createMessage).toHaveBeenCalled();
101
- expect(result.current.internal_coreProcessMessage).not.toHaveBeenCalled();
102
- });
103
-
104
- it('should handle message creation errors gracefully', async () => {
105
- const { result } = renderHook(() => useChatStore());
106
- vi.spyOn(messageService, 'createMessage').mockRejectedValue(
107
- new Error('create message error'),
108
- );
109
-
110
- await act(async () => {
111
- try {
112
- await result.current.sendMessage({ message: TEST_CONTENT.USER_MESSAGE });
113
- } catch {
114
- // Expected to throw
115
- }
116
- });
117
-
118
- expect(result.current.internal_coreProcessMessage).not.toHaveBeenCalled();
119
- });
120
- });
121
- });
122
-
123
- describe('regenerateMessage', () => {
124
- it('should trigger message regeneration', async () => {
125
- const { result } = renderHook(() => useChatStore());
126
- const traceId = 'test-trace-id';
127
-
128
- act(() => {
129
- setupStoreWithMessages([
130
- createMockMessage({
131
- id: TEST_IDS.MESSAGE_ID,
132
- tools: [{ id: 'tool1' }, { id: 'tool2' }] as any,
133
- traceId,
134
- }),
135
- ]);
136
- });
137
-
138
- const resendMessageSpy = vi.spyOn(result.current, 'internal_resendMessage');
139
-
140
- await act(async () => {
141
- await result.current.regenerateMessage(TEST_IDS.MESSAGE_ID);
142
- });
143
-
144
- expect(resendMessageSpy).toHaveBeenCalledWith(
145
- TEST_IDS.MESSAGE_ID,
146
- expect.objectContaining({}),
147
- );
148
- });
149
- });
150
-
151
- describe('delAndRegenerateMessage', () => {
152
- it('should delete message then regenerate', async () => {
153
- const { result } = renderHook(() => useChatStore());
154
-
155
- act(() => {
156
- setupStoreWithMessages([
157
- createMockMessage({
158
- id: TEST_IDS.MESSAGE_ID,
159
- tools: [{ id: 'tool1' }] as any,
160
- }),
161
- ]);
162
- });
163
-
164
- const deleteMessageSpy = vi.spyOn(result.current, 'deleteMessage');
165
- const resendMessageSpy = vi.spyOn(result.current, 'internal_resendMessage');
166
-
167
- await act(async () => {
168
- await result.current.delAndRegenerateMessage(TEST_IDS.MESSAGE_ID);
169
- });
170
-
171
- expect(deleteMessageSpy).toHaveBeenCalledWith(TEST_IDS.MESSAGE_ID);
172
- expect(resendMessageSpy).toHaveBeenCalled();
173
- });
174
- });
175
-
176
- describe('stopGenerateMessage', () => {
177
- it('should abort generation and clear loading state when controller exists', () => {
178
- const abortController = new AbortController();
179
-
180
- act(() => {
181
- useChatStore.setState({ chatLoadingIdsAbortController: abortController });
182
- });
183
-
184
- const { result } = renderHook(() => useChatStore());
185
- const toggleLoadingSpy = vi.spyOn(result.current, 'internal_toggleChatLoading');
186
-
187
- act(() => {
188
- result.current.stopGenerateMessage();
189
- });
190
-
191
- expect(abortController.signal.aborted).toBe(true);
192
- expect(toggleLoadingSpy).toHaveBeenCalledWith(false, undefined, expect.any(String));
193
- });
194
-
195
- it('should do nothing when abort controller is not set', () => {
196
- act(() => {
197
- useChatStore.setState({ chatLoadingIdsAbortController: undefined });
198
- });
199
-
200
- const { result } = renderHook(() => useChatStore());
201
- const toggleLoadingSpy = vi.spyOn(result.current, 'internal_toggleChatLoading');
202
-
203
- act(() => {
204
- result.current.stopGenerateMessage();
205
- });
206
-
207
- expect(toggleLoadingSpy).not.toHaveBeenCalled();
208
- });
209
- });
210
-
211
- describe('internal_coreProcessMessage', () => {
212
- it('should process user message and generate AI response', async () => {
213
- const mockMessages = [{ id: 'msg-1', content: 'test' }] as any;
214
-
215
- act(() => {
216
- useChatStore.setState({ internal_coreProcessMessage: realCoreProcessMessage });
217
- });
218
-
219
- const { result } = renderHook(() => useChatStore());
220
- const userMessage = createMockMessage({
221
- id: TEST_IDS.USER_MESSAGE_ID,
222
- role: 'user',
223
- content: TEST_CONTENT.USER_MESSAGE,
224
- });
225
-
226
- // ✅ Spy the direct dependency instead of chatService
227
- const fetchAIChatSpy = vi
228
- .spyOn(result.current, 'internal_fetchAIChatMessage')
229
- .mockResolvedValue({ isFunctionCall: false, content: 'AI response' });
230
-
231
- const createMessageSpy = vi
232
- .spyOn(messageService, 'createMessage')
233
- .mockResolvedValue({ id: TEST_IDS.ASSISTANT_MESSAGE_ID, messages: mockMessages });
234
-
235
- const replaceMessagesSpy = vi.spyOn(result.current, 'replaceMessages');
236
-
237
- await act(async () => {
238
- await result.current.internal_coreProcessMessage([userMessage], userMessage.id);
239
- });
240
-
241
- expect(createMessageSpy).toHaveBeenCalledWith(
242
- expect.objectContaining({
243
- role: 'assistant',
244
- content: LOADING_FLAT,
245
- parentId: TEST_IDS.USER_MESSAGE_ID,
246
- sessionId: TEST_IDS.SESSION_ID,
247
- topicId: TEST_IDS.TOPIC_ID,
248
- }),
249
- );
250
-
251
- expect(fetchAIChatSpy).toHaveBeenCalled();
252
- expect(replaceMessagesSpy).toHaveBeenCalledWith(mockMessages);
253
- });
254
-
255
- it('should handle RAG flow when ragQuery is provided', async () => {
256
- act(() => {
257
- useChatStore.setState({ internal_coreProcessMessage: realCoreProcessMessage });
258
- });
259
-
260
- const { result } = renderHook(() => useChatStore());
261
- const userMessage = createMockMessage({
262
- id: TEST_IDS.USER_MESSAGE_ID,
263
- role: 'user',
264
- content: TEST_CONTENT.RAG_QUERY,
265
- });
266
-
267
- const retrieveChunksSpy = vi
268
- .spyOn(result.current, 'internal_retrieveChunks')
269
- .mockResolvedValue({
270
- chunks: [{ id: 'chunk-1', similarity: 0.9, text: 'chunk text' }] as any,
271
- queryId: 'query-1',
272
- rewriteQuery: 'rewritten query',
273
- });
274
-
275
- vi.spyOn(messageService, 'createMessage').mockResolvedValue({
276
- id: TEST_IDS.ASSISTANT_MESSAGE_ID,
277
- messages: [],
278
- });
279
-
280
- await act(async () => {
281
- await result.current.internal_coreProcessMessage([userMessage], userMessage.id, {
282
- ragQuery: TEST_CONTENT.RAG_QUERY,
283
- });
284
- });
285
-
286
- expect(retrieveChunksSpy).toHaveBeenCalledWith(
287
- TEST_IDS.USER_MESSAGE_ID,
288
- TEST_CONTENT.RAG_QUERY,
289
- [],
290
- );
291
- });
292
-
293
- it('should not process when createMessage fails', async () => {
294
- act(() => {
295
- useChatStore.setState({ internal_coreProcessMessage: realCoreProcessMessage });
296
- });
297
-
298
- const { result } = renderHook(() => useChatStore());
299
- const userMessage = createMockMessage({
300
- id: TEST_IDS.USER_MESSAGE_ID,
301
- role: 'user',
302
- });
303
-
304
- // ✅ Spy the direct dependency instead of chatService
305
- const fetchAIChatSpy = vi
306
- .spyOn(result.current, 'internal_fetchAIChatMessage')
307
- .mockResolvedValue({ isFunctionCall: false, content: '' });
308
-
309
- vi.spyOn(messageService, 'createMessage').mockResolvedValue(undefined as any);
310
-
311
- await act(async () => {
312
- await result.current.internal_coreProcessMessage([userMessage], userMessage.id);
313
- });
314
-
315
- expect(fetchAIChatSpy).not.toHaveBeenCalled();
316
- });
317
- });
318
-
319
- describe('internal_fetchAIChatMessage', () => {
320
- it('should fetch and return AI chat response', async () => {
321
- const { result } = renderHook(() => useChatStore());
322
- const messages = [createMockMessage({ role: 'user' })];
323
-
324
- // ✅ Mock chatService instead of global fetch
325
- const streamSpy = vi
326
- .spyOn(chatService, 'createAssistantMessageStream')
327
- .mockImplementation(async ({ onMessageHandle, onFinish }) => {
328
- // Simulate text chunks streaming
329
- await onMessageHandle?.({ type: 'text', text: TEST_CONTENT.AI_RESPONSE } as any);
330
- await onFinish?.(TEST_CONTENT.AI_RESPONSE, {});
331
- });
332
-
333
- await act(async () => {
334
- const response = await result.current.internal_fetchAIChatMessage({
335
- messages,
336
- messageId: TEST_IDS.ASSISTANT_MESSAGE_ID,
337
- model: 'gpt-4o-mini',
338
- provider: 'openai',
339
- });
340
- expect(response.isFunctionCall).toEqual(false);
341
- expect(response.content).toEqual(TEST_CONTENT.AI_RESPONSE);
342
- });
343
-
344
- streamSpy.mockRestore();
345
- });
346
-
347
- it('should handle streaming errors gracefully', async () => {
348
- const { result } = renderHook(() => useChatStore());
349
- const messages = [createMockMessage({ role: 'user' })];
350
-
351
- // ✅ Mock chatService to simulate error
352
- const streamSpy = vi
353
- .spyOn(chatService, 'createAssistantMessageStream')
354
- .mockImplementation(async ({ onErrorHandle }) => {
355
- await onErrorHandle?.({ type: 'InvalidProviderAPIKey', message: 'Network error' } as any);
356
- });
357
-
358
- const updateMessageErrorSpy = vi.spyOn(messageService, 'updateMessageError');
359
-
360
- await act(async () => {
361
- await result.current.internal_fetchAIChatMessage({
362
- model: 'gpt-4o-mini',
363
- provider: 'openai',
364
- messages,
365
- messageId: TEST_IDS.ASSISTANT_MESSAGE_ID,
366
- });
367
- });
368
-
369
- expect(updateMessageErrorSpy).toHaveBeenCalledWith(
370
- TEST_IDS.ASSISTANT_MESSAGE_ID,
371
- expect.objectContaining({ type: 'InvalidProviderAPIKey' }),
372
- );
373
-
374
- streamSpy.mockRestore();
375
- });
376
-
377
- it('should handle tool call chunks during streaming', async () => {
378
- const { result } = renderHook(() => useChatStore());
379
- const messages = [createMockMessage({ role: 'user' })];
380
-
381
- const streamSpy = vi
382
- .spyOn(chatService, 'createAssistantMessageStream')
383
- .mockImplementation(async ({ onMessageHandle, onFinish }) => {
384
- await onMessageHandle?.({
385
- type: 'tool_calls',
386
- isAnimationActives: [true],
387
- tool_calls: [
388
- { id: 'tool-1', type: 'function', function: { name: 'test', arguments: '{}' } },
389
- ],
390
- } as any);
391
- await onFinish?.(TEST_CONTENT.AI_RESPONSE, {
392
- toolCalls: [
393
- { id: 'tool-1', type: 'function', function: { name: 'test', arguments: '{}' } },
394
- ],
395
- } as any);
396
- });
397
-
398
- await act(async () => {
399
- const response = await result.current.internal_fetchAIChatMessage({
400
- messages,
401
- messageId: TEST_IDS.ASSISTANT_MESSAGE_ID,
402
- model: 'gpt-4o-mini',
403
- provider: 'openai',
404
- });
405
- expect(response.isFunctionCall).toEqual(true);
406
- });
407
-
408
- streamSpy.mockRestore();
409
- });
410
-
411
- it('should handle text chunks during streaming', async () => {
412
- const { result } = renderHook(() => useChatStore());
413
- const messages = [createMockMessage({ role: 'user' })];
414
- const dispatchSpy = vi.spyOn(result.current, 'internal_dispatchMessage');
415
-
416
- const streamSpy = vi
417
- .spyOn(chatService, 'createAssistantMessageStream')
418
- .mockImplementation(async ({ onMessageHandle, onFinish }) => {
419
- await onMessageHandle?.({ type: 'text', text: 'Hello' } as any);
420
- await onMessageHandle?.({ type: 'text', text: ' World' } as any);
421
- await onFinish?.('Hello World', {} as any);
422
- });
423
-
424
- await act(async () => {
425
- await result.current.internal_fetchAIChatMessage({
426
- messages,
427
- messageId: TEST_IDS.ASSISTANT_MESSAGE_ID,
428
- model: 'gpt-4o-mini',
429
- provider: 'openai',
430
- });
431
- });
432
-
433
- expect(dispatchSpy).toHaveBeenCalledWith(
434
- expect.objectContaining({
435
- id: TEST_IDS.ASSISTANT_MESSAGE_ID,
436
- type: 'updateMessage',
437
- value: expect.objectContaining({ content: 'Hello' }),
438
- }),
439
- );
440
-
441
- streamSpy.mockRestore();
442
- });
443
-
444
- it('should handle reasoning chunks during streaming', async () => {
445
- const { result } = renderHook(() => useChatStore());
446
- const messages = [createMockMessage({ role: 'user' })];
447
- const dispatchSpy = vi.spyOn(result.current, 'internal_dispatchMessage');
448
-
449
- const streamSpy = vi
450
- .spyOn(chatService, 'createAssistantMessageStream')
451
- .mockImplementation(async ({ onMessageHandle, onFinish }) => {
452
- await onMessageHandle?.({ type: 'reasoning', text: 'Thinking...' } as any);
453
- await onMessageHandle?.({ type: 'text', text: 'Answer' } as any);
454
- await onFinish?.('Answer', {} as any);
455
- });
456
-
457
- await act(async () => {
458
- await result.current.internal_fetchAIChatMessage({
459
- messages,
460
- messageId: TEST_IDS.ASSISTANT_MESSAGE_ID,
461
- model: 'gpt-4o-mini',
462
- provider: 'openai',
463
- });
464
- });
465
-
466
- expect(dispatchSpy).toHaveBeenCalledWith(
467
- expect.objectContaining({
468
- id: TEST_IDS.ASSISTANT_MESSAGE_ID,
469
- type: 'updateMessage',
470
- value: expect.objectContaining({ reasoning: { content: 'Thinking...' } }),
471
- }),
472
- );
473
-
474
- streamSpy.mockRestore();
475
- });
476
-
477
- it('should skip grounding when citations are empty', async () => {
478
- const { result } = renderHook(() => useChatStore());
479
- const messages = [createMockMessage({ role: 'user' })];
480
- const dispatchSpy = vi.spyOn(result.current, 'internal_dispatchMessage');
481
-
482
- const streamSpy = vi
483
- .spyOn(chatService, 'createAssistantMessageStream')
484
- .mockImplementation(async ({ onMessageHandle, onFinish }) => {
485
- await onMessageHandle?.({
486
- type: 'grounding',
487
- grounding: { citations: [], searchQueries: [] },
488
- } as any);
489
- await onFinish?.('Answer', {} as any);
490
- });
491
-
492
- await act(async () => {
493
- await result.current.internal_fetchAIChatMessage({
494
- messages,
495
- messageId: TEST_IDS.ASSISTANT_MESSAGE_ID,
496
- model: 'gpt-4o-mini',
497
- provider: 'openai',
498
- });
499
- });
500
-
501
- // Should not dispatch when citations are empty
502
- const groundingCalls = dispatchSpy.mock.calls.filter((call) => {
503
- const dispatch = call[0];
504
- return dispatch?.type === 'updateMessage' && 'value' in dispatch && dispatch.value?.search;
505
- });
506
- expect(groundingCalls).toHaveLength(0);
507
-
508
- streamSpy.mockRestore();
509
- });
510
-
511
- it('should handle grounding chunks during streaming', async () => {
512
- const { result } = renderHook(() => useChatStore());
513
- const messages = [createMockMessage({ role: 'user' })];
514
- const dispatchSpy = vi.spyOn(result.current, 'internal_dispatchMessage');
515
-
516
- const streamSpy = vi
517
- .spyOn(chatService, 'createAssistantMessageStream')
518
- .mockImplementation(async ({ onMessageHandle, onFinish }) => {
519
- await onMessageHandle?.({
520
- type: 'grounding',
521
- grounding: {
522
- citations: [{ url: 'https://example.com', title: 'Example' }],
523
- searchQueries: ['test query'],
524
- },
525
- } as any);
526
- await onFinish?.('Answer', {} as any);
527
- });
528
-
529
- await act(async () => {
530
- await result.current.internal_fetchAIChatMessage({
531
- messages,
532
- messageId: TEST_IDS.ASSISTANT_MESSAGE_ID,
533
- model: 'gpt-4o-mini',
534
- provider: 'openai',
535
- });
536
- });
537
-
538
- expect(dispatchSpy).toHaveBeenCalledWith(
539
- expect.objectContaining({
540
- id: TEST_IDS.ASSISTANT_MESSAGE_ID,
541
- type: 'updateMessage',
542
- value: expect.objectContaining({
543
- search: expect.objectContaining({
544
- citations: expect.any(Array),
545
- }),
546
- }),
547
- }),
548
- );
549
-
550
- streamSpy.mockRestore();
551
- });
552
-
553
- it('should handle base64 image chunks during streaming', async () => {
554
- const { result } = renderHook(() => useChatStore());
555
- const messages = [createMockMessage({ role: 'user' })];
556
- const dispatchSpy = vi.spyOn(result.current, 'internal_dispatchMessage');
557
-
558
- const streamSpy = vi
559
- .spyOn(chatService, 'createAssistantMessageStream')
560
- .mockImplementation(async ({ onMessageHandle, onFinish }) => {
561
- await onMessageHandle?.({
562
- type: 'base64_image',
563
- image: { id: 'img-1', data: 'base64data' },
564
- images: [{ id: 'img-1', data: 'base64data' }],
565
- } as any);
566
- await onFinish?.('Answer', {} as any);
567
- });
568
-
569
- await act(async () => {
570
- await result.current.internal_fetchAIChatMessage({
571
- messages,
572
- messageId: TEST_IDS.ASSISTANT_MESSAGE_ID,
573
- model: 'gpt-4o-mini',
574
- provider: 'openai',
575
- });
576
- });
577
-
578
- expect(dispatchSpy).toHaveBeenCalledWith(
579
- expect.objectContaining({
580
- id: TEST_IDS.ASSISTANT_MESSAGE_ID,
581
- type: 'updateMessage',
582
- value: expect.objectContaining({
583
- imageList: expect.any(Array),
584
- }),
585
- }),
586
- );
587
-
588
- streamSpy.mockRestore();
589
- });
590
-
591
- it('should handle empty tool call arguments', async () => {
592
- const { result } = renderHook(() => useChatStore());
593
- const messages = [createMockMessage({ role: 'user' })];
594
-
595
- const streamSpy = vi
596
- .spyOn(chatService, 'createAssistantMessageStream')
597
- .mockImplementation(async ({ onFinish }) => {
598
- await onFinish?.(TEST_CONTENT.AI_RESPONSE, {
599
- toolCalls: [
600
- { id: 'tool-1', type: 'function', function: { name: 'test', arguments: '' } },
601
- ],
602
- } as any);
603
- });
604
-
605
- await act(async () => {
606
- const response = await result.current.internal_fetchAIChatMessage({
607
- messages,
608
- messageId: TEST_IDS.ASSISTANT_MESSAGE_ID,
609
- model: 'gpt-4o-mini',
610
- provider: 'openai',
611
- });
612
- expect(response.isFunctionCall).toEqual(true);
613
- });
614
-
615
- streamSpy.mockRestore();
616
- });
617
-
618
- it('should update message with traceId when provided in onFinish', async () => {
619
- const { result } = renderHook(() => useChatStore());
620
- const messages = [createMockMessage({ role: 'user' })];
621
- const traceId = 'test-trace-123';
622
-
623
- const updateMessageSpy = vi.spyOn(messageService, 'updateMessage');
624
- const streamSpy = vi
625
- .spyOn(chatService, 'createAssistantMessageStream')
626
- .mockImplementation(async ({ onFinish }) => {
627
- await onFinish?.(TEST_CONTENT.AI_RESPONSE, { traceId } as any);
628
- });
629
-
630
- await act(async () => {
631
- await result.current.internal_fetchAIChatMessage({
632
- messages,
633
- messageId: TEST_IDS.ASSISTANT_MESSAGE_ID,
634
- model: 'gpt-4o-mini',
635
- provider: 'openai',
636
- });
637
- });
638
-
639
- expect(updateMessageSpy).toHaveBeenCalledWith(
640
- TEST_IDS.ASSISTANT_MESSAGE_ID,
641
- expect.objectContaining({ traceId }),
642
- );
643
-
644
- streamSpy.mockRestore();
645
- });
646
- });
647
-
648
- describe('internal_resendMessage', () => {
649
- it('should not resend when message does not exist', async () => {
650
- const { result } = renderHook(() => useChatStore());
651
- const coreProcessSpy = vi.fn();
652
-
653
- act(() => {
654
- setupStoreWithMessages([]);
655
- useChatStore.setState({ internal_coreProcessMessage: coreProcessSpy });
656
- });
657
-
658
- await act(async () => {
659
- await result.current.internal_resendMessage('non-existent-id');
660
- });
661
-
662
- expect(coreProcessSpy).not.toHaveBeenCalled();
663
- expect(result.current.refreshMessages).not.toHaveBeenCalled();
664
- });
665
-
666
- describe('context generation', () => {
667
- it('should generate correct context for user role message', async () => {
668
- const { result } = renderHook(() => useChatStore());
669
- const messages = [
670
- createMockMessage({ id: 'msg-1', role: 'system' }),
671
- createMockMessage({ id: TEST_IDS.MESSAGE_ID, role: 'user', meta: { avatar: '😀' } }),
672
- createMockMessage({ id: 'msg-3', role: 'assistant' }),
673
- ];
674
-
675
- act(() => {
676
- useChatStore.setState({
677
- messagesMap: {
678
- [chatSelectors.currentChatKey(useChatStore.getState() as any)]: messages,
679
- },
680
- });
681
- });
682
-
683
- await act(async () => {
684
- await result.current.internal_resendMessage(TEST_IDS.MESSAGE_ID);
685
- });
686
-
687
- expect(result.current.internal_coreProcessMessage).toHaveBeenCalledWith(
688
- expect.arrayContaining([
689
- expect.objectContaining({ id: 'msg-1' }),
690
- expect.objectContaining({ id: TEST_IDS.MESSAGE_ID }),
691
- ]),
692
- TEST_IDS.MESSAGE_ID,
693
- expect.objectContaining({ traceId: undefined }),
694
- );
695
- });
696
-
697
- it('should generate correct context for assistant role message', async () => {
698
- const { result } = renderHook(() => useChatStore());
699
- const parentId = 'msg-2';
700
- const messages = [
701
- createMockMessage({ id: 'msg-1', role: 'system' }),
702
- createMockMessage({ id: parentId, role: 'user', meta: { avatar: '😀' } }),
703
- createMockMessage({ id: TEST_IDS.MESSAGE_ID, role: 'assistant', parentId }),
704
- ];
705
-
706
- act(() => {
707
- useChatStore.setState({
708
- messagesMap: {
709
- [chatSelectors.currentChatKey(useChatStore.getState() as any)]: messages,
710
- },
711
- });
712
- });
713
-
714
- await act(async () => {
715
- await result.current.internal_resendMessage(TEST_IDS.MESSAGE_ID);
716
- });
717
-
718
- expect(result.current.internal_coreProcessMessage).toHaveBeenCalledWith(
719
- expect.arrayContaining([
720
- expect.objectContaining({ id: 'msg-1' }),
721
- expect.objectContaining({ id: parentId }),
722
- ]),
723
- parentId,
724
- expect.objectContaining({ traceId: undefined }),
725
- );
726
- });
727
-
728
- it('should not process when context is empty', async () => {
729
- const { result } = renderHook(() => useChatStore());
730
-
731
- act(() => {
732
- useChatStore.setState({
733
- messagesMap: {
734
- [chatSelectors.currentChatKey(useChatStore.getState() as any)]: [],
735
- },
736
- });
737
- });
738
-
739
- await act(async () => {
740
- await result.current.internal_resendMessage(TEST_IDS.MESSAGE_ID);
741
- });
742
-
743
- expect(result.current.internal_coreProcessMessage).not.toHaveBeenCalled();
744
- });
745
-
746
- it('should generate correct context for tool role message', async () => {
747
- const { result } = renderHook(() => useChatStore());
748
- const messages = [
749
- createMockMessage({ id: 'msg-1', role: 'user' }),
750
- createMockMessage({ id: 'msg-2', role: 'assistant' }),
751
- createMockMessage({ id: TEST_IDS.MESSAGE_ID, role: 'tool' }),
752
- ];
753
-
754
- act(() => {
755
- useChatStore.setState({
756
- messagesMap: {
757
- [chatSelectors.currentChatKey(useChatStore.getState() as any)]: messages,
758
- },
759
- });
760
- });
761
-
762
- await act(async () => {
763
- await result.current.internal_resendMessage(TEST_IDS.MESSAGE_ID);
764
- });
765
-
766
- // For tool role, it processes all messages up to tool but uses last user message as parentId
767
- expect(result.current.internal_coreProcessMessage).toHaveBeenCalledWith(
768
- expect.any(Array),
769
- 'msg-1', // parentId is the last user message
770
- expect.objectContaining({ traceId: undefined }),
771
- );
772
- });
773
- });
774
- });
775
-
776
- describe('internal_toggleChatLoading', () => {
777
- it('should enable loading state with new abort controller', () => {
778
- const { result } = renderHook(() => useChatStore());
779
-
780
- act(() => {
781
- result.current.internal_toggleChatLoading(true, TEST_IDS.MESSAGE_ID, 'test-action');
782
- });
783
-
784
- const state = useChatStore.getState();
785
- expect(state.chatLoadingIdsAbortController).toBeInstanceOf(AbortController);
786
- expect(state.chatLoadingIds).toEqual([TEST_IDS.MESSAGE_ID]);
787
- });
788
-
789
- it('should disable loading state and clear abort controller', () => {
790
- const { result } = renderHook(() => useChatStore());
791
-
792
- act(() => {
793
- result.current.internal_toggleChatLoading(true, TEST_IDS.MESSAGE_ID, 'start');
794
- result.current.internal_toggleChatLoading(false, undefined, 'stop');
795
- });
796
-
797
- const state = useChatStore.getState();
798
- expect(state.chatLoadingIdsAbortController).toBeUndefined();
799
- expect(state.chatLoadingIds).toEqual([]);
800
- });
801
-
802
- it('should manage beforeunload event listener', () => {
803
- const { result } = renderHook(() => useChatStore());
804
- const addListenerSpy = vi.spyOn(window, 'addEventListener');
805
- const removeListenerSpy = vi.spyOn(window, 'removeEventListener');
806
-
807
- act(() => {
808
- result.current.internal_toggleChatLoading(true, TEST_IDS.MESSAGE_ID, 'start');
809
- });
810
-
811
- expect(addListenerSpy).toHaveBeenCalledWith('beforeunload', expect.any(Function));
812
-
813
- act(() => {
814
- result.current.internal_toggleChatLoading(false, undefined, 'stop');
815
- });
816
-
817
- expect(removeListenerSpy).toHaveBeenCalledWith('beforeunload', expect.any(Function));
818
- });
819
-
820
- it('should reuse existing abort controller', () => {
821
- const existingController = new AbortController();
822
-
823
- act(() => {
824
- useChatStore.setState({ chatLoadingIdsAbortController: existingController });
825
- });
826
-
827
- const { result } = renderHook(() => useChatStore());
828
-
829
- act(() => {
830
- result.current.internal_toggleChatLoading(true, TEST_IDS.MESSAGE_ID, 'test');
831
- });
832
-
833
- const state = useChatStore.getState();
834
- expect(state.chatLoadingIdsAbortController).toStrictEqual(existingController);
835
- });
836
- });
837
-
838
- describe('internal_toggleToolCallingStreaming', () => {
839
- it('should track tool calling stream status', () => {
840
- const { result } = renderHook(() => useChatStore());
841
-
842
- act(() => {
843
- result.current.internal_toggleToolCallingStreaming(TEST_IDS.MESSAGE_ID, [true]);
844
- });
845
-
846
- expect(result.current.toolCallingStreamIds[TEST_IDS.MESSAGE_ID]).toEqual([true]);
847
- });
848
-
849
- it('should clear tool calling stream status', () => {
850
- const { result } = renderHook(() => useChatStore());
851
-
852
- act(() => {
853
- result.current.internal_toggleToolCallingStreaming(TEST_IDS.MESSAGE_ID, [true]);
854
- result.current.internal_toggleToolCallingStreaming(TEST_IDS.MESSAGE_ID, undefined);
855
- });
856
-
857
- expect(result.current.toolCallingStreamIds[TEST_IDS.MESSAGE_ID]).toBeUndefined();
858
- });
859
- });
860
-
861
- describe('internal_toggleSearchWorkflow', () => {
862
- it('should enable search workflow loading state', () => {
863
- const { result } = renderHook(() => useChatStore());
864
-
865
- act(() => {
866
- result.current.internal_toggleSearchWorkflow(true, TEST_IDS.MESSAGE_ID);
867
- });
868
-
869
- const state = useChatStore.getState();
870
- expect(state.searchWorkflowLoadingIds).toEqual([TEST_IDS.MESSAGE_ID]);
871
- });
872
-
873
- it('should disable search workflow loading state', () => {
874
- const { result } = renderHook(() => useChatStore());
875
-
876
- act(() => {
877
- result.current.internal_toggleSearchWorkflow(true, TEST_IDS.MESSAGE_ID);
878
- result.current.internal_toggleSearchWorkflow(false, TEST_IDS.MESSAGE_ID);
879
- });
880
-
881
- const state = useChatStore.getState();
882
- expect(state.searchWorkflowLoadingIds).toEqual([]);
883
- });
884
- });
885
-
886
- describe('internal_toggleChatReasoning', () => {
887
- it('should enable reasoning loading state', () => {
888
- const { result } = renderHook(() => useChatStore());
889
-
890
- act(() => {
891
- result.current.internal_toggleChatReasoning(true, TEST_IDS.MESSAGE_ID, 'test-action');
892
- });
893
-
894
- const state = useChatStore.getState();
895
- expect(state.reasoningLoadingIds).toEqual([TEST_IDS.MESSAGE_ID]);
896
- });
897
-
898
- it('should disable reasoning loading state', () => {
899
- const { result } = renderHook(() => useChatStore());
900
-
901
- act(() => {
902
- result.current.internal_toggleChatReasoning(true, TEST_IDS.MESSAGE_ID, 'start');
903
- result.current.internal_toggleChatReasoning(false, TEST_IDS.MESSAGE_ID, 'stop');
904
- });
905
-
906
- const state = useChatStore.getState();
907
- expect(state.reasoningLoadingIds).toEqual([]);
908
- });
909
- });
910
-
911
- describe('internal_toggleMessageInToolsCalling', () => {
912
- it('should enable tools calling state', () => {
913
- const { result } = renderHook(() => useChatStore());
914
-
915
- act(() => {
916
- result.current.internal_toggleMessageInToolsCalling(true, TEST_IDS.MESSAGE_ID);
917
- });
918
-
919
- const state = useChatStore.getState();
920
- expect(state.messageInToolsCallingIds).toEqual([TEST_IDS.MESSAGE_ID]);
921
- });
922
-
923
- it('should disable tools calling state', () => {
924
- const { result } = renderHook(() => useChatStore());
925
-
926
- act(() => {
927
- result.current.internal_toggleMessageInToolsCalling(true, TEST_IDS.MESSAGE_ID);
928
- result.current.internal_toggleMessageInToolsCalling(false, TEST_IDS.MESSAGE_ID);
929
- });
930
-
931
- const state = useChatStore.getState();
932
- expect(state.messageInToolsCallingIds).toEqual([]);
933
- });
934
- });
935
-
936
- describe('internal_resendMessage with custom params', () => {
937
- it('should use provided messages instead of store messages', async () => {
938
- const { result } = renderHook(() => useChatStore());
939
- const customMessages = [createMockMessage({ id: 'custom-msg', role: 'user' })];
940
-
941
- await act(async () => {
942
- await result.current.internal_resendMessage('custom-msg', { messages: customMessages });
943
- });
944
-
945
- expect(result.current.internal_coreProcessMessage).toHaveBeenCalledWith(
946
- expect.arrayContaining([expect.objectContaining({ id: 'custom-msg' })]),
947
- 'custom-msg',
948
- expect.anything(),
949
- );
950
- });
951
-
952
- it('should handle assistant message without parentId', async () => {
953
- const { result } = renderHook(() => useChatStore());
954
- const messages = [
955
- createMockMessage({ id: 'msg-1', role: 'user' }),
956
- createMockMessage({ id: TEST_IDS.MESSAGE_ID, role: 'assistant', parentId: undefined }),
957
- ];
958
-
959
- act(() => {
960
- useChatStore.setState({
961
- messagesMap: {
962
- [chatSelectors.currentChatKey(useChatStore.getState() as any)]: messages,
963
- },
964
- });
965
- });
966
-
967
- await act(async () => {
968
- await result.current.internal_resendMessage(TEST_IDS.MESSAGE_ID);
969
- });
970
-
971
- // Should handle the case where parentId is not found
972
- expect(result.current.internal_coreProcessMessage).toHaveBeenCalled();
973
- });
974
- });
975
- });