@aleph-alpha/chat-kit 0.9.0 → 1.1.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 (263) hide show
  1. package/dist/{CkInput.vue_vue_type_script_setup_true_lang-DmcLXAgB.js → CkInput.vue_vue_type_script_setup_true_lang-DpYyyCgU.js} +481 -79
  2. package/dist/chat-kit.css +9 -14
  3. package/dist/components/base/CkAgentMessage/CkAgentMessage.stories.d.ts +20 -0
  4. package/dist/components/base/CkAgentMessage/CkAgentMessage.stories.d.ts.map +1 -1
  5. package/dist/components/base/CkAgentMessage/CkAgentMessage.vue.d.ts.map +1 -1
  6. package/dist/components/base/CkAgentMessage/index.d.ts +1 -0
  7. package/dist/components/base/CkAgentMessage/index.d.ts.map +1 -1
  8. package/dist/components/base/CkAgentMessage/types.d.ts +12 -0
  9. package/dist/components/base/CkAgentMessage/types.d.ts.map +1 -0
  10. package/dist/components/base/CkConversationLayout/CkConversationLayout.stories.d.ts +13 -0
  11. package/dist/components/base/CkConversationLayout/CkConversationLayout.stories.d.ts.map +1 -1
  12. package/dist/components/base/CkConversationLayout/CkConversationLayout.vue.d.ts.map +1 -1
  13. package/dist/components/base/CkConversationLayout/types.d.ts +9 -2
  14. package/dist/components/base/CkConversationLayout/types.d.ts.map +1 -1
  15. package/dist/components/base/CkCopyMessageActionButton/CkCopyMessageActionButton.stories.d.ts +8 -0
  16. package/dist/components/base/CkCopyMessageActionButton/CkCopyMessageActionButton.stories.d.ts.map +1 -1
  17. package/dist/components/base/CkCopyMessageActionButton/CkCopyMessageActionButton.vue.d.ts +3 -15
  18. package/dist/components/base/CkCopyMessageActionButton/CkCopyMessageActionButton.vue.d.ts.map +1 -1
  19. package/dist/components/base/CkCopyMessageActionButton/index.d.ts +1 -0
  20. package/dist/components/base/CkCopyMessageActionButton/index.d.ts.map +1 -1
  21. package/dist/components/base/CkCopyMessageActionButton/types.d.ts +45 -0
  22. package/dist/components/base/CkCopyMessageActionButton/types.d.ts.map +1 -0
  23. package/dist/components/base/CkHistoryItem/CkHistoryItem.stories.d.ts +31 -0
  24. package/dist/components/base/CkHistoryItem/CkHistoryItem.stories.d.ts.map +1 -0
  25. package/dist/components/base/CkHistoryItem/CkHistoryItem.vue.d.ts +31 -0
  26. package/dist/components/base/CkHistoryItem/CkHistoryItem.vue.d.ts.map +1 -0
  27. package/dist/components/base/CkHistoryItem/activeIndicator.d.ts +15 -0
  28. package/dist/components/base/CkHistoryItem/activeIndicator.d.ts.map +1 -0
  29. package/dist/components/base/CkHistoryItem/index.d.ts +3 -0
  30. package/dist/components/base/CkHistoryItem/index.d.ts.map +1 -0
  31. package/dist/components/base/CkHistoryItem/types.d.ts +96 -0
  32. package/dist/components/base/CkHistoryItem/types.d.ts.map +1 -0
  33. package/dist/components/base/CkHistoryWrapper/CkHistoryWrapper.stories.d.ts +16 -0
  34. package/dist/components/base/CkHistoryWrapper/CkHistoryWrapper.stories.d.ts.map +1 -0
  35. package/dist/components/base/CkHistoryWrapper/CkHistoryWrapper.vue.d.ts +17 -0
  36. package/dist/components/base/CkHistoryWrapper/CkHistoryWrapper.vue.d.ts.map +1 -0
  37. package/dist/components/base/CkHistoryWrapper/index.d.ts +3 -0
  38. package/dist/components/base/CkHistoryWrapper/index.d.ts.map +1 -0
  39. package/dist/components/base/CkHistoryWrapper/types.d.ts +17 -0
  40. package/dist/components/base/CkHistoryWrapper/types.d.ts.map +1 -0
  41. package/dist/components/base/CkMarkdownRenderer/CkMarkdownRenderer.stories.d.ts +28 -0
  42. package/dist/components/base/CkMarkdownRenderer/CkMarkdownRenderer.stories.d.ts.map +1 -1
  43. package/dist/components/base/CkMarkdownRenderer/CkMarkdownRenderer.vue.d.ts.map +1 -1
  44. package/dist/components/base/CkMarkdownRenderer/index.d.ts +1 -1
  45. package/dist/components/base/CkMarkdownRenderer/index.d.ts.map +1 -1
  46. package/dist/components/base/CkMarkdownRenderer/types.d.ts +16 -0
  47. package/dist/components/base/CkMarkdownRenderer/types.d.ts.map +1 -1
  48. package/dist/components/base/CkMessageActionButton/CkMessageActionButton.stories.d.ts +4 -0
  49. package/dist/components/base/CkMessageActionButton/CkMessageActionButton.stories.d.ts.map +1 -1
  50. package/dist/components/base/CkMessageActionButton/CkMessageActionButton.vue.d.ts +2 -5
  51. package/dist/components/base/CkMessageActionButton/CkMessageActionButton.vue.d.ts.map +1 -1
  52. package/dist/components/base/CkMessageActionButton/index.d.ts +1 -0
  53. package/dist/components/base/CkMessageActionButton/index.d.ts.map +1 -1
  54. package/dist/components/base/CkMessageActionButton/types.d.ts +22 -0
  55. package/dist/components/base/CkMessageActionButton/types.d.ts.map +1 -0
  56. package/dist/components/base/CkPromptActionButton/CkPromptActionButton.stories.d.ts +12 -0
  57. package/dist/components/base/CkPromptActionButton/CkPromptActionButton.stories.d.ts.map +1 -1
  58. package/dist/components/base/CkPromptActionButton/CkPromptActionButton.vue.d.ts +2 -7
  59. package/dist/components/base/CkPromptActionButton/CkPromptActionButton.vue.d.ts.map +1 -1
  60. package/dist/components/base/CkPromptActionButton/index.d.ts +1 -0
  61. package/dist/components/base/CkPromptActionButton/index.d.ts.map +1 -1
  62. package/dist/components/base/CkPromptActionButton/types.d.ts +33 -0
  63. package/dist/components/base/CkPromptActionButton/types.d.ts.map +1 -0
  64. package/dist/components/base/CkPromptInput/CkPromptInput.stories.d.ts +17 -0
  65. package/dist/components/base/CkPromptInput/CkPromptInput.stories.d.ts.map +1 -1
  66. package/dist/components/base/CkPromptInput/CkPromptInput.vue.d.ts +3 -9
  67. package/dist/components/base/CkPromptInput/CkPromptInput.vue.d.ts.map +1 -1
  68. package/dist/components/base/CkPromptInput/index.d.ts +1 -0
  69. package/dist/components/base/CkPromptInput/index.d.ts.map +1 -1
  70. package/dist/components/base/CkPromptInput/types.d.ts +39 -0
  71. package/dist/components/base/CkPromptInput/types.d.ts.map +1 -0
  72. package/dist/components/base/CkPromptInputDisclaimer/CkPromptInputDisclaimer.stories.d.ts +12 -0
  73. package/dist/components/base/CkPromptInputDisclaimer/CkPromptInputDisclaimer.stories.d.ts.map +1 -1
  74. package/dist/components/base/CkPromptInputDisclaimer/CkPromptInputDisclaimer.vue.d.ts +2 -4
  75. package/dist/components/base/CkPromptInputDisclaimer/CkPromptInputDisclaimer.vue.d.ts.map +1 -1
  76. package/dist/components/base/CkPromptInputDisclaimer/index.d.ts +1 -0
  77. package/dist/components/base/CkPromptInputDisclaimer/index.d.ts.map +1 -1
  78. package/dist/components/base/CkPromptInputDisclaimer/types.d.ts +18 -0
  79. package/dist/components/base/CkPromptInputDisclaimer/types.d.ts.map +1 -0
  80. package/dist/components/base/CkPromptStopButton/CkPromptStopButton.stories.d.ts +8 -0
  81. package/dist/components/base/CkPromptStopButton/CkPromptStopButton.stories.d.ts.map +1 -1
  82. package/dist/components/base/CkPromptStopButton/CkPromptStopButton.vue.d.ts +3 -6
  83. package/dist/components/base/CkPromptStopButton/CkPromptStopButton.vue.d.ts.map +1 -1
  84. package/dist/components/base/CkPromptStopButton/index.d.ts +1 -0
  85. package/dist/components/base/CkPromptStopButton/index.d.ts.map +1 -1
  86. package/dist/components/base/CkPromptStopButton/types.d.ts +28 -0
  87. package/dist/components/base/CkPromptStopButton/types.d.ts.map +1 -0
  88. package/dist/components/base/CkUserMessage/CkUserMessage.stories.d.ts +15 -0
  89. package/dist/components/base/CkUserMessage/CkUserMessage.stories.d.ts.map +1 -1
  90. package/dist/components/base/CkUserMessage/CkUserMessage.vue.d.ts.map +1 -1
  91. package/dist/components/base/CkUserMessage/types.d.ts +9 -2
  92. package/dist/components/base/CkUserMessage/types.d.ts.map +1 -1
  93. package/dist/components/base/icons/CkStopIcon.vue.d.ts.map +1 -1
  94. package/dist/components/base/index.d.ts +5 -1
  95. package/dist/components/base/index.d.ts.map +1 -1
  96. package/dist/components/composed/CkConversation/CkConversation.stories.d.ts +55 -0
  97. package/dist/components/composed/CkConversation/CkConversation.stories.d.ts.map +1 -1
  98. package/dist/components/composed/CkConversation/CkConversation.vue.d.ts.map +1 -1
  99. package/dist/components/composed/CkConversation/types.d.ts +25 -6
  100. package/dist/components/composed/CkConversation/types.d.ts.map +1 -1
  101. package/dist/components/composed/CkHistory/CkHistory.stories.d.ts +33 -0
  102. package/dist/components/composed/CkHistory/CkHistory.stories.d.ts.map +1 -0
  103. package/dist/components/composed/CkHistory/CkHistory.vue.d.ts +50 -0
  104. package/dist/components/composed/CkHistory/CkHistory.vue.d.ts.map +1 -0
  105. package/dist/components/composed/CkHistory/index.d.ts +3 -0
  106. package/dist/components/composed/CkHistory/index.d.ts.map +1 -0
  107. package/dist/components/composed/CkHistory/types.d.ts +71 -0
  108. package/dist/components/composed/CkHistory/types.d.ts.map +1 -0
  109. package/dist/components/composed/CkInput/CkInput.stories.d.ts +31 -1
  110. package/dist/components/composed/CkInput/CkInput.stories.d.ts.map +1 -1
  111. package/dist/components/composed/CkInput/CkInput.vue.d.ts +3 -12
  112. package/dist/components/composed/CkInput/CkInput.vue.d.ts.map +1 -1
  113. package/dist/components/composed/CkInput/index.d.ts +1 -0
  114. package/dist/components/composed/CkInput/index.d.ts.map +1 -1
  115. package/dist/components/composed/CkInput/types.d.ts +60 -0
  116. package/dist/components/composed/CkInput/types.d.ts.map +1 -0
  117. package/dist/components/composed/index.d.ts +2 -0
  118. package/dist/components/composed/index.d.ts.map +1 -1
  119. package/dist/components/index.d.ts +4 -4
  120. package/dist/components/index.d.ts.map +1 -1
  121. package/dist/components/index.js +14 -11
  122. package/dist/composables/index.d.ts +2 -0
  123. package/dist/composables/index.d.ts.map +1 -1
  124. package/dist/composables/index.js +3 -3
  125. package/dist/composables/useConversations.d.ts +72 -0
  126. package/dist/composables/useConversations.d.ts.map +1 -0
  127. package/dist/generateId-BHf0W4Rq.js +6 -0
  128. package/dist/handlers/index.js +2 -2
  129. package/dist/handlers/useChatKitLabels.d.ts +8 -0
  130. package/dist/handlers/useChatKitLabels.d.ts.map +1 -1
  131. package/dist/handlers/useMarkdownParser.d.ts.map +1 -1
  132. package/dist/helpers/conversationTree.d.ts.map +1 -1
  133. package/dist/helpers/index.d.ts +2 -0
  134. package/dist/helpers/index.d.ts.map +1 -1
  135. package/dist/helpers/secondsToDate.d.ts +6 -0
  136. package/dist/helpers/secondsToDate.d.ts.map +1 -0
  137. package/dist/helpers/toError.d.ts +7 -0
  138. package/dist/helpers/toError.d.ts.map +1 -0
  139. package/dist/index.d.ts +6 -6
  140. package/dist/index.d.ts.map +1 -1
  141. package/dist/index.js +20 -16
  142. package/dist/services/ChatService.d.ts.map +1 -1
  143. package/dist/services/MockChatService.d.ts.map +1 -1
  144. package/dist/services/index.d.ts +1 -1
  145. package/dist/services/index.d.ts.map +1 -1
  146. package/dist/services/index.js +4 -1
  147. package/dist/services/types.d.ts +53 -0
  148. package/dist/services/types.d.ts.map +1 -1
  149. package/dist/types.d.ts +13 -0
  150. package/dist/types.d.ts.map +1 -1
  151. package/dist/{useChatKitLabels-DNwoZ1x6.js → useChatKitLabels-Dvi2FmjE.js} +8 -2
  152. package/dist/useConversations-Ca242D2P.js +195 -0
  153. package/dist/{useMessageHandler-D7OdrrcB.js → useMessageHandler-uLHNac39.js} +1 -3
  154. package/package.json +5 -4
  155. package/src/components/base/CkAgentMessage/CkAgentMessage.spec.ts +91 -0
  156. package/src/components/base/CkAgentMessage/CkAgentMessage.stories.ts +215 -0
  157. package/src/components/base/CkAgentMessage/CkAgentMessage.vue +25 -0
  158. package/src/components/base/CkAgentMessage/index.ts +2 -0
  159. package/src/components/base/CkAgentMessage/types.ts +11 -0
  160. package/src/components/base/CkConversationLayout/CkConversationLayout.spec.ts +129 -0
  161. package/src/components/base/CkConversationLayout/CkConversationLayout.stories.ts +251 -0
  162. package/src/components/base/CkConversationLayout/CkConversationLayout.vue +206 -0
  163. package/src/components/base/CkConversationLayout/index.ts +2 -0
  164. package/src/components/base/CkConversationLayout/types.ts +18 -0
  165. package/src/components/base/CkCopyMessageActionButton/CkCopyMessageActionButton.spec.ts +89 -0
  166. package/src/components/base/CkCopyMessageActionButton/CkCopyMessageActionButton.stories.ts +70 -0
  167. package/src/components/base/CkCopyMessageActionButton/CkCopyMessageActionButton.vue +110 -0
  168. package/src/components/base/CkCopyMessageActionButton/index.ts +2 -0
  169. package/src/components/base/CkCopyMessageActionButton/types.ts +43 -0
  170. package/src/components/base/CkHistoryItem/CkHistoryItem.spec.ts +42 -0
  171. package/src/components/base/CkHistoryItem/CkHistoryItem.stories.ts +230 -0
  172. package/src/components/base/CkHistoryItem/CkHistoryItem.vue +285 -0
  173. package/src/components/base/CkHistoryItem/activeIndicator.ts +15 -0
  174. package/src/components/base/CkHistoryItem/index.ts +2 -0
  175. package/src/components/base/CkHistoryItem/types.ts +111 -0
  176. package/src/components/base/CkHistoryWrapper/CkHistoryWrapper.spec.ts +26 -0
  177. package/src/components/base/CkHistoryWrapper/CkHistoryWrapper.stories.ts +149 -0
  178. package/src/components/base/CkHistoryWrapper/CkHistoryWrapper.vue +36 -0
  179. package/src/components/base/CkHistoryWrapper/index.ts +2 -0
  180. package/src/components/base/CkHistoryWrapper/types.ts +16 -0
  181. package/src/components/base/CkMarkdownRenderer/CkMarkdownRenderer.spec.ts +348 -0
  182. package/src/components/base/CkMarkdownRenderer/CkMarkdownRenderer.stories.ts +382 -0
  183. package/src/components/base/CkMarkdownRenderer/CkMarkdownRenderer.vue +89 -0
  184. package/src/components/base/CkMarkdownRenderer/defaultMarkdownClasses.ts +19 -0
  185. package/src/components/base/CkMarkdownRenderer/index.ts +5 -0
  186. package/src/components/base/CkMarkdownRenderer/types.ts +53 -0
  187. package/src/components/base/CkMessageActionButton/CkMessageActionButton.spec.ts +40 -0
  188. package/src/components/base/CkMessageActionButton/CkMessageActionButton.stories.ts +56 -0
  189. package/src/components/base/CkMessageActionButton/CkMessageActionButton.vue +23 -0
  190. package/src/components/base/CkMessageActionButton/index.ts +2 -0
  191. package/src/components/base/CkMessageActionButton/types.ts +21 -0
  192. package/src/components/base/CkPromptActionButton/CkPromptActionButton.spec.ts +52 -0
  193. package/src/components/base/CkPromptActionButton/CkPromptActionButton.stories.ts +126 -0
  194. package/src/components/base/CkPromptActionButton/CkPromptActionButton.vue +25 -0
  195. package/src/components/base/CkPromptActionButton/index.ts +2 -0
  196. package/src/components/base/CkPromptActionButton/types.ts +32 -0
  197. package/src/components/base/CkPromptInput/CkPromptInput.spec.ts +181 -0
  198. package/src/components/base/CkPromptInput/CkPromptInput.stories.ts +224 -0
  199. package/src/components/base/CkPromptInput/CkPromptInput.vue +104 -0
  200. package/src/components/base/CkPromptInput/index.ts +2 -0
  201. package/src/components/base/CkPromptInput/types.ts +39 -0
  202. package/src/components/base/CkPromptInputDisclaimer/CkPromptInputDisclaimer.spec.ts +40 -0
  203. package/src/components/base/CkPromptInputDisclaimer/CkPromptInputDisclaimer.stories.ts +103 -0
  204. package/src/components/base/CkPromptInputDisclaimer/CkPromptInputDisclaimer.vue +16 -0
  205. package/src/components/base/CkPromptInputDisclaimer/index.ts +2 -0
  206. package/src/components/base/CkPromptInputDisclaimer/types.ts +17 -0
  207. package/src/components/base/CkPromptStopButton/CkPromptStopButton.spec.ts +64 -0
  208. package/src/components/base/CkPromptStopButton/CkPromptStopButton.stories.ts +81 -0
  209. package/src/components/base/CkPromptStopButton/CkPromptStopButton.vue +27 -0
  210. package/src/components/base/CkPromptStopButton/index.ts +2 -0
  211. package/src/components/base/CkPromptStopButton/types.ts +28 -0
  212. package/src/components/base/CkUserMessage/CkUserMessage.spec.ts +20 -0
  213. package/src/components/base/CkUserMessage/CkUserMessage.stories.ts +112 -0
  214. package/src/components/base/CkUserMessage/CkUserMessage.vue +19 -0
  215. package/src/components/base/CkUserMessage/index.ts +2 -0
  216. package/src/components/base/CkUserMessage/types.ts +14 -0
  217. package/src/components/base/icons/CkSendIcon.vue +22 -0
  218. package/src/components/base/icons/CkStopIcon.vue +18 -0
  219. package/src/components/base/icons/index.ts +2 -0
  220. package/src/components/base/index.ts +21 -0
  221. package/src/components/composed/CkConversation/CkConversation.spec.ts +439 -0
  222. package/src/components/composed/CkConversation/CkConversation.stories.ts +642 -0
  223. package/src/components/composed/CkConversation/CkConversation.vue +146 -0
  224. package/src/components/composed/CkConversation/index.ts +2 -0
  225. package/src/components/composed/CkConversation/types.ts +37 -0
  226. package/src/components/composed/CkHistory/CkHistory.spec.ts +327 -0
  227. package/src/components/composed/CkHistory/CkHistory.stories.ts +373 -0
  228. package/src/components/composed/CkHistory/CkHistory.vue +126 -0
  229. package/src/components/composed/CkHistory/index.ts +2 -0
  230. package/src/components/composed/CkHistory/types.ts +72 -0
  231. package/src/components/composed/CkInput/CkInput.spec.ts +191 -0
  232. package/src/components/composed/CkInput/CkInput.stories.ts +438 -0
  233. package/src/components/composed/CkInput/CkInput.vue +104 -0
  234. package/src/components/composed/CkInput/index.ts +2 -0
  235. package/src/components/composed/CkInput/types.ts +60 -0
  236. package/src/components/composed/index.ts +5 -0
  237. package/src/components/index.ts +34 -0
  238. package/src/composables/index.ts +9 -0
  239. package/src/composables/types.ts +36 -0
  240. package/src/composables/useChat.ts +18 -0
  241. package/src/composables/useConversations.spec.ts +557 -0
  242. package/src/composables/useConversations.ts +321 -0
  243. package/src/handlers/index.ts +9 -0
  244. package/src/handlers/useChatKitLabels.spec.ts +290 -0
  245. package/src/handlers/useChatKitLabels.ts +96 -0
  246. package/src/handlers/useMarkdownParser.spec.ts +112 -0
  247. package/src/handlers/useMarkdownParser.ts +47 -0
  248. package/src/handlers/useMessageHandler.spec.ts +535 -0
  249. package/src/handlers/useMessageHandler.ts +138 -0
  250. package/src/helpers/conversationTree.spec.ts +423 -0
  251. package/src/helpers/conversationTree.ts +117 -0
  252. package/src/helpers/generateId.ts +5 -0
  253. package/src/helpers/index.ts +4 -0
  254. package/src/helpers/secondsToDate.ts +7 -0
  255. package/src/helpers/toError.ts +8 -0
  256. package/src/index.ts +79 -0
  257. package/src/services/ChatService.ts +60 -0
  258. package/src/services/MockChatService.ts +47 -0
  259. package/src/services/index.ts +12 -0
  260. package/src/services/types.ts +92 -0
  261. package/src/test-setup.ts +12 -0
  262. package/src/test-utils/createTextPlugin.ts +9 -0
  263. package/src/types.ts +47 -0
