@aleph-alpha/chat-kit 1.0.0 → 1.2.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 (133) hide show
  1. package/dist/{CkInput.vue_vue_type_script_setup_true_lang-744ZHsQU.js → CkInput.vue_vue_type_script_setup_true_lang-CRymGkQs.js} +24 -69
  2. package/dist/components/base/CkHistoryItem/CkHistoryItem.vue.d.ts +3 -4
  3. package/dist/components/base/CkHistoryItem/CkHistoryItem.vue.d.ts.map +1 -1
  4. package/dist/components/base/CkHistoryItem/types.d.ts +3 -2
  5. package/dist/components/base/CkHistoryItem/types.d.ts.map +1 -1
  6. package/dist/components/base/CkPromptStopButton/CkPromptStopButton.vue.d.ts.map +1 -1
  7. package/dist/components/index.js +1 -1
  8. package/dist/composables/index.d.ts +2 -0
  9. package/dist/composables/index.d.ts.map +1 -1
  10. package/dist/composables/index.js +3 -2
  11. package/dist/composables/useConversationSessions.d.ts +29 -0
  12. package/dist/composables/useConversationSessions.d.ts.map +1 -0
  13. package/dist/handlers/index.js +2 -2
  14. package/dist/index.d.ts +2 -2
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.js +7 -6
  17. package/dist/purify.es-md74IFlY.js +1081 -0
  18. package/dist/{useChatKitLabels-Dvi2FmjE.js → useChatKitLabels-BmWmMyqX.js} +75 -1152
  19. package/dist/{useConversations-Ca242D2P.js → useConversationSessions-ulFhtpQr.js} +50 -2
  20. package/dist/{useMessageHandler-uLHNac39.js → useMessageHandler-BmBdu2EW.js} +4 -1
  21. package/package.json +5 -5
  22. package/src/components/base/CkAgentMessage/CkAgentMessage.spec.ts +91 -0
  23. package/src/components/base/CkAgentMessage/CkAgentMessage.stories.ts +215 -0
  24. package/src/components/base/CkAgentMessage/CkAgentMessage.vue +25 -0
  25. package/src/components/base/CkAgentMessage/index.ts +2 -0
  26. package/src/components/base/CkAgentMessage/types.ts +11 -0
  27. package/src/components/base/CkConversationLayout/CkConversationLayout.spec.ts +129 -0
  28. package/src/components/base/CkConversationLayout/CkConversationLayout.stories.ts +251 -0
  29. package/src/components/base/CkConversationLayout/CkConversationLayout.vue +206 -0
  30. package/src/components/base/CkConversationLayout/index.ts +2 -0
  31. package/src/components/base/CkConversationLayout/types.ts +18 -0
  32. package/src/components/base/CkCopyMessageActionButton/CkCopyMessageActionButton.spec.ts +89 -0
  33. package/src/components/base/CkCopyMessageActionButton/CkCopyMessageActionButton.stories.ts +70 -0
  34. package/src/components/base/CkCopyMessageActionButton/CkCopyMessageActionButton.vue +110 -0
  35. package/src/components/base/CkCopyMessageActionButton/index.ts +2 -0
  36. package/src/components/base/CkCopyMessageActionButton/types.ts +43 -0
  37. package/src/components/base/CkHistoryItem/CkHistoryItem.spec.ts +42 -0
  38. package/src/components/base/CkHistoryItem/CkHistoryItem.stories.ts +230 -0
  39. package/src/components/base/CkHistoryItem/CkHistoryItem.vue +285 -0
  40. package/src/components/base/CkHistoryItem/activeIndicator.ts +15 -0
  41. package/src/components/base/CkHistoryItem/index.ts +2 -0
  42. package/src/components/base/CkHistoryItem/types.ts +111 -0
  43. package/src/components/base/CkHistoryWrapper/CkHistoryWrapper.spec.ts +26 -0
  44. package/src/components/base/CkHistoryWrapper/CkHistoryWrapper.stories.ts +149 -0
  45. package/src/components/base/CkHistoryWrapper/CkHistoryWrapper.vue +36 -0
  46. package/src/components/base/CkHistoryWrapper/index.ts +2 -0
  47. package/src/components/base/CkHistoryWrapper/types.ts +16 -0
  48. package/src/components/base/CkMarkdownRenderer/CkMarkdownRenderer.spec.ts +348 -0
  49. package/src/components/base/CkMarkdownRenderer/CkMarkdownRenderer.stories.ts +382 -0
  50. package/src/components/base/CkMarkdownRenderer/CkMarkdownRenderer.vue +89 -0
  51. package/src/components/base/CkMarkdownRenderer/defaultMarkdownClasses.ts +19 -0
  52. package/src/components/base/CkMarkdownRenderer/index.ts +5 -0
  53. package/src/components/base/CkMarkdownRenderer/types.ts +53 -0
  54. package/src/components/base/CkMessageActionButton/CkMessageActionButton.spec.ts +40 -0
  55. package/src/components/base/CkMessageActionButton/CkMessageActionButton.stories.ts +56 -0
  56. package/src/components/base/CkMessageActionButton/CkMessageActionButton.vue +23 -0
  57. package/src/components/base/CkMessageActionButton/index.ts +2 -0
  58. package/src/components/base/CkMessageActionButton/types.ts +21 -0
  59. package/src/components/base/CkPromptActionButton/CkPromptActionButton.spec.ts +52 -0
  60. package/src/components/base/CkPromptActionButton/CkPromptActionButton.stories.ts +126 -0
  61. package/src/components/base/CkPromptActionButton/CkPromptActionButton.vue +25 -0
  62. package/src/components/base/CkPromptActionButton/index.ts +2 -0
  63. package/src/components/base/CkPromptActionButton/types.ts +32 -0
  64. package/src/components/base/CkPromptInput/CkPromptInput.spec.ts +181 -0
  65. package/src/components/base/CkPromptInput/CkPromptInput.stories.ts +224 -0
  66. package/src/components/base/CkPromptInput/CkPromptInput.vue +104 -0
  67. package/src/components/base/CkPromptInput/index.ts +2 -0
  68. package/src/components/base/CkPromptInput/types.ts +39 -0
  69. package/src/components/base/CkPromptInputDisclaimer/CkPromptInputDisclaimer.spec.ts +40 -0
  70. package/src/components/base/CkPromptInputDisclaimer/CkPromptInputDisclaimer.stories.ts +103 -0
  71. package/src/components/base/CkPromptInputDisclaimer/CkPromptInputDisclaimer.vue +16 -0
  72. package/src/components/base/CkPromptInputDisclaimer/index.ts +2 -0
  73. package/src/components/base/CkPromptInputDisclaimer/types.ts +17 -0
  74. package/src/components/base/CkPromptStopButton/CkPromptStopButton.spec.ts +64 -0
  75. package/src/components/base/CkPromptStopButton/CkPromptStopButton.stories.ts +81 -0
  76. package/src/components/base/CkPromptStopButton/CkPromptStopButton.vue +27 -0
  77. package/src/components/base/CkPromptStopButton/index.ts +2 -0
  78. package/src/components/base/CkPromptStopButton/types.ts +28 -0
  79. package/src/components/base/CkUserMessage/CkUserMessage.spec.ts +20 -0
  80. package/src/components/base/CkUserMessage/CkUserMessage.stories.ts +112 -0
  81. package/src/components/base/CkUserMessage/CkUserMessage.vue +19 -0
  82. package/src/components/base/CkUserMessage/index.ts +2 -0
  83. package/src/components/base/CkUserMessage/types.ts +14 -0
  84. package/src/components/base/icons/CkSendIcon.vue +22 -0
  85. package/src/components/base/icons/CkStopIcon.vue +18 -0
  86. package/src/components/base/icons/index.ts +2 -0
  87. package/src/components/base/index.ts +21 -0
  88. package/src/components/composed/CkConversation/CkConversation.spec.ts +439 -0
  89. package/src/components/composed/CkConversation/CkConversation.stories.ts +642 -0
  90. package/src/components/composed/CkConversation/CkConversation.vue +146 -0
  91. package/src/components/composed/CkConversation/index.ts +2 -0
  92. package/src/components/composed/CkConversation/types.ts +37 -0
  93. package/src/components/composed/CkHistory/CkHistory.spec.ts +327 -0
  94. package/src/components/composed/CkHistory/CkHistory.stories.ts +373 -0
  95. package/src/components/composed/CkHistory/CkHistory.vue +126 -0
  96. package/src/components/composed/CkHistory/index.ts +2 -0
  97. package/src/components/composed/CkHistory/types.ts +72 -0
  98. package/src/components/composed/CkInput/CkInput.spec.ts +191 -0
  99. package/src/components/composed/CkInput/CkInput.stories.ts +438 -0
  100. package/src/components/composed/CkInput/CkInput.vue +104 -0
  101. package/src/components/composed/CkInput/index.ts +2 -0
  102. package/src/components/composed/CkInput/types.ts +60 -0
  103. package/src/components/composed/index.ts +5 -0
  104. package/src/components/index.ts +34 -0
  105. package/src/composables/index.ts +15 -0
  106. package/src/composables/types.ts +36 -0
  107. package/src/composables/useChat.ts +18 -0
  108. package/src/composables/useConversationSessions.spec.ts +86 -0
  109. package/src/composables/useConversationSessions.ts +85 -0
  110. package/src/composables/useConversations.spec.ts +557 -0
  111. package/src/composables/useConversations.ts +321 -0
  112. package/src/handlers/index.ts +9 -0
  113. package/src/handlers/useChatKitLabels.spec.ts +290 -0
  114. package/src/handlers/useChatKitLabels.ts +96 -0
  115. package/src/handlers/useMarkdownParser.spec.ts +112 -0
  116. package/src/handlers/useMarkdownParser.ts +47 -0
  117. package/src/handlers/useMessageHandler.spec.ts +535 -0
  118. package/src/handlers/useMessageHandler.ts +138 -0
  119. package/src/helpers/conversationTree.spec.ts +423 -0
  120. package/src/helpers/conversationTree.ts +117 -0
  121. package/src/helpers/generateId.ts +5 -0
  122. package/src/helpers/index.ts +4 -0
  123. package/src/helpers/secondsToDate.ts +7 -0
  124. package/src/helpers/toError.ts +8 -0
  125. package/src/index.ts +86 -0
  126. package/src/services/ChatService.ts +60 -0
  127. package/src/services/MockChatService.ts +47 -0
  128. package/src/services/index.ts +12 -0
  129. package/src/services/types.ts +92 -0
  130. package/src/test-setup.ts +12 -0
  131. package/src/test-utils/createTextPlugin.ts +9 -0
  132. package/src/types.ts +47 -0
  133. package/dist/generateId-BHf0W4Rq.js +0 -6
