@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,112 @@
1
+ import { nextTick, ref } from 'vue';
2
+ import { useMarkdownParser } from './useMarkdownParser';
3
+ import { createTextPlugin } from '../test-utils/createTextPlugin';
4
+
5
+ describe('useMarkdownParser', () => {
6
+ it('converts a plain text paragraph to HTML', () => {
7
+ const source = ref('Hello world');
8
+ const { html } = useMarkdownParser(source, ref(undefined));
9
+
10
+ expect(html.value).toBe('<p>Hello world</p>');
11
+ });
12
+
13
+ it('converts headings and inline formatting', () => {
14
+ const source = ref('# Title\n\n**bold** and *italic*');
15
+ const { html } = useMarkdownParser(source, ref(undefined));
16
+
17
+ expect(html.value).toContain('<h1>Title</h1>');
18
+ expect(html.value).toContain('<strong>bold</strong>');
19
+ expect(html.value).toContain('<em>italic</em>');
20
+ });
21
+
22
+ it('re-computes when the source ref changes', async () => {
23
+ const source = ref('first');
24
+ const { html } = useMarkdownParser(source, ref(undefined));
25
+
26
+ expect(html.value).toBe('<p>first</p>');
27
+
28
+ source.value = 'second';
29
+ await nextTick();
30
+
31
+ expect(html.value).toBe('<p>second</p>');
32
+ });
33
+
34
+ it('returns empty string for empty source', () => {
35
+ const source = ref('');
36
+ const { html } = useMarkdownParser(source, ref(undefined));
37
+
38
+ expect(html.value).toBe('');
39
+ });
40
+
41
+ it('renders GFM tables', () => {
42
+ const source = ref('| A | B |\n| --- | --- |\n| 1 | 2 |');
43
+ const { html } = useMarkdownParser(source, ref(undefined));
44
+
45
+ expect(html.value).toContain('<table>');
46
+ expect(html.value).toContain('<th>A</th>');
47
+ expect(html.value).toContain('<td>1</td>');
48
+ });
49
+
50
+ it('renders GFM strikethrough', () => {
51
+ const source = ref('~~deleted~~');
52
+ const { html } = useMarkdownParser(source, ref(undefined));
53
+
54
+ expect(html.value).toContain('<del>deleted</del>');
55
+ });
56
+
57
+ it('renders GFM task list checkboxes', () => {
58
+ const source = ref('- [x] done\n- [ ] pending');
59
+ const { html } = useMarkdownParser(source, ref(undefined));
60
+
61
+ expect(html.value).toContain('checked=""');
62
+ expect(html.value).toContain('type="checkbox"');
63
+ });
64
+
65
+ it('auto-links bare URLs', () => {
66
+ const source = ref('Visit https://example.com for details');
67
+ const { html } = useMarkdownParser(source, ref(undefined));
68
+
69
+ expect(html.value).toContain('<a href="https://example.com">');
70
+ });
71
+
72
+ it('sanitizes script tags', () => {
73
+ const source = ref('Safe\n\n<script>alert("xss")</script>');
74
+ const { html } = useMarkdownParser(source, ref(undefined));
75
+
76
+ expect(html.value).not.toContain('<script>');
77
+ expect(html.value).toContain('Safe');
78
+ });
79
+
80
+ it('strips onerror attributes from images', () => {
81
+ const source = ref('<img src=x onerror=alert(1)>');
82
+ const { html } = useMarkdownParser(source, ref(undefined));
83
+
84
+ expect(html.value).not.toContain('onerror');
85
+ });
86
+
87
+ it('applies a custom plugin', () => {
88
+ const source = ref('hello world');
89
+ const plugins = ref([createTextPlugin((t) => t.toUpperCase())]);
90
+ const { html } = useMarkdownParser(source, plugins);
91
+
92
+ expect(html.value).toBe('<p>HELLO WORLD</p>');
93
+ });
94
+
95
+ it('applies multiple plugins in order', () => {
96
+ const source = ref('hello');
97
+ const plugins = ref([
98
+ createTextPlugin((t) => `prefix:${t}`),
99
+ createTextPlugin((t) => t.toUpperCase()),
100
+ ]);
101
+ const { html } = useMarkdownParser(source, plugins);
102
+
103
+ expect(html.value).toBe('<p>PREFIX:HELLO</p>');
104
+ });
105
+
106
+ it('works when plugins is undefined', () => {
107
+ const source = ref('Hello world');
108
+ const { html } = useMarkdownParser(source, ref(undefined));
109
+
110
+ expect(html.value).toBe('<p>Hello world</p>');
111
+ });
112
+ });
@@ -0,0 +1,47 @@
1
+ import { computed, type MaybeRefOrGetter, toValue } from 'vue';
2
+ import { unified, type PluggableList } from 'unified';
3
+ import remarkParse from 'remark-parse';
4
+ import remarkGfm from 'remark-gfm';
5
+ import remarkRehype from 'remark-rehype';
6
+ import rehypeStringify from 'rehype-stringify';
7
+ import DOMPurify from 'dompurify';
8
+
9
+ type SyncProcessor = { processSync: (value: string) => { toString(): string } };
10
+
11
+ function buildProcessor(plugins: PluggableList): SyncProcessor {
12
+ let proc = unified().use(remarkParse).use(remarkGfm);
13
+ for (const plugin of plugins) {
14
+ proc = proc.use(plugin as any);
15
+ }
16
+ return proc.use(remarkRehype).use(rehypeStringify);
17
+ }
18
+
19
+ export function useMarkdownParser(
20
+ source: MaybeRefOrGetter<string>,
21
+ plugins: MaybeRefOrGetter<PluggableList | undefined>,
22
+ ) {
23
+ let cachedPlugins: PluggableList | undefined;
24
+ let cachedProcessor: SyncProcessor | undefined;
25
+
26
+ function getProcessor(list: PluggableList): SyncProcessor {
27
+ if (list === cachedPlugins) return cachedProcessor!;
28
+ cachedPlugins = list;
29
+ cachedProcessor = buildProcessor(list);
30
+ return cachedProcessor;
31
+ }
32
+
33
+ const html = computed(() => {
34
+ const markdown = toValue(source);
35
+ if (!markdown) return '';
36
+ try {
37
+ const raw = String(
38
+ getProcessor(toValue(plugins) ?? []).processSync(markdown),
39
+ );
40
+ return DOMPurify.sanitize(raw);
41
+ } catch {
42
+ return DOMPurify.sanitize(markdown);
43
+ }
44
+ });
45
+
46
+ return { html };
47
+ }
@@ -0,0 +1,535 @@
1
+ import type { ConversationTree, TreeMessage } from '../types';
2
+ import { useMessageHandler } from './useMessageHandler';
3
+
4
+ function makeTreeMessage(
5
+ overrides: Partial<TreeMessage> & { id: string },
6
+ ): TreeMessage {
7
+ return {
8
+ parentId: null,
9
+ childrenIds: [],
10
+ activeChildIndex: 0,
11
+ role: 'user',
12
+ content: '',
13
+ status: 'completed',
14
+ createdAt: new Date('2025-01-01'),
15
+ ...overrides,
16
+ };
17
+ }
18
+
19
+ function makeTree(
20
+ nodes: Record<string, TreeMessage>,
21
+ rootId: string,
22
+ ): ConversationTree {
23
+ for (const node of Object.values(nodes)) {
24
+ node.childrenIds = [];
25
+ }
26
+ for (const node of Object.values(nodes)) {
27
+ if (node.parentId !== null) {
28
+ const parent = nodes[node.parentId];
29
+ if (parent) parent.childrenIds.push(node.id);
30
+ }
31
+ }
32
+ for (const node of Object.values(nodes)) {
33
+ node.activeChildIndex = 0;
34
+ }
35
+ return { nodes, rootId };
36
+ }
37
+
38
+ describe('useMessageHandler', () => {
39
+ describe('commitUserMessage', () => {
40
+ it('adds a user message to the map and exposes it in the ordered list', () => {
41
+ const handler = useMessageHandler();
42
+ handler.setCurrentUserMessage('Hello');
43
+ const msg = handler.commitUserMessage();
44
+
45
+ expect(msg.role).toBe('user');
46
+ expect(msg.content).toBe('Hello');
47
+ expect(handler.messages.value).toHaveLength(1);
48
+ expect(handler.messages.value[0].id).toBe(msg.id);
49
+ });
50
+
51
+ it('appends consecutive messages in order', () => {
52
+ const handler = useMessageHandler();
53
+
54
+ handler.setCurrentUserMessage('First');
55
+ const first = handler.commitUserMessage();
56
+
57
+ handler.setCurrentUserMessage('Second');
58
+ const second = handler.commitUserMessage();
59
+
60
+ expect(handler.messages.value).toHaveLength(2);
61
+ expect(handler.messages.value[0].id).toBe(first.id);
62
+ expect(handler.messages.value[1].id).toBe(second.id);
63
+ });
64
+
65
+ it('clears currentUserMessage after commit', () => {
66
+ const handler = useMessageHandler();
67
+ handler.setCurrentUserMessage('Hello');
68
+ handler.commitUserMessage();
69
+
70
+ expect(handler.currentUserMessage.value).toBe('');
71
+ });
72
+
73
+ it('throws when current message is empty', () => {
74
+ const handler = useMessageHandler();
75
+ expect(() => handler.commitUserMessage()).toThrow('No message');
76
+ });
77
+
78
+ it('throws when current message is only whitespace', () => {
79
+ const handler = useMessageHandler();
80
+ handler.setCurrentUserMessage(' ');
81
+ expect(() => handler.commitUserMessage()).toThrow('No message');
82
+ });
83
+ });
84
+
85
+ describe('updateMessage', () => {
86
+ it('updates an existing message in place', () => {
87
+ const handler = useMessageHandler();
88
+ handler.setCurrentUserMessage('Hello');
89
+ const msg = handler.commitUserMessage();
90
+
91
+ handler.updateMessage({
92
+ id: msg.id,
93
+ content: 'Updated',
94
+ role: 'user',
95
+ status: 'completed',
96
+ });
97
+
98
+ expect(handler.messages.value[0].content).toBe('Updated');
99
+ });
100
+
101
+ it('creates a new node when id does not exist (upsert)', () => {
102
+ const handler = useMessageHandler();
103
+ handler.updateMessage({
104
+ id: 'new-1',
105
+ content: 'Upserted',
106
+ role: 'assistant',
107
+ status: 'completed',
108
+ });
109
+
110
+ expect(handler.messages.value).toHaveLength(1);
111
+ expect(handler.messages.value[0].content).toBe('Upserted');
112
+ expect(handler.messages.value[0].id).toBe('new-1');
113
+ });
114
+
115
+ it('upserted message is appended after existing messages', () => {
116
+ const handler = useMessageHandler();
117
+ handler.setCurrentUserMessage('Hello');
118
+ handler.commitUserMessage();
119
+
120
+ handler.updateMessage({
121
+ id: 'assistant-1',
122
+ content: 'Reply',
123
+ role: 'assistant',
124
+ status: 'completed',
125
+ });
126
+
127
+ expect(handler.messages.value).toHaveLength(2);
128
+ expect(handler.messages.value[1].id).toBe('assistant-1');
129
+ expect(handler.messages.value[1].content).toBe('Reply');
130
+ });
131
+
132
+ it('updates a mid-tree node in place without truncating the path', () => {
133
+ const handler = useMessageHandler();
134
+ handler.setTree(
135
+ makeTree(
136
+ {
137
+ u1: makeTreeMessage({ id: 'u1', content: 'First' }),
138
+ a1: makeTreeMessage({
139
+ id: 'a1',
140
+ parentId: 'u1',
141
+ role: 'assistant',
142
+ content: 'Reply 1',
143
+ }),
144
+ u2: makeTreeMessage({
145
+ id: 'u2',
146
+ parentId: 'a1',
147
+ content: 'Second',
148
+ }),
149
+ a2: makeTreeMessage({
150
+ id: 'a2',
151
+ parentId: 'u2',
152
+ role: 'assistant',
153
+ content: 'Reply 2',
154
+ }),
155
+ },
156
+ 'u1',
157
+ ),
158
+ );
159
+
160
+ handler.updateMessage({
161
+ id: 'a1',
162
+ content: 'Regenerated',
163
+ role: 'assistant',
164
+ status: 'sending',
165
+ });
166
+
167
+ expect(handler.messages.value).toHaveLength(4);
168
+ expect(handler.messages.value[1].content).toBe('Regenerated');
169
+ });
170
+
171
+ it('does not truncate when updating the last message', () => {
172
+ const handler = useMessageHandler();
173
+ handler.setTree(
174
+ makeTree(
175
+ {
176
+ u1: makeTreeMessage({ id: 'u1', content: 'Hello' }),
177
+ a1: makeTreeMessage({
178
+ id: 'a1',
179
+ parentId: 'u1',
180
+ role: 'assistant',
181
+ content: 'Hi',
182
+ }),
183
+ },
184
+ 'u1',
185
+ ),
186
+ );
187
+
188
+ handler.updateMessage({
189
+ id: 'a1',
190
+ content: 'Streaming...',
191
+ role: 'assistant',
192
+ status: 'sending',
193
+ });
194
+
195
+ expect(handler.messages.value).toHaveLength(2);
196
+ expect(handler.messages.value[1].content).toBe('Streaming...');
197
+ });
198
+ });
199
+
200
+ describe('getMessageById', () => {
201
+ it('returns the message for a known id', () => {
202
+ const handler = useMessageHandler();
203
+ handler.setCurrentUserMessage('Hello');
204
+ const msg = handler.commitUserMessage();
205
+
206
+ const found = handler.getMessageById(msg.id);
207
+ expect(found).toBeDefined();
208
+ expect(found?.id).toBe(msg.id);
209
+ expect(found?.content).toBe('Hello');
210
+ expect(found?.role).toBe('user');
211
+ });
212
+
213
+ it('returns undefined for an unknown id', () => {
214
+ const handler = useMessageHandler();
215
+ expect(handler.getMessageById('nonexistent')).toBeUndefined();
216
+ });
217
+ });
218
+
219
+ describe('updateMessageId', () => {
220
+ it('renames the id of an existing message', () => {
221
+ const handler = useMessageHandler();
222
+ handler.setCurrentUserMessage('Hello');
223
+ const msg = handler.commitUserMessage();
224
+ const oldId = msg.id;
225
+
226
+ handler.updateMessageId(oldId, 'new-id');
227
+
228
+ expect(handler.messages.value[0].id).toBe('new-id');
229
+ expect(handler.getMessageById('new-id')).toBeDefined();
230
+ expect(handler.getMessageById(oldId)).toBeUndefined();
231
+ });
232
+
233
+ it('preserves all other message fields', () => {
234
+ const handler = useMessageHandler();
235
+ handler.setCurrentUserMessage('Hello');
236
+ const msg = handler.commitUserMessage();
237
+ const { role, content, status, createdAt } = msg;
238
+
239
+ handler.updateMessageId(msg.id, 'renamed');
240
+
241
+ const renamed = handler.getMessageById('renamed');
242
+ expect(renamed?.role).toBe(role);
243
+ expect(renamed?.content).toBe(content);
244
+ expect(renamed?.status).toBe(status);
245
+ expect(renamed?.createdAt).toEqual(createdAt);
246
+ });
247
+
248
+ it('keeps the message at the same position in the list', () => {
249
+ const handler = useMessageHandler();
250
+ handler.setTree(
251
+ makeTree(
252
+ {
253
+ a: makeTreeMessage({ id: 'a', content: 'A' }),
254
+ b: makeTreeMessage({
255
+ id: 'b',
256
+ parentId: 'a',
257
+ role: 'assistant',
258
+ content: 'B',
259
+ }),
260
+ c: makeTreeMessage({ id: 'c', parentId: 'b', content: 'C' }),
261
+ },
262
+ 'a',
263
+ ),
264
+ );
265
+
266
+ handler.updateMessageId('b', 'b-renamed');
267
+
268
+ expect(handler.messages.value.map((m) => m.id)).toEqual([
269
+ 'a',
270
+ 'b-renamed',
271
+ 'c',
272
+ ]);
273
+ });
274
+
275
+ it('is a no-op when oldId does not exist', () => {
276
+ const handler = useMessageHandler();
277
+ handler.setCurrentUserMessage('Hello');
278
+ handler.commitUserMessage();
279
+
280
+ handler.updateMessageId('nonexistent', 'whatever');
281
+
282
+ expect(handler.messages.value).toHaveLength(1);
283
+ expect(handler.getMessageById('whatever')).toBeUndefined();
284
+ });
285
+ });
286
+
287
+ describe('clearMessages', () => {
288
+ it('empties all state', () => {
289
+ const handler = useMessageHandler();
290
+ handler.setCurrentUserMessage('Hello');
291
+ handler.commitUserMessage();
292
+ handler.updateMessage({
293
+ id: 'a1',
294
+ content: 'Reply',
295
+ role: 'assistant',
296
+ status: 'completed',
297
+ });
298
+
299
+ handler.clearMessages();
300
+
301
+ expect(handler.messages.value).toHaveLength(0);
302
+ expect(handler.getMessageById('a1')).toBeUndefined();
303
+ });
304
+ });
305
+
306
+ describe('setCurrentUserMessage / clearCurrentUserMessage', () => {
307
+ it('sets and clears the current user message', () => {
308
+ const handler = useMessageHandler();
309
+
310
+ handler.setCurrentUserMessage('Draft');
311
+ expect(handler.currentUserMessage.value).toBe('Draft');
312
+
313
+ handler.clearCurrentUserMessage();
314
+ expect(handler.currentUserMessage.value).toBe('');
315
+ });
316
+ });
317
+
318
+ describe('messages computed', () => {
319
+ it('returns messages in root-to-leaf order', () => {
320
+ const handler = useMessageHandler();
321
+ handler.setCurrentUserMessage('One');
322
+ handler.commitUserMessage();
323
+
324
+ handler.updateMessage({
325
+ id: 'a1',
326
+ content: 'Reply 1',
327
+ role: 'assistant',
328
+ status: 'completed',
329
+ });
330
+
331
+ handler.setCurrentUserMessage('Two');
332
+ handler.commitUserMessage();
333
+
334
+ handler.updateMessage({
335
+ id: 'a2',
336
+ content: 'Reply 2',
337
+ role: 'assistant',
338
+ status: 'completed',
339
+ });
340
+
341
+ const roles = handler.messages.value.map((m) => m.role);
342
+ expect(roles).toEqual(['user', 'assistant', 'user', 'assistant']);
343
+
344
+ const contents = handler.messages.value.map((m) => m.content);
345
+ expect(contents).toEqual(['One', 'Reply 1', 'Two', 'Reply 2']);
346
+ });
347
+
348
+ it('updates reactively when a message is added', () => {
349
+ const handler = useMessageHandler();
350
+ expect(handler.messages.value).toHaveLength(0);
351
+
352
+ handler.setCurrentUserMessage('Hello');
353
+ handler.commitUserMessage();
354
+ expect(handler.messages.value).toHaveLength(1);
355
+
356
+ handler.updateMessage({
357
+ id: 'a1',
358
+ content: 'Hi',
359
+ role: 'assistant',
360
+ status: 'completed',
361
+ });
362
+ expect(handler.messages.value).toHaveLength(2);
363
+ });
364
+ });
365
+
366
+ describe('setTree', () => {
367
+ it('replaces the tree and exposes correct flat messages', () => {
368
+ const handler = useMessageHandler();
369
+ handler.setTree(
370
+ makeTree(
371
+ {
372
+ u1: makeTreeMessage({ id: 'u1', content: 'Hello' }),
373
+ a1: makeTreeMessage({
374
+ id: 'a1',
375
+ parentId: 'u1',
376
+ role: 'assistant',
377
+ content: 'Hi',
378
+ }),
379
+ },
380
+ 'u1',
381
+ ),
382
+ );
383
+
384
+ expect(handler.messages.value).toHaveLength(2);
385
+ expect(handler.messages.value.map((m) => m.id)).toEqual(['u1', 'a1']);
386
+ });
387
+
388
+ it('preserves branching from incoming tree', () => {
389
+ const handler = useMessageHandler();
390
+ handler.setTree(
391
+ makeTree(
392
+ {
393
+ r: makeTreeMessage({ id: 'r' }),
394
+ a: makeTreeMessage({
395
+ id: 'a',
396
+ parentId: 'r',
397
+ role: 'assistant',
398
+ content: 'A',
399
+ }),
400
+ b: makeTreeMessage({
401
+ id: 'b',
402
+ parentId: 'r',
403
+ role: 'assistant',
404
+ content: 'B',
405
+ }),
406
+ },
407
+ 'r',
408
+ ),
409
+ );
410
+
411
+ const first = handler.messages.value.find((m) => m.id === 'a')!;
412
+ expect(first.siblingIds).toEqual(['a', 'b']);
413
+ });
414
+ });
415
+
416
+ describe('navigate', () => {
417
+ it('switches the visible path to a different branch', () => {
418
+ const handler = useMessageHandler();
419
+ handler.setTree(
420
+ makeTree(
421
+ {
422
+ u1: makeTreeMessage({ id: 'u1', content: 'Hello' }),
423
+ a1: makeTreeMessage({
424
+ id: 'a1',
425
+ parentId: 'u1',
426
+ role: 'assistant',
427
+ content: 'Hi',
428
+ }),
429
+ a2: makeTreeMessage({
430
+ id: 'a2',
431
+ parentId: 'u1',
432
+ role: 'assistant',
433
+ content: 'Hey',
434
+ }),
435
+ u2: makeTreeMessage({
436
+ id: 'u2',
437
+ parentId: 'a2',
438
+ content: 'Follow-up',
439
+ }),
440
+ },
441
+ 'u1',
442
+ ),
443
+ );
444
+
445
+ expect(handler.messages.value.map((m) => m.id)).toEqual(['u1', 'a1']);
446
+
447
+ handler.navigate('a2');
448
+
449
+ expect(handler.messages.value.map((m) => m.id)).toEqual([
450
+ 'u1',
451
+ 'a2',
452
+ 'u2',
453
+ ]);
454
+ expect(handler.currentLeafId.value).toBe('u2');
455
+ });
456
+
457
+ it('exposes sibling metadata on branched messages', () => {
458
+ const handler = useMessageHandler();
459
+ handler.setTree(
460
+ makeTree(
461
+ {
462
+ u1: makeTreeMessage({ id: 'u1', content: 'Hello' }),
463
+ a1: makeTreeMessage({
464
+ id: 'a1',
465
+ parentId: 'u1',
466
+ role: 'assistant',
467
+ content: 'Hi',
468
+ }),
469
+ a2: makeTreeMessage({
470
+ id: 'a2',
471
+ parentId: 'u1',
472
+ role: 'assistant',
473
+ content: 'Hey',
474
+ }),
475
+ },
476
+ 'u1',
477
+ ),
478
+ );
479
+
480
+ const a1Msg = handler.messages.value.find((m) => m.id === 'a1')!;
481
+ expect(a1Msg.siblingIds).toEqual(['a1', 'a2']);
482
+ expect(a1Msg.siblingIndex).toBe(0);
483
+ });
484
+ });
485
+
486
+ describe('currentLeafId', () => {
487
+ it('is null when there are no messages', () => {
488
+ const handler = useMessageHandler();
489
+ expect(handler.currentLeafId.value).toBeNull();
490
+ });
491
+
492
+ it('returns the id of the last message on the active path', () => {
493
+ const handler = useMessageHandler();
494
+ handler.setCurrentUserMessage('Hello');
495
+ const msg = handler.commitUserMessage();
496
+
497
+ expect(handler.currentLeafId.value).toBe(msg.id);
498
+ });
499
+
500
+ it('updates after navigate', () => {
501
+ const handler = useMessageHandler();
502
+ handler.setTree(
503
+ makeTree(
504
+ {
505
+ u1: makeTreeMessage({ id: 'u1', content: 'Hello' }),
506
+ a1: makeTreeMessage({
507
+ id: 'a1',
508
+ parentId: 'u1',
509
+ role: 'assistant',
510
+ content: 'Hi',
511
+ }),
512
+ a2: makeTreeMessage({
513
+ id: 'a2',
514
+ parentId: 'u1',
515
+ role: 'assistant',
516
+ content: 'Hey',
517
+ }),
518
+ u2: makeTreeMessage({
519
+ id: 'u2',
520
+ parentId: 'a2',
521
+ content: 'Follow-up',
522
+ }),
523
+ },
524
+ 'u1',
525
+ ),
526
+ );
527
+
528
+ expect(handler.currentLeafId.value).toBe('a1');
529
+
530
+ handler.navigate('a2');
531
+
532
+ expect(handler.currentLeafId.value).toBe('u2');
533
+ });
534
+ });
535
+ });