@@ -0,0 +1,138 @@
1
+ import {
2
+ generateId,
3
+ addNode as treeAddNode,
4
+ createEmptyTree,
5
+ getLinearPath,
6
+ navigate as treeNavigate,
7
+ updateNode,
8
+ updateNodeId,
9
+ } from '../helpers';
10
+ import type {
11
+ BaseMessage,
12
+ ConversationTree,
13
+ MessageRole,
14
+ MessageStatus,
15
+ TreeMessage,
16
+ } from '../types';
17
+ import { computed, reactive, readonly, ref, type Ref } from 'vue';
18
+
19
+ export const useMessageHandler = () => {
20
+ const tree = reactive<ConversationTree>(createEmptyTree());
21
+ const currentUserMessage = ref('');
22
+
23
+ const messages = computed<BaseMessage[]>(() => getLinearPath(tree));
24
+
25
+ const currentLeafId = computed<string | null>(() => {
26
+ const list = messages.value;
27
+ return list.length > 0 ? list[list.length - 1].id : null;
28
+ });
29
+
30
+ function setCurrentUserMessage(text: string) {
31
+ currentUserMessage.value = text;
32
+ }
33
+
34
+ function clearCurrentUserMessage() {
35
+ currentUserMessage.value = '';
36
+ }
37
+
38
+ function commitUserMessage(): BaseMessage {
39
+ const id = generateId();
40
+ const text = currentUserMessage.value.trim();
41
+ if (!text) throw new Error('No message');
42
+
43
+ const node: TreeMessage = {
44
+ id,
45
+ parentId: currentLeafId.value,
46
+ childrenIds: [],
47
+ activeChildIndex: 0,
48
+ role: 'user',
49
+ content: text,
50
+ status: 'completed',
51
+ createdAt: new Date(),
52
+ };
53
+
54
+ treeAddNode(tree, node);
55
+ clearCurrentUserMessage();
56
+
57
+ return {
58
+ id: node.id,
59
+ role: node.role,
60
+ content: node.content,
61
+ createdAt: node.createdAt,
62
+ status: node.status,
63
+ };
64
+ }
65
+
66
+ function updateMessage(message: {
67
+ id: string;
68
+ content: string;
69
+ role: MessageRole;
70
+ status: MessageStatus;
71
+ }) {
72
+ const existing = tree.nodes[message.id];
73
+
74
+ if (existing) {
75
+ updateNode(tree, message.id, {
76
+ content: message.content,
77
+ role: message.role,
78
+ status: message.status,
79
+ });
80
+ } else {
81
+ const node: TreeMessage = {
82
+ id: message.id,
83
+ parentId: currentLeafId.value,
84
+ childrenIds: [],
85
+ activeChildIndex: 0,
86
+ role: message.role,
87
+ content: message.content,
88
+ status: message.status,
89
+ createdAt: new Date(),
90
+ };
91
+ treeAddNode(tree, node);
92
+ }
93
+ }
94
+
95
+ function getMessageById(id: string): BaseMessage | undefined {
96
+ const node = tree.nodes[id];
97
+ if (!node) return undefined;
98
+ return {
99
+ id: node.id,
100
+ role: node.role,
101
+ content: node.content,
102
+ createdAt: node.createdAt,
103
+ status: node.status,
104
+ };
105
+ }
106
+
107
+ function updateMessageId(oldId: string, newId: string) {
108
+ updateNodeId(tree, oldId, newId);
109
+ }
110
+
111
+ function clearMessages() {
112
+ Object.assign(tree, createEmptyTree());
113
+ }
114
+
115
+ function setTree(incoming: ConversationTree) {
116
+ tree.nodes = incoming.nodes;
117
+ tree.rootId = incoming.rootId;
118
+ }
119
+
120
+ function navigate(targetId: string) {
121
+ treeNavigate(tree, targetId);
122
+ }
123
+
124
+ return {
125
+ messages: messages as Ref<BaseMessage[]>,
126
+ currentUserMessage: readonly(currentUserMessage),
127
+ currentLeafId,
128
+ updateMessage,
129
+ setCurrentUserMessage,
130
+ getMessageById,
131
+ clearCurrentUserMessage,
132
+ commitUserMessage,
133
+ updateMessageId,
134
+ clearMessages,
135
+ setTree,
136
+ navigate,
137
+ };
138
+ };
@@ -0,0 +1,423 @@
1
+ import type { ConversationTree, TreeMessage } from '../types';
2
+ import {
3
+ addNode,
4
+ createEmptyTree,
5
+ getLinearPath,
6
+ navigate,
7
+ updateNode,
8
+ updateNodeId,
9
+ } from './conversationTree';
10
+
11
+ function makeTreeMessage(
12
+ overrides: Partial<TreeMessage> & { id: string },
13
+ ): TreeMessage {
14
+ return {
15
+ parentId: null,
16
+ childrenIds: [],
17
+ activeChildIndex: 0,
18
+ role: 'user',
19
+ content: '',
20
+ status: 'completed',
21
+ createdAt: new Date('2025-01-01'),
22
+ ...overrides,
23
+ };
24
+ }
25
+
26
+ function makeTree(
27
+ nodes: Record<string, TreeMessage>,
28
+ rootId: string,
29
+ ): ConversationTree {
30
+ for (const node of Object.values(nodes)) {
31
+ node.childrenIds = [];
32
+ }
33
+ for (const node of Object.values(nodes)) {
34
+ if (node.parentId !== null) {
35
+ const parent = nodes[node.parentId];
36
+ if (parent) parent.childrenIds.push(node.id);
37
+ }
38
+ }
39
+ for (const node of Object.values(nodes)) {
40
+ node.activeChildIndex = 0;
41
+ }
42
+ return { nodes, rootId };
43
+ }
44
+
45
+ /**
46
+ * Builds a simple branching tree for tests:
47
+ *
48
+ * root(u1)
49
+ * ├─ a1 (assistant, child 0) [activeChildIndex=0 → u2]
50
+ * │ └─ u2 [activeChildIndex=0 → a2]
51
+ * │ ├─ a2 (child 0)
52
+ * │ └─ a3 (child 1, regeneration of a2)
53
+ * └─ a1b (assistant, child 1, regeneration of a1)
54
+ * └─ u3
55
+ *
56
+ * Active path: u1 → a1 → u2 → a2 (activeChildIndex=0 everywhere)
57
+ */
58
+ function makeBranchingTree(): ConversationTree {
59
+ return makeTree(
60
+ {
61
+ u1: makeTreeMessage({ id: 'u1', role: 'user', content: 'Hello' }),
62
+ a1: makeTreeMessage({
63
+ id: 'a1',
64
+ parentId: 'u1',
65
+ role: 'assistant',
66
+ content: 'Hi',
67
+ }),
68
+ u2: makeTreeMessage({
69
+ id: 'u2',
70
+ parentId: 'a1',
71
+ role: 'user',
72
+ content: 'Question',
73
+ }),
74
+ a2: makeTreeMessage({
75
+ id: 'a2',
76
+ parentId: 'u2',
77
+ role: 'assistant',
78
+ content: 'Answer 1',
79
+ }),
80
+ a3: makeTreeMessage({
81
+ id: 'a3',
82
+ parentId: 'u2',
83
+ role: 'assistant',
84
+ content: 'Answer 2',
85
+ }),
86
+ a1b: makeTreeMessage({
87
+ id: 'a1b',
88
+ parentId: 'u1',
89
+ role: 'assistant',
90
+ content: 'Alt hi',
91
+ }),
92
+ u3: makeTreeMessage({
93
+ id: 'u3',
94
+ parentId: 'a1b',
95
+ role: 'user',
96
+ content: 'Follow-up',
97
+ }),
98
+ },
99
+ 'u1',
100
+ );
101
+ }
102
+
103
+ describe('createEmptyTree', () => {
104
+ it('returns an empty tree', () => {
105
+ const tree = createEmptyTree();
106
+ expect(tree.nodes).toEqual({});
107
+ expect(tree.rootId).toBeNull();
108
+ });
109
+ });
110
+
111
+ describe('getLinearPath', () => {
112
+ it('returns an empty array for an empty tree', () => {
113
+ expect(getLinearPath(createEmptyTree())).toEqual([]);
114
+ });
115
+
116
+ it('returns the active path from root following activeChildIndex', () => {
117
+ const tree = makeBranchingTree();
118
+ const path = getLinearPath(tree);
119
+
120
+ expect(path.map((m) => m.id)).toEqual(['u1', 'a1', 'u2', 'a2']);
121
+ });
122
+
123
+ it('returns a different path after changing activeChildIndex', () => {
124
+ const tree = makeBranchingTree();
125
+ tree.nodes.u1.activeChildIndex = 1;
126
+ const path = getLinearPath(tree);
127
+
128
+ expect(path.map((m) => m.id)).toEqual(['u1', 'a1b', 'u3']);
129
+ });
130
+
131
+ it('enriches messages with siblingIds and siblingIndex when siblings exist', () => {
132
+ const tree = makeBranchingTree();
133
+ const path = getLinearPath(tree);
134
+
135
+ const a1Msg = path.find((m) => m.id === 'a1')!;
136
+ expect(a1Msg.siblingIds).toEqual(['a1', 'a1b']);
137
+ expect(a1Msg.siblingIndex).toBe(0);
138
+
139
+ const a2Msg = path.find((m) => m.id === 'a2')!;
140
+ expect(a2Msg.siblingIds).toEqual(['a2', 'a3']);
141
+ expect(a2Msg.siblingIndex).toBe(0);
142
+ });
143
+
144
+ it('does not set siblingIds for nodes without siblings', () => {
145
+ const tree = makeBranchingTree();
146
+ const path = getLinearPath(tree);
147
+
148
+ const u2Msg = path.find((m) => m.id === 'u2')!;
149
+ expect(u2Msg.siblingIds).toBeUndefined();
150
+ expect(u2Msg.siblingIndex).toBeUndefined();
151
+ });
152
+
153
+ it('does not set siblingIds on the root node', () => {
154
+ const tree = makeBranchingTree();
155
+ const path = getLinearPath(tree);
156
+
157
+ const rootMsg = path[0];
158
+ expect(rootMsg.id).toBe('u1');
159
+ expect(rootMsg.siblingIds).toBeUndefined();
160
+ expect(rootMsg.siblingIndex).toBeUndefined();
161
+ });
162
+
163
+ it('preserves message fields', () => {
164
+ const tree = makeBranchingTree();
165
+ const path = getLinearPath(tree);
166
+
167
+ const u1Msg = path[0];
168
+ expect(u1Msg.role).toBe('user');
169
+ expect(u1Msg.content).toBe('Hello');
170
+ expect(u1Msg.status).toBe('completed');
171
+ expect(u1Msg.createdAt).toEqual(new Date('2025-01-01'));
172
+ });
173
+ });
174
+
175
+ describe('addNode', () => {
176
+ it('adds the first node as root', () => {
177
+ const tree = createEmptyTree();
178
+ const node = makeTreeMessage({ id: 'u1', content: 'Hello' });
179
+
180
+ addNode(tree, node);
181
+
182
+ expect(tree.nodes.u1).toBe(node);
183
+ expect(tree.rootId).toBe('u1');
184
+ });
185
+
186
+ it('appends a child and updates parent.childrenIds', () => {
187
+ const tree = createEmptyTree();
188
+ addNode(tree, makeTreeMessage({ id: 'u1' }));
189
+ addNode(
190
+ tree,
191
+ makeTreeMessage({ id: 'a1', parentId: 'u1', role: 'assistant' }),
192
+ );
193
+
194
+ expect(tree.nodes.u1.childrenIds).toEqual(['a1']);
195
+ });
196
+
197
+ it('sets parent.activeChildIndex to point at the new child', () => {
198
+ const tree = createEmptyTree();
199
+ addNode(tree, makeTreeMessage({ id: 'u1' }));
200
+ addNode(
201
+ tree,
202
+ makeTreeMessage({ id: 'a1', parentId: 'u1', role: 'assistant' }),
203
+ );
204
+
205
+ expect(tree.nodes.u1.activeChildIndex).toBe(0);
206
+
207
+ addNode(
208
+ tree,
209
+ makeTreeMessage({ id: 'a2', parentId: 'u1', role: 'assistant' }),
210
+ );
211
+
212
+ expect(tree.nodes.u1.activeChildIndex).toBe(1);
213
+ });
214
+
215
+ it('makes the new node the last in the active path', () => {
216
+ const tree = createEmptyTree();
217
+ addNode(tree, makeTreeMessage({ id: 'u1' }));
218
+ addNode(
219
+ tree,
220
+ makeTreeMessage({ id: 'a1', parentId: 'u1', role: 'assistant' }),
221
+ );
222
+ addNode(tree, makeTreeMessage({ id: 'u2', parentId: 'a1' }));
223
+
224
+ const path = getLinearPath(tree);
225
+ expect(path[path.length - 1].id).toBe('u2');
226
+ });
227
+
228
+ it('supports adding sibling nodes (regeneration)', () => {
229
+ const tree = createEmptyTree();
230
+ addNode(tree, makeTreeMessage({ id: 'u1' }));
231
+ addNode(
232
+ tree,
233
+ makeTreeMessage({ id: 'a1', parentId: 'u1', role: 'assistant' }),
234
+ );
235
+ addNode(
236
+ tree,
237
+ makeTreeMessage({ id: 'a2', parentId: 'u1', role: 'assistant' }),
238
+ );
239
+
240
+ expect(tree.nodes.u1.childrenIds).toEqual(['a1', 'a2']);
241
+ expect(tree.nodes.u1.activeChildIndex).toBe(1);
242
+
243
+ const path = getLinearPath(tree);
244
+ expect(path.map((m) => m.id)).toEqual(['u1', 'a2']);
245
+ });
246
+ });
247
+
248
+ describe('updateNode', () => {
249
+ it('updates content of an existing node', () => {
250
+ const tree = makeBranchingTree();
251
+ updateNode(tree, 'a1', { content: 'Updated hi' });
252
+ expect(tree.nodes.a1.content).toBe('Updated hi');
253
+ });
254
+
255
+ it('updates status of an existing node', () => {
256
+ const tree = makeBranchingTree();
257
+ updateNode(tree, 'a1', { status: 'sending' });
258
+ expect(tree.nodes.a1.status).toBe('sending');
259
+ });
260
+
261
+ it('is a no-op for nonexistent id', () => {
262
+ const tree = makeBranchingTree();
263
+ const nodeCount = Object.keys(tree.nodes).length;
264
+ updateNode(tree, 'nonexistent', { content: 'x' });
265
+ expect(Object.keys(tree.nodes).length).toBe(nodeCount);
266
+ });
267
+ });
268
+
269
+ describe('updateNodeId', () => {
270
+ it('re-keys a node from old to new id', () => {
271
+ const tree = makeBranchingTree();
272
+ updateNodeId(tree, 'a2', 'a2-server');
273
+
274
+ expect(tree.nodes['a2-server']).toBeDefined();
275
+ expect(tree.nodes['a2']).toBeUndefined();
276
+ expect(tree.nodes['a2-server'].id).toBe('a2-server');
277
+ });
278
+
279
+ it('updates parent.childrenIds reference', () => {
280
+ const tree = makeBranchingTree();
281
+ updateNodeId(tree, 'a2', 'a2-server');
282
+
283
+ expect(tree.nodes.u2.childrenIds).toContain('a2-server');
284
+ expect(tree.nodes.u2.childrenIds).not.toContain('a2');
285
+ });
286
+
287
+ it('updates children.parentId references', () => {
288
+ const tree = makeBranchingTree();
289
+ updateNodeId(tree, 'u2', 'u2-server');
290
+
291
+ expect(tree.nodes.a2.parentId).toBe('u2-server');
292
+ expect(tree.nodes.a3.parentId).toBe('u2-server');
293
+ });
294
+
295
+ it('updates rootId when renaming the root', () => {
296
+ const tree = makeBranchingTree();
297
+ updateNodeId(tree, 'u1', 'u1-server');
298
+ expect(tree.rootId).toBe('u1-server');
299
+ });
300
+
301
+ it('is a no-op for nonexistent oldId', () => {
302
+ const tree = makeBranchingTree();
303
+ updateNodeId(tree, 'nonexistent', 'whatever');
304
+ expect(tree.nodes['whatever']).toBeUndefined();
305
+ });
306
+ });
307
+
308
+ describe('navigate', () => {
309
+ it('updates parent.activeChildIndex to the target sibling', () => {
310
+ const tree = makeBranchingTree();
311
+ expect(tree.nodes.u1.activeChildIndex).toBe(0);
312
+
313
+ navigate(tree, 'a1b');
314
+
315
+ expect(tree.nodes.u1.activeChildIndex).toBe(1);
316
+ });
317
+
318
+ it('changes the linear path accordingly', () => {
319
+ const tree = makeBranchingTree();
320
+
321
+ navigate(tree, 'a1b');
322
+
323
+ const path = getLinearPath(tree);
324
+ expect(path.map((m) => m.id)).toEqual(['u1', 'a1b', 'u3']);
325
+ });
326
+
327
+ it('navigating back restores the original path', () => {
328
+ const tree = makeBranchingTree();
329
+
330
+ navigate(tree, 'a1b');
331
+ navigate(tree, 'a1');
332
+
333
+ const path = getLinearPath(tree);
334
+ expect(path.map((m) => m.id)).toEqual(['u1', 'a1', 'u2', 'a2']);
335
+ });
336
+
337
+ it('navigates between leaf siblings', () => {
338
+ const tree = makeBranchingTree();
339
+
340
+ navigate(tree, 'a3');
341
+ expect(tree.nodes.u2.activeChildIndex).toBe(1);
342
+
343
+ const path = getLinearPath(tree);
344
+ expect(path.map((m) => m.id)).toEqual(['u1', 'a1', 'u2', 'a3']);
345
+ });
346
+
347
+ it('is a no-op for nonexistent targetId', () => {
348
+ const tree = makeBranchingTree();
349
+ const originalIndex = tree.nodes.u1.activeChildIndex;
350
+
351
+ navigate(tree, 'nonexistent');
352
+
353
+ expect(tree.nodes.u1.activeChildIndex).toBe(originalIndex);
354
+ });
355
+
356
+ it('is a no-op for root node (has no parent)', () => {
357
+ const tree = makeBranchingTree();
358
+
359
+ navigate(tree, 'u1');
360
+
361
+ expect(tree.nodes.u1.activeChildIndex).toBe(0);
362
+ });
363
+ });
364
+
365
+ describe('integration: full branching scenario', () => {
366
+ it('simulates send → regenerate → navigate workflow', () => {
367
+ const tree = createEmptyTree();
368
+
369
+ addNode(tree, makeTreeMessage({ id: 'u1', content: 'Hello' }));
370
+ addNode(
371
+ tree,
372
+ makeTreeMessage({
373
+ id: 'a1',
374
+ parentId: 'u1',
375
+ role: 'assistant',
376
+ content: 'Hi there',
377
+ }),
378
+ );
379
+ addNode(
380
+ tree,
381
+ makeTreeMessage({ id: 'u2', parentId: 'a1', content: 'Tell me more' }),
382
+ );
383
+ addNode(
384
+ tree,
385
+ makeTreeMessage({
386
+ id: 'a2',
387
+ parentId: 'u2',
388
+ role: 'assistant',
389
+ content: 'Sure thing',
390
+ }),
391
+ );
392
+
393
+ let path = getLinearPath(tree);
394
+ expect(path.map((m) => m.id)).toEqual(['u1', 'a1', 'u2', 'a2']);
395
+
396
+ addNode(
397
+ tree,
398
+ makeTreeMessage({
399
+ id: 'a3',
400
+ parentId: 'u2',
401
+ role: 'assistant',
402
+ content: 'Certainly!',
403
+ }),
404
+ );
405
+
406
+ path = getLinearPath(tree);
407
+ expect(path.map((m) => m.id)).toEqual(['u1', 'a1', 'u2', 'a3']);
408
+
409
+ navigate(tree, 'a2');
410
+ path = getLinearPath(tree);
411
+ expect(path.map((m) => m.id)).toEqual(['u1', 'a1', 'u2', 'a2']);
412
+
413
+ const a2Msg = path.find((m) => m.id === 'a2')!;
414
+ expect(a2Msg.siblingIds).toEqual(['a2', 'a3']);
415
+ expect(a2Msg.siblingIndex).toBe(0);
416
+
417
+ navigate(tree, 'a3');
418
+ path = getLinearPath(tree);
419
+ const a3Msg = path.find((m) => m.id === 'a3')!;
420
+ expect(a3Msg.siblingIds).toEqual(['a2', 'a3']);
421
+ expect(a3Msg.siblingIndex).toBe(1);
422
+ });
423
+ });
@@ -0,0 +1,117 @@
1
+ import type { BaseMessage, ConversationTree, TreeMessage } from '../types';
2
+
3
+ export function createEmptyTree(): ConversationTree {
4
+ return { nodes: {}, rootId: null };
5
+ }
6
+
7
+ /**
8
+ * Walks from rootId following activeChildIndex at each node.
9
+ * Enriches each node with sibling metadata for the UI.
10
+ */
11
+ export function getLinearPath(tree: ConversationTree): BaseMessage[] {
12
+ const { nodes, rootId } = tree;
13
+ if (!rootId || !nodes[rootId]) return [];
14
+
15
+ const path: BaseMessage[] = [];
16
+ let current: string | null = rootId;
17
+
18
+ while (current !== null) {
19
+ const node: TreeMessage | undefined = nodes[current];
20
+ if (!node) break;
21
+
22
+ const base: BaseMessage = {
23
+ id: node.id,
24
+ role: node.role,
25
+ content: node.content,
26
+ createdAt: node.createdAt,
27
+ status: node.status,
28
+ };
29
+
30
+ if (node.parentId !== null) {
31
+ const parent = nodes[node.parentId];
32
+ if (parent && parent.childrenIds.length > 1) {
33
+ base.siblingIds = [...parent.childrenIds];
34
+ base.siblingIndex = parent.childrenIds.indexOf(node.id);
35
+ }
36
+ }
37
+
38
+ path.push(base);
39
+
40
+ if (node.childrenIds.length === 0) break;
41
+ current = node.childrenIds[node.activeChildIndex];
42
+ }
43
+
44
+ return path;
45
+ }
46
+
47
+ export function addNode(tree: ConversationTree, node: TreeMessage): void {
48
+ tree.nodes[node.id] = node;
49
+
50
+ if (node.parentId !== null) {
51
+ const parent = tree.nodes[node.parentId];
52
+ if (parent && !parent.childrenIds.includes(node.id)) {
53
+ parent.childrenIds.push(node.id);
54
+ parent.activeChildIndex = parent.childrenIds.length - 1;
55
+ }
56
+ } else if (tree.rootId === null) {
57
+ tree.rootId = node.id;
58
+ }
59
+ }
60
+
61
+ export function updateNode(
62
+ tree: ConversationTree,
63
+ id: string,
64
+ updates: Partial<Pick<TreeMessage, 'content' | 'status' | 'role'>>,
65
+ ): void {
66
+ const node = tree.nodes[id];
67
+ if (!node) return;
68
+
69
+ if (updates.content !== undefined) node.content = updates.content;
70
+ if (updates.status !== undefined) node.status = updates.status;
71
+ if (updates.role !== undefined) node.role = updates.role;
72
+ }
73
+
74
+ export function updateNodeId(
75
+ tree: ConversationTree,
76
+ oldId: string,
77
+ newId: string,
78
+ ): void {
79
+ const node = tree.nodes[oldId];
80
+ if (!node) return;
81
+
82
+ delete tree.nodes[oldId];
83
+ node.id = newId;
84
+ tree.nodes[newId] = node;
85
+
86
+ if (node.parentId !== null) {
87
+ const parent = tree.nodes[node.parentId];
88
+ if (parent) {
89
+ const idx = parent.childrenIds.indexOf(oldId);
90
+ if (idx !== -1) parent.childrenIds[idx] = newId;
91
+ }
92
+ }
93
+
94
+ for (const childId of node.childrenIds) {
95
+ const child = tree.nodes[childId];
96
+ if (child) child.parentId = newId;
97
+ }
98
+
99
+ if (tree.rootId === oldId) tree.rootId = newId;
100
+ }
101
+
102
+ /**
103
+ * Navigate to a different branch by updating the parent's activeChildIndex
104
+ * to point at the target sibling.
105
+ */
106
+ export function navigate(tree: ConversationTree, targetId: string): void {
107
+ const node = tree.nodes[targetId];
108
+ if (!node || node.parentId === null) return;
109
+
110
+ const parent = tree.nodes[node.parentId];
111
+ if (!parent) return;
112
+
113
+ const idx = parent.childrenIds.indexOf(targetId);
114
+ if (idx === -1) return;
115
+
116
+ parent.activeChildIndex = idx;
117
+ }
@@ -0,0 +1,5 @@
1
+ export function generateId() {
2
+ return typeof crypto !== 'undefined' && 'randomUUID' in crypto
3
+ ? crypto.randomUUID()
4
+ : String(Date.now());
5
+ }
@@ -0,0 +1,4 @@
1
+ export * from './conversationTree';
2
+ export * from './generateId';
3
+ export * from './secondsToDate';
4
+ export * from './toError';
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Convert a unix timestamp in seconds to a `Date`. Useful when consuming
3
+ * APIs that return `created_at` / `updated_at` as integer seconds.
4
+ */
5
+ export function secondsToDate(seconds: number): Date {
6
+ return new Date(seconds * 1000);
7
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Normalise an unknown thrown value into an `Error` instance.
3
+ * Wraps non-Error values (strings, plain objects, etc.) so callers can
4
+ * always rely on `.message` and a stable type.
5
+ */
6
+ export function toError(value: unknown): Error {
7
+ return value instanceof Error ? value : new Error(String(value));
8
+ }