@@ -0,0 +1,382 @@
1
+ import CkMarkdownRenderer from './CkMarkdownRenderer.vue';
2
+ import type { Meta, StoryObj } from '@storybook/vue3-vite';
3
+
4
+ const meta: Meta<typeof CkMarkdownRenderer> = {
5
+ title: 'Base/CkMarkdownRenderer',
6
+ component: CkMarkdownRenderer,
7
+ tags: ['autodocs'],
8
+ argTypes: {
9
+ content: {
10
+ control: 'text',
11
+ description: 'Raw markdown string to render.',
12
+ },
13
+ classes: {
14
+ control: 'object',
15
+ description: 'Per-tag class overrides (e.g. `{ h1: "text-2xl" }`).',
16
+ },
17
+ plugins: {
18
+ control: false,
19
+ description: 'Additional `unified` plugins to extend Markdown parsing.',
20
+ },
21
+ },
22
+ };
23
+
24
+ export default meta;
25
+ type Story = StoryObj<typeof meta>;
26
+
27
+ const defaultTemplateSource = `<script setup lang="ts">
28
+ import { CkMarkdownRenderer } from '@aleph-alpha/chat-kit';
29
+ const content = 'Hello **world**! This is a *simple* paragraph with \`inline code\`.';
30
+ </script>
31
+
32
+ <template>
33
+ <CkMarkdownRenderer :content="content" />
34
+ </template>`;
35
+
36
+ const withCodeBlocksTemplateSource = `<script setup lang="ts">
37
+ import { CkMarkdownRenderer } from '@aleph-alpha/chat-kit';
38
+ const content = [
39
+ '# Code Example',
40
+ '',
41
+ '\`\`\`javascript',
42
+ 'function greet(name) {',
43
+ ' return \`Hello, \${name}!\`;',
44
+ '}',
45
+ '\`\`\`',
46
+ ].join('\\n');
47
+ </script>
48
+
49
+ <template>
50
+ <CkMarkdownRenderer :content="content" />
51
+ </template>`;
52
+
53
+ const withCustomCodeBlockSlotTemplateSource = `<script setup lang="ts">
54
+ import { CkMarkdownRenderer } from '@aleph-alpha/chat-kit';
55
+ const content = 'Example:\\n\\n\`\`\`typescript\\nconst x: number = 42;\\n\`\`\`';
56
+ </script>
57
+
58
+ <template>
59
+ <CkMarkdownRenderer :content="content">
60
+ <template #code-block="{ code, language }">
61
+ <div class="rounded-lg bg-black p-4 font-mono text-white">
62
+ <div class="mb-2 text-xs opacity-60">{{ language }}</div>
63
+ <code>{{ code }}</code>
64
+ </div>
65
+ </template>
66
+ </CkMarkdownRenderer>
67
+ </template>`;
68
+
69
+ const complexMarkdownTemplateSource = `<script setup lang="ts">
70
+ import { CkMarkdownRenderer } from '@aleph-alpha/chat-kit';
71
+ const content = \`# Project Overview
72
+
73
+ This document describes the **architecture** of the system.
74
+
75
+ ## Key Features
76
+
77
+ - Real-time processing
78
+ - Scalable infrastructure
79
+
80
+ ## Setup
81
+
82
+ \\\`\\\`\\\`bash
83
+ npm install && npm run build
84
+ \\\`\\\`\\\`
85
+
86
+ > **Note:** Restart the server after changing config.\`;
87
+ </script>
88
+
89
+ <template>
90
+ <CkMarkdownRenderer :content="content" />
91
+ </template>`;
92
+
93
+ const withCustomClassesTemplateSource = `<script setup lang="ts">
94
+ import { CkMarkdownRenderer } from '@aleph-alpha/chat-kit';
95
+ const content = '# Styled Heading\\n\\nA paragraph with **bold** text.';
96
+ const classes = {
97
+ h1: 'text-2xl font-bold mb-4',
98
+ p: 'text-base leading-7 mb-2',
99
+ strong: 'font-bold',
100
+ };
101
+ </script>
102
+
103
+ <template>
104
+ <CkMarkdownRenderer :content="content" :classes="classes" />
105
+ </template>`;
106
+
107
+ const gfmFeaturesTemplateSource = `<script setup lang="ts">
108
+ import { CkMarkdownRenderer } from '@aleph-alpha/chat-kit';
109
+ const content = [
110
+ '## Tables',
111
+ '| Feature | Status |',
112
+ '| --- | --- |',
113
+ '| Tables | Done |',
114
+ '',
115
+ '- [x] Add remark-gfm',
116
+ '- [ ] Ship to production',
117
+ ].join('\\n');
118
+ </script>
119
+
120
+ <template>
121
+ <CkMarkdownRenderer :content="content" />
122
+ </template>`;
123
+
124
+ const withCustomPluginTemplateSource = `<script setup lang="ts">
125
+ import { CkMarkdownRenderer } from '@aleph-alpha/chat-kit';
126
+
127
+ function visitTextNodes(node: any, transform: (value: string) => string): void {
128
+ if (node.type === 'text') node.value = transform(node.value);
129
+ if (node.children) node.children.forEach((c: any) => visitTextNodes(c, transform));
130
+ }
131
+
132
+ const uppercasePlugin = () => (tree: any) => visitTextNodes(tree, (v) => v.toUpperCase());
133
+
134
+ const content = '# Important Notice\\n\\nThis will be **uppercased**.';
135
+ const plugins = [uppercasePlugin];
136
+ </script>
137
+
138
+ <template>
139
+ <CkMarkdownRenderer :content="content" :plugins="plugins" />
140
+ </template>`;
141
+
142
+ /**
143
+ * Renders inline Markdown with bold, italic, and inline code. Use as the
144
+ * default way to display assistant prose replies.
145
+ */
146
+ export const Default: Story = {
147
+ args: {
148
+ content:
149
+ 'Hello **world**! This is a *simple* paragraph with `inline code`.',
150
+ },
151
+ parameters: { docs: { source: { code: defaultTemplateSource } } },
152
+ };
153
+
154
+ /**
155
+ * Renders fenced code blocks with language hints. Use when replies contain
156
+ * code samples and you rely on the default `<pre><code>` styling.
157
+ */
158
+ export const WithCodeBlocks: Story = {
159
+ args: {
160
+ content: [
161
+ '# Code Example',
162
+ '',
163
+ 'Here is a JavaScript snippet:',
164
+ '',
165
+ '```javascript',
166
+ 'function greet(name) {',
167
+ ' return `Hello, ${name}!`;',
168
+ '}',
169
+ '```',
170
+ '',
171
+ 'And a Python one:',
172
+ '',
173
+ '```python',
174
+ 'def greet(name: str) -> str:',
175
+ ' return f"Hello, {name}!"',
176
+ '```',
177
+ ].join('\n'),
178
+ },
179
+ parameters: { docs: { source: { code: withCodeBlocksTemplateSource } } },
180
+ };
181
+
182
+ /**
183
+ * Provides a custom `code-block` slot so you can integrate a syntax highlighter
184
+ * (Shiki, Prism, highlight.js) or render a copy button next to each block.
185
+ */
186
+ export const WithCustomCodeBlockSlot: Story = {
187
+ render: () => ({
188
+ components: { CkMarkdownRenderer },
189
+ setup() {
190
+ const content = [
191
+ 'Some text before the code.',
192
+ '',
193
+ '```typescript',
194
+ 'const x: number = 42;',
195
+ 'console.log(x);',
196
+ '```',
197
+ '',
198
+ 'Some text after the code.',
199
+ ].join('\n');
200
+
201
+ return { content };
202
+ },
203
+ template: `
204
+ <CkMarkdownRenderer :content="content">
205
+ <template #code-block="{ code, language }">
206
+ <div style="background: #1e1e1e; color: #d4d4d4; padding: 16px; border-radius: 8px; font-family: monospace; white-space: pre; margin: 8px 0;">
207
+ <div style="color: #888; font-size: 12px; margin-bottom: 8px;">{{ language }}</div>
208
+ <code>{{ code }}</code>
209
+ </div>
210
+ </template>
211
+ </CkMarkdownRenderer>
212
+ `,
213
+ }),
214
+ parameters: {
215
+ docs: { source: { code: withCustomCodeBlockSlotTemplateSource } },
216
+ },
217
+ };
218
+
219
+ /**
220
+ * End-to-end example with headings, lists, code blocks, blockquotes, and a
221
+ * horizontal rule. Use as a reference for typical long-form assistant output.
222
+ */
223
+ export const ComplexMarkdown: Story = {
224
+ args: {
225
+ content: [
226
+ '# Project Overview',
227
+ '',
228
+ 'This document describes the **architecture** of the system.',
229
+ '',
230
+ '## Key Features',
231
+ '',
232
+ '- Real-time processing',
233
+ '- Scalable infrastructure',
234
+ '- Modular design with *pluggable* components',
235
+ '',
236
+ '## Setup',
237
+ '',
238
+ 'Run the following command to install:',
239
+ '',
240
+ '```bash',
241
+ 'npm install && npm run build',
242
+ '```',
243
+ '',
244
+ '### Configuration',
245
+ '',
246
+ 'Edit `config.json` to set your preferences:',
247
+ '',
248
+ '```json',
249
+ '{',
250
+ ' "port": 3000,',
251
+ ' "debug": true',
252
+ '}',
253
+ '```',
254
+ '',
255
+ '> **Note:** Make sure to restart the server after changing configuration.',
256
+ '',
257
+ '---',
258
+ '',
259
+ 'For more details, visit the documentation.',
260
+ ].join('\n'),
261
+ },
262
+ parameters: { docs: { source: { code: complexMarkdownTemplateSource } } },
263
+ };
264
+
265
+ /**
266
+ * Overrides default tag styles via the `classes` prop. Use to apply design-system
267
+ * tokens/Tailwind classes without introducing a code-block slot.
268
+ */
269
+ export const WithCustomClasses: Story = {
270
+ render: () => ({
271
+ components: { CkMarkdownRenderer },
272
+ setup() {
273
+ const content = [
274
+ '# Styled Heading',
275
+ '',
276
+ 'A paragraph with **bold**, *italic*, and ~~deleted~~ text.',
277
+ '',
278
+ '- First item',
279
+ '- Second item',
280
+ '',
281
+ '| Feature | Status |',
282
+ '| --- | --- |',
283
+ '| Tables | Done |',
284
+ '| Styling | Done |',
285
+ '',
286
+ '> A blockquote for emphasis.',
287
+ '',
288
+ 'Visit https://example.com for details.',
289
+ ].join('\n');
290
+
291
+ const classes = {
292
+ h1: 'text-2xl font-bold mb-4',
293
+ p: 'text-base leading-7 mb-2',
294
+ strong: 'font-bold',
295
+ em: 'italic',
296
+ del: 'line-through opacity-50',
297
+ a: 'underline',
298
+ ul: 'list-disc pl-6 mb-2',
299
+ li: 'mb-1',
300
+ table: 'w-full border-collapse mb-4',
301
+ th: 'border p-2 font-bold text-left',
302
+ td: 'border p-2',
303
+ blockquote: 'border-l-4 pl-4 italic opacity-80',
304
+ };
305
+
306
+ return { content, classes };
307
+ },
308
+ template: '<CkMarkdownRenderer :content="content" :classes="classes" />',
309
+ }),
310
+ parameters: { docs: { source: { code: withCustomClassesTemplateSource } } },
311
+ };
312
+
313
+ /**
314
+ * Demonstrates GFM features (tables, task lists, strikethrough, autolinks).
315
+ * These are enabled by default because the renderer ships with `remark-gfm`.
316
+ */
317
+ export const GfmFeatures: Story = {
318
+ args: {
319
+ content: [
320
+ '## Tables',
321
+ '',
322
+ '| Feature | Status | Priority |',
323
+ '| --- | :---: | ---: |',
324
+ '| Tables | Done | High |',
325
+ '| Strikethrough | Done | Medium |',
326
+ '| Task lists | Done | Medium |',
327
+ '| Autolinks | Done | Low |',
328
+ '',
329
+ '## Strikethrough',
330
+ '',
331
+ 'This feature is ~~deprecated~~ and should not be used.',
332
+ '',
333
+ '## Task List',
334
+ '',
335
+ '- [x] Add remark-gfm',
336
+ '- [x] Write tests',
337
+ '- [ ] Ship to production',
338
+ '',
339
+ '## Autolinks',
340
+ '',
341
+ 'Check out https://example.com for more info.',
342
+ ].join('\n'),
343
+ },
344
+ parameters: { docs: { source: { code: gfmFeaturesTemplateSource } } },
345
+ };
346
+
347
+ function visitTextNodes(node: any, transform: (value: string) => string): void {
348
+ if (node.type === 'text') {
349
+ node.value = transform(node.value);
350
+ }
351
+ if (node.children) {
352
+ node.children.forEach((child: any) => visitTextNodes(child, transform));
353
+ }
354
+ }
355
+
356
+ const uppercasePlugin = () => (tree: any) => {
357
+ visitTextNodes(tree, (value) => value.toUpperCase());
358
+ };
359
+
360
+ /**
361
+ * Supplies a custom remark/unified plugin through the `plugins` prop. Use to
362
+ * transform the AST before rendering (e.g. math, directive, custom nodes).
363
+ */
364
+ export const WithCustomPlugin: Story = {
365
+ render: () => ({
366
+ components: { CkMarkdownRenderer },
367
+ setup() {
368
+ const content = [
369
+ '# Important Notice',
370
+ '',
371
+ 'This text will be **uppercased** by a custom remark plugin.',
372
+ '',
373
+ '- List items too',
374
+ '- Everything gets transformed',
375
+ ].join('\n');
376
+
377
+ return { content, plugins: [uppercasePlugin] };
378
+ },
379
+ template: '<CkMarkdownRenderer :content="content" :plugins="plugins" />',
380
+ }),
381
+ parameters: { docs: { source: { code: withCustomPluginTemplateSource } } },
382
+ };
@@ -0,0 +1,89 @@
1
+ <script lang="ts">
2
+ import {
3
+ computed,
4
+ defineComponent,
5
+ h,
6
+ toRefs,
7
+ type PropType,
8
+ type Slots,
9
+ type VNode,
10
+ } from 'vue';
11
+ import type { PluggableList } from 'unified';
12
+ import { useMarkdownParser } from '../../../handlers/useMarkdownParser';
13
+ import type { CkMarkdownElementClasses } from './types';
14
+ import { defaultMarkdownClasses } from './defaultMarkdownClasses';
15
+
16
+ function domToVNode(
17
+ node: Node,
18
+ slots: Slots,
19
+ classes: CkMarkdownElementClasses,
20
+ ): VNode | string | null {
21
+ if (node.nodeType === Node.TEXT_NODE) return node.textContent;
22
+ if (node.nodeType !== Node.ELEMENT_NODE) return null;
23
+
24
+ const el = node as Element;
25
+
26
+ if (el.tagName === 'PRE' && el.firstElementChild?.tagName === 'CODE') {
27
+ const codeEl = el.firstElementChild;
28
+ const language = codeEl.className?.match(/language-(\S+)/)?.[1] ?? '';
29
+ const code = codeEl.textContent ?? '';
30
+
31
+ if (slots['code-block']) {
32
+ return slots['code-block']({ code, language }) as unknown as VNode;
33
+ }
34
+ }
35
+
36
+ const children = Array.from(node.childNodes)
37
+ .map((child) => domToVNode(child, slots, classes))
38
+ .filter((c): c is VNode | string => c != null);
39
+
40
+ const attrs: Record<string, string> = {};
41
+ for (const attr of el.attributes) {
42
+ attrs[attr.name] = attr.value;
43
+ }
44
+
45
+ const tag = el.tagName.toLowerCase();
46
+ const customClass = classes[tag as keyof CkMarkdownElementClasses];
47
+ if (customClass) {
48
+ attrs.class = attrs.class ? `${attrs.class} ${customClass}` : customClass;
49
+ }
50
+
51
+ return h(tag, attrs, children);
52
+ }
53
+
54
+ export default defineComponent({
55
+ name: 'CkMarkdownRenderer',
56
+ props: {
57
+ content: { type: String, required: true },
58
+ classes: {
59
+ type: Object as PropType<CkMarkdownElementClasses>,
60
+ default: () => ({}),
61
+ },
62
+ plugins: { type: Array as PropType<PluggableList>, default: undefined },
63
+ },
64
+ setup(props, { slots }) {
65
+ const { content, plugins } = toRefs(props);
66
+ const { html } = useMarkdownParser(content, plugins);
67
+
68
+ const parsedNodes = computed(() => {
69
+ const raw = html.value;
70
+ if (!raw) return [];
71
+ const doc = new DOMParser().parseFromString(raw, 'text/html');
72
+ return Array.from(doc.body.childNodes);
73
+ });
74
+
75
+ const mergedClasses = computed<CkMarkdownElementClasses>(() => ({
76
+ ...defaultMarkdownClasses,
77
+ ...props.classes,
78
+ }));
79
+
80
+ return () => {
81
+ const elementClasses = mergedClasses.value;
82
+ const children = parsedNodes.value
83
+ .map((node) => domToVNode(node, slots, elementClasses))
84
+ .filter((c): c is VNode | string => c != null);
85
+ return h('div', { class: 'markdown-renderer' }, children);
86
+ };
87
+ },
88
+ });
89
+ </script>
@@ -0,0 +1,19 @@
1
+ import type { CkMarkdownElementClasses } from './types';
2
+
3
+ export const defaultMarkdownClasses: CkMarkdownElementClasses = {
4
+ p: 'my-1 leading-7',
5
+ a: 'underline underline-offset-4',
6
+ h1: 'mt-4 mb-2 text-2xl font-bold',
7
+ h2: 'mt-3 mb-2 text-xl font-semibold',
8
+ h3: 'mt-3 mb-1 text-lg font-semibold',
9
+ ul: 'my-1 ml-6 list-disc',
10
+ ol: 'my-1 ml-6 list-decimal',
11
+ li: 'my-0.5',
12
+ pre: 'my-2 overflow-x-auto rounded-lg p-4',
13
+ code: 'rounded px-1.5 py-0.5',
14
+ blockquote: 'my-2 border-l-2 pl-4 italic',
15
+ table: 'my-2 w-full border-collapse',
16
+ th: 'border p-2 text-left font-semibold',
17
+ td: 'border p-2',
18
+ hr: 'my-4',
19
+ };
@@ -0,0 +1,5 @@
1
+ export { default as CkMarkdownRenderer } from './CkMarkdownRenderer.vue';
2
+ export type {
3
+ CkMarkdownRendererProps,
4
+ CkMarkdownElementClasses,
5
+ } from './types';
@@ -0,0 +1,53 @@
1
+ import type { PluggableList } from 'unified';
2
+
3
+ export interface CkMarkdownElementClasses {
4
+ h1?: string;
5
+ h2?: string;
6
+ h3?: string;
7
+ h4?: string;
8
+ h5?: string;
9
+ h6?: string;
10
+ p?: string;
11
+ a?: string;
12
+ img?: string;
13
+ strong?: string;
14
+ em?: string;
15
+ del?: string;
16
+ blockquote?: string;
17
+ ul?: string;
18
+ ol?: string;
19
+ li?: string;
20
+ pre?: string;
21
+ code?: string;
22
+ hr?: string;
23
+ br?: string;
24
+ table?: string;
25
+ thead?: string;
26
+ tbody?: string;
27
+ tr?: string;
28
+ th?: string;
29
+ td?: string;
30
+ input?: string;
31
+ }
32
+
33
+ /**
34
+ * Renders a Markdown string as Vue VNodes with per-tag class overrides and an
35
+ * optional `code-block` slot. Use for assistant messages that contain prose,
36
+ * lists, tables, and code blocks.
37
+ *
38
+ * @category Data Display
39
+ * @useCases render assistant markdown reply, syntax-highlighted code blocks, GFM tables, chat message body
40
+ * @keywords markdown, renderer, gfm, remark, code block, prose
41
+ * @related CkAgentMessage, CkConversation
42
+ */
43
+ export interface CkMarkdownRendererProps {
44
+ /** Raw Markdown string to render. */
45
+ content: string;
46
+ /**
47
+ * Per-tag class overrides. Provide Tailwind (or any) classes to style
48
+ * generated HTML elements (e.g. `h1`, `p`, `ul`).
49
+ */
50
+ classes?: CkMarkdownElementClasses;
51
+ /** Extra [unified](https://unifiedjs.com/) plugins to extend parsing. */
52
+ plugins?: PluggableList;
53
+ }
@@ -0,0 +1,40 @@
1
+ import CkMessageActionButton from './CkMessageActionButton.vue';
2
+ import { render, screen } from '@testing-library/vue';
3
+ import { describe, expect, it } from 'vitest';
4
+
5
+ describe('CkMessageActionButton', () => {
6
+ it('renders slot content inside the button', () => {
7
+ render(CkMessageActionButton, {
8
+ props: { ariaLabel: 'Action' },
9
+ slots: { default: '<svg data-testid="icon"></svg>' },
10
+ });
11
+
12
+ expect(screen.getByTestId('icon')).toBeInTheDocument();
13
+ });
14
+
15
+ it('sets the accessible label on the button', () => {
16
+ render(CkMessageActionButton, {
17
+ props: { ariaLabel: 'Copy message' },
18
+ });
19
+
20
+ expect(
21
+ screen.getByRole('button', { name: 'Copy message' }),
22
+ ).toBeInTheDocument();
23
+ });
24
+
25
+ it('passes disabled state to the button', () => {
26
+ render(CkMessageActionButton, {
27
+ props: { ariaLabel: 'Action', disabled: true },
28
+ });
29
+
30
+ expect(screen.getByRole('button', { name: 'Action' })).toBeDisabled();
31
+ });
32
+
33
+ it('defaults to enabled', () => {
34
+ render(CkMessageActionButton, {
35
+ props: { ariaLabel: 'Action' },
36
+ });
37
+
38
+ expect(screen.getByRole('button', { name: 'Action' })).toBeEnabled();
39
+ });
40
+ });
@@ -0,0 +1,56 @@
1
+ import CkMessageActionButton from './CkMessageActionButton.vue';
2
+ import type { Meta, StoryObj } from '@storybook/vue3-vite';
3
+ import { UiIcon } from '@aleph-alpha/ui-library';
4
+
5
+ const meta: Meta<typeof CkMessageActionButton> = {
6
+ title: 'Base/CkMessageActionButton',
7
+ component: CkMessageActionButton,
8
+ tags: ['autodocs'],
9
+ argTypes: {
10
+ ariaLabel: {
11
+ control: 'text',
12
+ description:
13
+ 'Accessible label for the button. Required because the button is icon-only.',
14
+ },
15
+ disabled: {
16
+ control: 'boolean',
17
+ description: 'Whether the button is disabled and ignores clicks.',
18
+ },
19
+ },
20
+ decorators: [
21
+ () => ({
22
+ template:
23
+ '<div style="max-width: 640px; margin: 0 auto; padding: 24px;"><story /></div>',
24
+ }),
25
+ ],
26
+ };
27
+
28
+ export default meta;
29
+ type Story = StoryObj<typeof meta>;
30
+
31
+ const defaultTemplateSource = `<script setup lang="ts">
32
+ import { CkMessageActionButton } from '@aleph-alpha/chat-kit';
33
+ import { UiIcon } from '@aleph-alpha/ui-library';
34
+ </script>
35
+
36
+ <template>
37
+ <CkMessageActionButton ariaLabel="Copy message">
38
+ <UiIcon name="copy" :size="16" aria-hidden="true" />
39
+ </CkMessageActionButton>
40
+ </template>`;
41
+
42
+ /**
43
+ * Generic icon-only action button used in message footers. Supply any icon
44
+ * in the default slot and a descriptive `ariaLabel` for screen readers.
45
+ */
46
+ export const Default: Story = {
47
+ render: () => ({
48
+ components: { CkMessageActionButton, UiIcon },
49
+ template: `
50
+ <CkMessageActionButton ariaLabel="Copy message">
51
+ <UiIcon name="copy" :size="16" aria-hidden="true" />
52
+ </CkMessageActionButton>
53
+ `,
54
+ }),
55
+ parameters: { docs: { source: { code: defaultTemplateSource } } },
56
+ };
@@ -0,0 +1,23 @@
1
+ <script setup lang="ts">
2
+ import type { CkMessageActionButtonProps } from './types';
3
+ import { UiButton } from '@aleph-alpha/ui-library';
4
+
5
+ defineOptions({
6
+ name: 'CkMessageActionButton',
7
+ });
8
+
9
+ withDefaults(defineProps<CkMessageActionButtonProps>(), {
10
+ disabled: false,
11
+ });
12
+ </script>
13
+
14
+ <template>
15
+ <UiButton
16
+ v-bind="{ ariaLabel }"
17
+ :disabled="disabled"
18
+ variant="ghost"
19
+ size="icon-sm"
20
+ >
21
+ <slot />
22
+ </UiButton>
23
+ </template>
@@ -0,0 +1,2 @@
1
+ export { default as CkMessageActionButton } from './CkMessageActionButton.vue';
2
+ export type * from './types';
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Small icon-only action button used in the footer of chat messages. Acts as
3
+ * a ghost-styled wrapper around `UiButton` with consistent chat-kit tokens.
4
+ *
5
+ * @category Actions
6
+ * @useCases message footer action, like/dislike reaction, share button on reply, secondary message action
7
+ * @keywords icon button, action, message, ghost, accessible, aria-label
8
+ * @related CkCopyMessageActionButton, CkAgentMessage, CkConversation
9
+ */
10
+ export interface CkMessageActionButtonProps {
11
+ /**
12
+ * Accessible label for the button. **Required** because the button only
13
+ * renders an icon.
14
+ */
15
+ ariaLabel: string;
16
+ /**
17
+ * Whether the button is disabled and ignores clicks.
18
+ * @default false
19
+ */
20
+ disabled?: boolean;
21
+ }