@aleph-alpha/chat-kit 5.13.0 → 6.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 (199) hide show
  1. package/dist/{CkHistory.vue_vue_type_script_setup_true_lang-BNcJURkq.js → CkHistory.vue_vue_type_script_setup_true_lang-Cr8vF94e.js} +221 -115
  2. package/dist/adapters/responses-api.d.ts +1 -1
  3. package/dist/adapters/responses-api.d.ts.map +1 -1
  4. package/dist/adapters/responses-api.js +9 -7
  5. package/dist/apl-fqmucPXA.js +140 -0
  6. package/dist/asciiarmor-DucZyvP0.js +56 -0
  7. package/dist/asn1-BnOEsgAm.js +144 -0
  8. package/dist/asterisk-QAlztEwS.js +345 -0
  9. package/dist/brainfuck-DZVCuF_t.js +53 -0
  10. package/dist/chat-kit.css +1177 -12
  11. package/dist/clike-CTYGlS6x.js +799 -0
  12. package/dist/clojure-BhXMqnxz.js +849 -0
  13. package/dist/cmake-BGaNd9E7.js +71 -0
  14. package/dist/cobol-4yqQntpt.js +120 -0
  15. package/dist/coffeescript-D2dXvhEc.js +308 -0
  16. package/dist/commonlisp-CF_VNHQR.js +130 -0
  17. package/dist/components/base/CkHistoryItem/CkHistoryItem.vue.d.ts.map +1 -1
  18. package/dist/components/base/CkHistoryWrapper/CkHistoryWrapper.vue.d.ts.map +1 -1
  19. package/dist/components/base/CkMarkdownRenderer/CkMarkdownRenderer.stories.d.ts.map +1 -1
  20. package/dist/components/base/CkMilkdownEditor/CkMilkdownEditor.stories.d.ts +41 -0
  21. package/dist/components/base/CkMilkdownEditor/CkMilkdownEditor.stories.d.ts.map +1 -0
  22. package/dist/components/base/CkMilkdownEditor/CkMilkdownEditor.vue.d.ts +26 -0
  23. package/dist/components/base/CkMilkdownEditor/CkMilkdownEditor.vue.d.ts.map +1 -0
  24. package/dist/components/base/CkMilkdownEditor/codeMirrorTheme.d.ts +20 -0
  25. package/dist/components/base/CkMilkdownEditor/codeMirrorTheme.d.ts.map +1 -0
  26. package/dist/components/base/CkMilkdownEditor/createCrepeEditor.d.ts +42 -0
  27. package/dist/components/base/CkMilkdownEditor/createCrepeEditor.d.ts.map +1 -0
  28. package/dist/components/base/CkMilkdownEditor/crepeIcons.d.ts +33 -0
  29. package/dist/components/base/CkMilkdownEditor/crepeIcons.d.ts.map +1 -0
  30. package/dist/components/base/CkMilkdownEditor/index.d.ts +3 -0
  31. package/dist/components/base/CkMilkdownEditor/index.d.ts.map +1 -0
  32. package/dist/components/base/CkMilkdownEditor/markdownClassAttrs.d.ts +16 -0
  33. package/dist/components/base/CkMilkdownEditor/markdownClassAttrs.d.ts.map +1 -0
  34. package/dist/components/base/CkMilkdownEditor/types.d.ts +98 -0
  35. package/dist/components/base/CkMilkdownEditor/types.d.ts.map +1 -0
  36. package/dist/components/base/index.d.ts +2 -0
  37. package/dist/components/base/index.d.ts.map +1 -1
  38. package/dist/components/index.d.ts +2 -2
  39. package/dist/components/index.d.ts.map +1 -1
  40. package/dist/components/index.js +12 -11
  41. package/dist/composables/fileUpload/useFileUpload.d.ts.map +1 -1
  42. package/dist/composables/index.js +1 -1
  43. package/dist/composables/useChat.d.ts.map +1 -1
  44. package/dist/composables/useConversations.d.ts.map +1 -1
  45. package/dist/createCrepeEditor-CFbvSH3s.js +74141 -0
  46. package/dist/crystal-DyuLTqLs.js +398 -0
  47. package/dist/css-DkSyTW67.js +1772 -0
  48. package/dist/cypher-Dlu_3r4V.js +121 -0
  49. package/dist/d-UURgV0Ux.js +179 -0
  50. package/dist/diff-B_Bi2Crb.js +25 -0
  51. package/dist/dockerfile-Bvk733Ga.js +201 -0
  52. package/dist/dtd-Dy74G54E.js +114 -0
  53. package/dist/dylan-TSb-Nfix.js +314 -0
  54. package/dist/ebnf-4fKAGW3a.js +140 -0
  55. package/dist/ecl-B59qGGVg.js +178 -0
  56. package/dist/eiffel-Dze7nlu3.js +134 -0
  57. package/dist/elm-DG7jkhNZ.js +176 -0
  58. package/dist/erlang-BO6gOnGA.js +674 -0
  59. package/dist/factor-CMxFHDqz.js +65 -0
  60. package/dist/fcl-CDDUNjTj.js +141 -0
  61. package/dist/forth-B9D2JCeE.js +116 -0
  62. package/dist/fortran-CAG2BFbe.js +467 -0
  63. package/dist/gas-0Aw8zDr5.js +293 -0
  64. package/dist/gherkin-DhZlEZiy.js +115 -0
  65. package/dist/groovy-CpwJiBl7.js +223 -0
  66. package/dist/handlers/index.js +1 -1
  67. package/dist/handlers/useChatKitLabels.d.ts +62 -1
  68. package/dist/handlers/useChatKitLabels.d.ts.map +1 -1
  69. package/dist/haskell-ySd-OUo8.js +459 -0
  70. package/dist/haxe-7MlzfeYV.js +514 -0
  71. package/dist/http-BqypyemW.js +79 -0
  72. package/dist/idl-4HIGJlDI.js +985 -0
  73. package/dist/index-A5rWjRtZ.js +689 -0
  74. package/dist/index-B0ldhzaj.js +385 -0
  75. package/dist/index-BVOjyn3y.js +2490 -0
  76. package/dist/index-BWvkps_G.js +309 -0
  77. package/dist/index-BaS8OW8V.js +151 -0
  78. package/dist/index-BlHHEbV-.js +406 -0
  79. package/dist/index-BoqzHVNB.js +82 -0
  80. package/dist/index-C6zYjwIQ.js +704 -0
  81. package/dist/{index-Dn4ctxk8.js → index-C72e57-5.js} +48 -24
  82. package/dist/index-CMJGuXap.js +330 -0
  83. package/dist/index-CRfGWVdQ.js +116 -0
  84. package/dist/index-C_gTb50u.js +81 -0
  85. package/dist/index-DSU7cBGz.js +178 -0
  86. package/dist/index-DSq3gvqU.js +1767 -0
  87. package/dist/index-Da3777l2.js +289 -0
  88. package/dist/index-DdlzabmR.js +1021 -0
  89. package/dist/index-DhY_9bdS.js +61 -0
  90. package/dist/index-Dm6C1blN.js +1052 -0
  91. package/dist/index-Tj3pzx-E.js +327 -0
  92. package/dist/index-ZeRS5rER.js +97 -0
  93. package/dist/index-gzleljO6.js +157 -0
  94. package/dist/index-naWlQpaL.js +642 -0
  95. package/dist/index-sKO1lHHC.js +312 -0
  96. package/dist/index.d.ts +2 -2
  97. package/dist/index.d.ts.map +1 -1
  98. package/dist/index.js +15 -14
  99. package/dist/javascript-Bt8B7yTi.js +992 -0
  100. package/dist/julia-Bs6JJhYG.js +407 -0
  101. package/dist/livescript-DmzgM3Yt.js +296 -0
  102. package/dist/lua-Cvg8VrAA.js +256 -0
  103. package/dist/mathematica-DNLOL9PQ.js +110 -0
  104. package/dist/mbox-Ga7d4MMN.js +117 -0
  105. package/dist/mirc-Dma3B8rS.js +107 -0
  106. package/dist/mllike-DHn7xckP.js +334 -0
  107. package/dist/modelica-0d55jYY0.js +147 -0
  108. package/dist/mscgen-DdqZYINH.js +135 -0
  109. package/dist/mumps-Btr8VblO.js +93 -0
  110. package/dist/nginx-DTDtBDVN.js +141 -0
  111. package/dist/nsis-CyKBdPWp.js +62 -0
  112. package/dist/ntriples-CvgOYMpL.js +153 -0
  113. package/dist/octave-DYBj3-tl.js +200 -0
  114. package/dist/oz-R_e8WMIi.js +231 -0
  115. package/dist/pascal-GD8iposT.js +105 -0
  116. package/dist/perl-DL9mHpoi.js +1105 -0
  117. package/dist/pig-C_4T4YIV.js +101 -0
  118. package/dist/powershell-B0suO7Vd.js +328 -0
  119. package/dist/properties-BR-vP1aU.js +58 -0
  120. package/dist/protobuf-BxgpyhoW.js +77 -0
  121. package/dist/pug-vHnWR0UE.js +405 -0
  122. package/dist/puppet-Bdao66PW.js +137 -0
  123. package/dist/python-BFGRmuZ9.js +426 -0
  124. package/dist/q-CrbCVq4a.js +131 -0
  125. package/dist/r-V7nswm59.js +170 -0
  126. package/dist/rpm-C-DLY-If.js +109 -0
  127. package/dist/ruby-CQTpZw6f.js +331 -0
  128. package/dist/sas-D2UG-yhZ.js +207 -0
  129. package/dist/scheme-BKzrkGJD.js +222 -0
  130. package/dist/shell-BlsXDxCn.js +222 -0
  131. package/dist/sieve-CjwBwOY5.js +135 -0
  132. package/dist/simple-mode-DMneyfDu.js +130 -0
  133. package/dist/smalltalk-BOIGQuhN.js +121 -0
  134. package/dist/solr-CwD7U71z.js +69 -0
  135. package/dist/sparql-DYskk2vE.js +249 -0
  136. package/dist/spreadsheet-Bgtt3oLP.js +87 -0
  137. package/dist/sql-Cei9CMfk.js +342 -0
  138. package/dist/stex-C1nZSzAw.js +230 -0
  139. package/dist/stylus-BkS-boTH.js +565 -0
  140. package/dist/swift-FRZi1uvB.js +291 -0
  141. package/dist/tcl-CUcaCdmq.js +114 -0
  142. package/dist/test-utils/createTextPlugin.d.ts +7 -1
  143. package/dist/test-utils/createTextPlugin.d.ts.map +1 -1
  144. package/dist/textile-BnFpjsrl.js +414 -0
  145. package/dist/tiddlywiki-CjprD-Qp.js +218 -0
  146. package/dist/tiki-DK9DOeWn.js +268 -0
  147. package/dist/toml-BOuWGMcf.js +76 -0
  148. package/dist/troff-E1bJ0PPL.js +61 -0
  149. package/dist/ttcn-cfg-Dc39-fIP.js +133 -0
  150. package/dist/ttcn-tKd4HLu4.js +192 -0
  151. package/dist/turtle-Dq7-1WAf.js +124 -0
  152. package/dist/{useChatKitLabels-CMOr3Kjq.js → useChatKitLabels-Ww1Q7xUr.js} +26 -3
  153. package/dist/vb-Dp90gtsv.js +196 -0
  154. package/dist/vbscript-Bfn8O8I7.js +478 -0
  155. package/dist/velocity-BwIZK1TH.js +149 -0
  156. package/dist/verilog-CnT9bMk0.js +429 -0
  157. package/dist/vhdl-DCkMIyT9.js +158 -0
  158. package/dist/webidl-BTLTThCm.js +204 -0
  159. package/dist/xquery-BrBUuxMR.js +525 -0
  160. package/dist/yacas-b5lAVEIl.js +130 -0
  161. package/dist/z80-ClgwfNdB.js +92 -0
  162. package/package.json +11 -5
  163. package/src/adapters/responses-api.spec.ts +86 -0
  164. package/src/adapters/responses-api.ts +13 -9
  165. package/src/components/base/CkCitationCard/CkCitationCard.vue +1 -1
  166. package/src/components/base/CkCitationLabel/CkCitationLabel.vue +1 -1
  167. package/src/components/base/CkConversationLayout/CkConversationLayout.spec.ts +1 -5
  168. package/src/components/base/CkCopyMessageButton/CkCopyMessageButton.spec.ts +1 -1
  169. package/src/components/base/CkFeedbackButton/CkFeedbackButton.vue +3 -3
  170. package/src/components/base/CkFeedbackDialog/CkFeedbackDialog.vue +9 -9
  171. package/src/components/base/CkHistoryItem/CkHistoryItem.vue +5 -7
  172. package/src/components/base/CkHistoryWrapper/CkHistoryWrapper.vue +2 -1
  173. package/src/components/base/CkMarkdownRenderer/CkMarkdownRenderer.stories.ts +9 -4
  174. package/src/components/base/CkMarkdownRenderer/CkMarkdownRenderer.vue +1 -1
  175. package/src/components/base/CkMilkdownEditor/CkMilkdownEditor.spec.ts +221 -0
  176. package/src/components/base/CkMilkdownEditor/CkMilkdownEditor.stories.ts +274 -0
  177. package/src/components/base/CkMilkdownEditor/CkMilkdownEditor.vue +151 -0
  178. package/src/components/base/CkMilkdownEditor/codeMirrorTheme.ts +132 -0
  179. package/src/components/base/CkMilkdownEditor/createCrepeEditor.ts +192 -0
  180. package/src/components/base/CkMilkdownEditor/crepeIcons.ts +77 -0
  181. package/src/components/base/CkMilkdownEditor/index.ts +2 -0
  182. package/src/components/base/CkMilkdownEditor/markdownClassAttrs.ts +64 -0
  183. package/src/components/base/CkMilkdownEditor/milkdown-theme.css +441 -0
  184. package/src/components/base/CkMilkdownEditor/types.ts +100 -0
  185. package/src/components/base/CkModelSelector/CkModelSelector.vue +3 -3
  186. package/src/components/base/CkPromptInputDisclaimer/CkPromptInputDisclaimer.stories.ts +2 -2
  187. package/src/components/base/index.ts +6 -0
  188. package/src/components/index.ts +4 -0
  189. package/src/composables/fileUpload/useFileUpload.ts +45 -30
  190. package/src/composables/fileUpload/useFileUploadWrapper.spec.ts +1 -1
  191. package/src/composables/fileUpload/useFileUploadWrapper.ts +3 -3
  192. package/src/composables/useChat.ts +3 -1
  193. package/src/composables/useConversations.ts +8 -5
  194. package/src/handlers/useChatKitLabels.spec.ts +0 -1
  195. package/src/handlers/useChatKitLabels.ts +29 -2
  196. package/src/handlers/useMarkdownParser.ts +1 -1
  197. package/src/index.ts +4 -0
  198. package/src/test-setup.ts +3 -3
  199. package/src/test-utils/createTextPlugin.ts +11 -3
@@ -0,0 +1,221 @@
1
+ import { render, waitFor } from '@testing-library/vue';
2
+ import { insert } from '@milkdown/kit/utils';
3
+ import CkMilkdownEditor from './CkMilkdownEditor.vue';
4
+ import { defaultMarkdownClasses } from '../CkMarkdownRenderer/defaultMarkdownClasses';
5
+ import { applyMarkdownClasses, type AttrCtx } from './markdownClassAttrs';
6
+ import { createCrepeEditor } from './createCrepeEditor';
7
+ import { defaultChatKitLabels } from '../../../handlers/useChatKitLabels';
8
+
9
+ // ProseMirror measures selections and positions during mount; jsdom lacks the
10
+ // layout APIs it calls. Stubs are scoped to this spec (not `test-setup.ts`) so
11
+ // other specs keep running against an unmodified jsdom.
12
+ const EMPTY_RECT = {
13
+ x: 0,
14
+ y: 0,
15
+ top: 0,
16
+ left: 0,
17
+ right: 0,
18
+ bottom: 0,
19
+ width: 0,
20
+ height: 0,
21
+ toJSON: () => ({}),
22
+ } as DOMRect;
23
+
24
+ beforeAll(() => {
25
+ Range.prototype.getBoundingClientRect = () => EMPTY_RECT;
26
+ Range.prototype.getClientRects = () =>
27
+ ({
28
+ length: 0,
29
+ item: () => null,
30
+ *[Symbol.iterator]() {
31
+ // no rects
32
+ },
33
+ }) as unknown as DOMRectList;
34
+ Element.prototype.scrollIntoView ??= () => undefined;
35
+ document.elementFromPoint ??= () => null;
36
+ });
37
+
38
+ async function renderEditor(
39
+ props: InstanceType<typeof CkMilkdownEditor>['$props'] = {},
40
+ ) {
41
+ const utils = render(CkMilkdownEditor, { props });
42
+ // The editor loads lazily; the ready class is set once Crepe has created the
43
+ // view and the initial props have been applied.
44
+ await waitFor(() =>
45
+ expect(
46
+ utils.container.querySelector('.ck-milkdown--ready .ProseMirror'),
47
+ ).toBeInTheDocument(),
48
+ );
49
+ return utils;
50
+ }
51
+
52
+ describe('CkMilkdownEditor', () => {
53
+ it('renders the initial markdown as editable DOM', async () => {
54
+ const { container } = await renderEditor({
55
+ modelValue: '# Title\n\nSome *text*\n\n- one\n- two',
56
+ });
57
+
58
+ expect(container.querySelector('h1')?.textContent).toBe('Title');
59
+ expect(container.querySelector('em')?.textContent).toBe('text');
60
+ expect(container.querySelectorAll('li')).toHaveLength(2);
61
+ });
62
+
63
+ it('applies the CkMarkdownRenderer default classes to the nodes', async () => {
64
+ const { container } = await renderEditor({
65
+ modelValue:
66
+ '# Title\n\nA paragraph with `code` and a [link](https://a.b).',
67
+ });
68
+
69
+ expect(container.querySelector('h1')?.className).toContain(
70
+ defaultMarkdownClasses.h1,
71
+ );
72
+ expect(container.querySelector('p')?.className).toContain(
73
+ defaultMarkdownClasses.p,
74
+ );
75
+ expect(container.querySelector('code')?.className).toContain(
76
+ defaultMarkdownClasses.code,
77
+ );
78
+ expect(container.querySelector('a')?.className).toContain(
79
+ defaultMarkdownClasses.a,
80
+ );
81
+ });
82
+
83
+ it('lets `classes` override a tag while keeping the other defaults', async () => {
84
+ const { container } = await renderEditor({
85
+ modelValue: '# Title\n\nParagraph',
86
+ classes: { h1: 'custom-h1' },
87
+ });
88
+
89
+ const h1 = container.querySelector('h1');
90
+ expect(h1?.className).toContain('custom-h1');
91
+ expect(h1?.className).not.toContain(defaultMarkdownClasses.h1);
92
+ expect(container.querySelector('p')?.className).toContain(
93
+ defaultMarkdownClasses.p,
94
+ );
95
+ });
96
+
97
+ it('applies an external modelValue change without echoing it back', async () => {
98
+ const { container, rerender, emitted } = await renderEditor({
99
+ modelValue: '# One',
100
+ });
101
+
102
+ await rerender({ modelValue: '## Two' });
103
+
104
+ await waitFor(() =>
105
+ expect(container.querySelector('h2')?.textContent).toBe('Two'),
106
+ );
107
+ expect(container.querySelector('h1')).toBeNull();
108
+
109
+ // Give Milkdown's debounced listener time to fire, then make sure the value
110
+ // we applied was not emitted back to the parent.
111
+ await new Promise((resolve) => setTimeout(resolve, 300));
112
+ const updates = (emitted()['update:modelValue'] ?? []) as string[][];
113
+ expect(updates.map(([value]) => value)).not.toContain('## Two');
114
+ });
115
+
116
+ it('toggles contenteditable with `readonly`', async () => {
117
+ const { container, rerender } = await renderEditor({
118
+ modelValue: 'Hello',
119
+ readonly: true,
120
+ });
121
+
122
+ const pm = () => container.querySelector('.ProseMirror');
123
+ expect(pm()?.getAttribute('contenteditable')).toBe('false');
124
+
125
+ await rerender({ modelValue: 'Hello', readonly: false });
126
+ await waitFor(() =>
127
+ expect(pm()?.getAttribute('contenteditable')).toBe('true'),
128
+ );
129
+ });
130
+
131
+ it('marks the root disabled and locks editing with `disabled`', async () => {
132
+ const { container } = await renderEditor({
133
+ modelValue: 'Hello',
134
+ disabled: true,
135
+ });
136
+
137
+ expect(
138
+ container.querySelector('.ck-milkdown')?.getAttribute('aria-disabled'),
139
+ ).toBe('true');
140
+ expect(
141
+ container.querySelector('.ProseMirror')?.getAttribute('contenteditable'),
142
+ ).toBe('false');
143
+ });
144
+
145
+ it('exposes the editable region as a labelled textbox', async () => {
146
+ const { container } = await renderEditor({
147
+ modelValue: 'Hello',
148
+ ariaLabel: 'Reply editor',
149
+ });
150
+
151
+ const textbox = container.querySelector('[role="textbox"]');
152
+ expect(textbox).toBe(container.querySelector('.ProseMirror'));
153
+ expect(textbox?.getAttribute('aria-label')).toBe('Reply editor');
154
+ expect(textbox?.getAttribute('aria-multiline')).toBe('true');
155
+ });
156
+
157
+ it('does not throw when unmounted before or after the editor is created', async () => {
158
+ const early = render(CkMilkdownEditor, { props: { modelValue: 'x' } });
159
+ expect(() => early.unmount()).not.toThrow();
160
+
161
+ const late = await renderEditor({ modelValue: 'y' });
162
+ expect(() => late.unmount()).not.toThrow();
163
+ });
164
+ });
165
+
166
+ describe('createCrepeEditor', () => {
167
+ it('reports document changes through onMarkdownUpdated', async () => {
168
+ const root = document.createElement('div');
169
+ document.body.append(root);
170
+ const onMarkdownUpdated = vi.fn();
171
+
172
+ const handle = await createCrepeEditor({
173
+ root,
174
+ defaultValue: 'Hello',
175
+ classes: defaultMarkdownClasses,
176
+ labels: defaultChatKitLabels,
177
+ onMarkdownUpdated,
178
+ });
179
+
180
+ expect(handle.getMarkdown().trim()).toBe('Hello');
181
+
182
+ handle.crepe.editor.action(insert(' world', true));
183
+
184
+ await waitFor(() => expect(onMarkdownUpdated).toHaveBeenCalled());
185
+ expect(onMarkdownUpdated.mock.lastCall?.[0]).toContain('world');
186
+
187
+ handle.destroy();
188
+ root.remove();
189
+ });
190
+ });
191
+
192
+ describe('applyMarkdownClasses', () => {
193
+ function captureHooks(classes: Parameters<typeof applyMarkdownClasses>[1]) {
194
+ const hooks = new Map<unknown, (node: unknown) => unknown>();
195
+ const ctx = {
196
+ set: vi.fn((key: unknown, value: unknown) => {
197
+ hooks.set(key, value as (node: unknown) => unknown);
198
+ }),
199
+ } as unknown as AttrCtx;
200
+ applyMarkdownClasses(ctx, classes);
201
+ return { ctx, hooks: [...hooks.values()] };
202
+ }
203
+
204
+ it('maps heading levels to the matching h* class', () => {
205
+ const { hooks } = captureHooks({ h1: 'one', h2: 'two' });
206
+ const results = hooks.map((hook) => hook({ attrs: { level: 2 } }));
207
+ expect(results).toContainEqual({ class: 'two' });
208
+ expect(results).not.toContainEqual({ class: 'one' });
209
+ });
210
+
211
+ it('returns no attributes for tags without a class', () => {
212
+ const { hooks } = captureHooks({});
213
+ for (const hook of hooks) {
214
+ const result = hook({ attrs: { level: 1 } }) as Record<string, unknown>;
215
+ // The code block hook returns per-element maps; every other hook returns
216
+ // the element's attributes directly. Either way nothing should be set.
217
+ const values = 'pre' in result ? Object.values(result) : [result];
218
+ for (const value of values) expect(value).toEqual({});
219
+ }
220
+ });
221
+ });
@@ -0,0 +1,274 @@
1
+ import { ref } from 'vue';
2
+ import { UiCodeBlock } from '@aleph-alpha/ui-library';
3
+ import type { Meta, StoryObj } from '@storybook/vue3-vite';
4
+ import CkMilkdownEditor from './CkMilkdownEditor.vue';
5
+ import CkMarkdownRenderer from '../CkMarkdownRenderer/CkMarkdownRenderer.vue';
6
+
7
+ const meta: Meta<typeof CkMilkdownEditor> = {
8
+ title: 'Base/CkMilkdownEditor',
9
+ component: CkMilkdownEditor,
10
+ tags: ['autodocs'],
11
+ argTypes: {
12
+ modelValue: {
13
+ control: 'text',
14
+ description: 'Markdown source of the document (`v-model`).',
15
+ },
16
+ classes: {
17
+ control: 'object',
18
+ description:
19
+ 'Per-tag class overrides, merged over the same defaults `CkMarkdownRenderer` uses.',
20
+ },
21
+ placeholder: {
22
+ control: 'text',
23
+ description: 'Shown when the document is empty. Read once at mount.',
24
+ },
25
+ readonly: { control: 'boolean' },
26
+ disabled: { control: 'boolean' },
27
+ autofocus: { control: 'boolean' },
28
+ },
29
+ };
30
+
31
+ export default meta;
32
+ type Story = StoryObj<typeof meta>;
33
+
34
+ const richContent = [
35
+ '# Release notes',
36
+ '',
37
+ 'This build ships **bold** changes, a few *italic* remarks, ~~dropped~~ ideas and some `inline code`. Read the [documentation](https://example.com) for details.',
38
+ '',
39
+ '## Highlights',
40
+ '',
41
+ '- Faster streaming',
42
+ '- Smaller bundles',
43
+ ' - Lazy-loaded editor',
44
+ ' - Tree-shaken icons',
45
+ '',
46
+ '1. Install',
47
+ '2. Configure',
48
+ '3. Ship',
49
+ '',
50
+ '- [x] Write the code',
51
+ '- [ ] Write the docs',
52
+ '',
53
+ '> **Note:** the editor and the renderer share one class map.',
54
+ '',
55
+ '---',
56
+ '',
57
+ '```typescript',
58
+ 'export function greet(name: string): string {',
59
+ ' const message = `Hello, ${name}!`; // template literal',
60
+ ' return message.toUpperCase();',
61
+ '}',
62
+ '```',
63
+ ].join('\n');
64
+
65
+ const defaultTemplateSource = `<script setup lang="ts">
66
+ import { ref } from 'vue';
67
+ import { CkMilkdownEditor } from '@aleph-alpha/chat-kit';
68
+
69
+ const markdown = ref('');
70
+ </script>
71
+
72
+ <template>
73
+ <CkMilkdownEditor
74
+ v-model="markdown"
75
+ placeholder="Write your reply…"
76
+ aria-label="Reply editor"
77
+ />
78
+ </template>`;
79
+
80
+ const withInitialContentTemplateSource = `<script setup lang="ts">
81
+ import { ref } from 'vue';
82
+ import { CkMilkdownEditor } from '@aleph-alpha/chat-kit';
83
+
84
+ const markdown = ref(\`# Release notes
85
+
86
+ This build ships **bold** changes and some \\\`inline code\\\`.
87
+
88
+ - Faster streaming
89
+ - Smaller bundles
90
+
91
+ \\\`\\\`\\\`typescript
92
+ export function greet(name: string): string {
93
+ return \\\`Hello, \\\${name}!\\\`;
94
+ }
95
+ \\\`\\\`\\\`\`);
96
+ </script>
97
+
98
+ <template>
99
+ <CkMilkdownEditor v-model="markdown" aria-label="Release notes" />
100
+ </template>`;
101
+
102
+ const readonlyTemplateSource = `<script setup lang="ts">
103
+ import { CkMilkdownEditor } from '@aleph-alpha/chat-kit';
104
+
105
+ const markdown = '# Read me\\n\\nThis document **cannot** be edited.';
106
+ </script>
107
+
108
+ <template>
109
+ <CkMilkdownEditor :model-value="markdown" readonly aria-label="Read-only document" />
110
+ </template>`;
111
+
112
+ const disabledTemplateSource = `<script setup lang="ts">
113
+ import { CkMilkdownEditor } from '@aleph-alpha/chat-kit';
114
+
115
+ const markdown = '# Unavailable\\n\\nThis editor is currently disabled.';
116
+ </script>
117
+
118
+ <template>
119
+ <CkMilkdownEditor
120
+ :model-value="markdown"
121
+ disabled
122
+ aria-label="Disabled document"
123
+ />
124
+ </template>`;
125
+
126
+ const customClassesTemplateSource = `<script setup lang="ts">
127
+ import { ref } from 'vue';
128
+ import { CkMilkdownEditor } from '@aleph-alpha/chat-kit';
129
+
130
+ const markdown = ref('# Styled heading\\n\\nA paragraph with **bold** text.');
131
+ const classes = {
132
+ h1: 'mt-6 mb-3 text-3xl font-black tracking-tight',
133
+ p: 'my-2 leading-8',
134
+ };
135
+ </script>
136
+
137
+ <template>
138
+ <CkMilkdownEditor v-model="markdown" :classes="classes" aria-label="Styled editor" />
139
+ </template>`;
140
+
141
+ const sideBySideTemplateSource = `<script setup lang="ts">
142
+ import { ref } from 'vue';
143
+ import { CkMarkdownRenderer, CkMilkdownEditor } from '@aleph-alpha/chat-kit';
144
+ import { UiCodeBlock } from '@aleph-alpha/ui-library';
145
+
146
+ const markdown = ref('# Hello\\n\\nEdit me on the left, watch the right.');
147
+ </script>
148
+
149
+ <template>
150
+ <div class="text-content-on-surface-primary grid gap-6 text-base leading-7 md:grid-cols-2">
151
+ <CkMilkdownEditor v-model="markdown" aria-label="Editor" />
152
+ <CkMarkdownRenderer :content="markdown">
153
+ <template #code-block="{ code, language }">
154
+ <UiCodeBlock :code="code" :language="language || 'text'" />
155
+ </template>
156
+ </CkMarkdownRenderer>
157
+ </div>
158
+ </template>`;
159
+
160
+ /**
161
+ * Empty editor with a placeholder. Select text to reveal the floating toolbar
162
+ * (bold, italic, strikethrough, inline code, link); type `- `, `1. `, `> ` or
163
+ * a fenced ``` block to create lists, quotes and code.
164
+ */
165
+ export const Default: Story = {
166
+ render: (args) => ({
167
+ components: { CkMilkdownEditor },
168
+ setup() {
169
+ const markdown = ref(args.modelValue ?? '');
170
+ return { args, markdown };
171
+ },
172
+ template: `
173
+ <CkMilkdownEditor
174
+ v-bind="args"
175
+ v-model="markdown"
176
+ placeholder="Write your reply…"
177
+ aria-label="Reply editor"
178
+ />
179
+ `,
180
+ }),
181
+ args: { modelValue: '' },
182
+ parameters: { docs: { source: { code: defaultTemplateSource } } },
183
+ };
184
+
185
+ /**
186
+ * A document exercising every enabled node: headings, inline marks, links,
187
+ * nested / ordered / task lists, a blockquote, a rule and a highlighted code
188
+ * block. Compare with `CkMarkdownRenderer`'s stories — the spacing and type
189
+ * scale are identical because both use the same class map.
190
+ */
191
+ export const WithInitialContent: Story = {
192
+ render: (args) => ({
193
+ components: { CkMilkdownEditor },
194
+ setup() {
195
+ const markdown = ref(args.modelValue ?? '');
196
+ return { args, markdown };
197
+ },
198
+ template: `<CkMilkdownEditor v-bind="args" v-model="markdown" aria-label="Release notes" />`,
199
+ }),
200
+ args: { modelValue: richContent },
201
+ parameters: { docs: { source: { code: withInitialContentTemplateSource } } },
202
+ };
203
+
204
+ /**
205
+ * `readonly` renders the document without a caret or toolbar. Use it to show
206
+ * a message in the editor's layout while a request is in flight.
207
+ */
208
+ export const Readonly: Story = {
209
+ args: { modelValue: richContent, readonly: true },
210
+ parameters: { docs: { source: { code: readonlyTemplateSource } } },
211
+ };
212
+
213
+ /**
214
+ * `disabled` also mutes the content and blocks pointer interaction, and sets
215
+ * `aria-disabled` on the root.
216
+ */
217
+ export const Disabled: Story = {
218
+ args: {
219
+ modelValue: '# Unavailable\n\nThis editor is currently disabled.',
220
+ disabled: true,
221
+ },
222
+ parameters: { docs: { source: { code: disabledTemplateSource } } },
223
+ };
224
+
225
+ /**
226
+ * `classes` overrides individual tags while the rest keeps the defaults — the
227
+ * same contract as `CkMarkdownRenderer`'s `classes` prop, so one object can
228
+ * drive both components.
229
+ */
230
+ export const CustomClasses: Story = {
231
+ render: (args) => ({
232
+ components: { CkMilkdownEditor },
233
+ setup() {
234
+ const markdown = ref(args.modelValue ?? '');
235
+ return { args, markdown };
236
+ },
237
+ template: `<CkMilkdownEditor v-bind="args" v-model="markdown" aria-label="Styled editor" />`,
238
+ }),
239
+ args: {
240
+ modelValue: '# Styled heading\n\nA paragraph with **bold** text.',
241
+ classes: {
242
+ h1: 'mt-6 mb-3 text-3xl font-black tracking-tight',
243
+ p: 'my-2 leading-8',
244
+ },
245
+ },
246
+ parameters: { docs: { source: { code: customClassesTemplateSource } } },
247
+ };
248
+
249
+ /**
250
+ * Live parity check: the editor on the left drives `CkMarkdownRenderer` on the
251
+ * right through one `v-model`. Toggle the theme toolbar to compare light and
252
+ * dark, including the code block against `UiCodeBlock`.
253
+ */
254
+ export const SideBySideWithRenderer: Story = {
255
+ render: (args) => ({
256
+ components: { CkMilkdownEditor, CkMarkdownRenderer, UiCodeBlock },
257
+ setup() {
258
+ const markdown = ref(args.modelValue ?? '');
259
+ return { args, markdown };
260
+ },
261
+ template: `
262
+ <div class="text-content-on-surface-primary grid gap-6 text-base leading-7 md:grid-cols-2">
263
+ <CkMilkdownEditor v-bind="args" v-model="markdown" aria-label="Editor" />
264
+ <CkMarkdownRenderer :content="markdown">
265
+ <template #code-block="{ code, language }">
266
+ <UiCodeBlock :code="code" :language="language || 'text'" />
267
+ </template>
268
+ </CkMarkdownRenderer>
269
+ </div>
270
+ `,
271
+ }),
272
+ args: { modelValue: richContent },
273
+ parameters: { docs: { source: { code: sideBySideTemplateSource } } },
274
+ };
@@ -0,0 +1,151 @@
1
+ <script setup lang="ts">
2
+ import {
3
+ computed,
4
+ onBeforeUnmount,
5
+ onMounted,
6
+ ref,
7
+ useTemplateRef,
8
+ watch,
9
+ } from 'vue';
10
+ import { useChatKitLabels } from '../../../handlers/useChatKitLabels';
11
+ import { defaultMarkdownClasses } from '../CkMarkdownRenderer/defaultMarkdownClasses';
12
+ import type { CrepeEditorHandle } from './createCrepeEditor';
13
+ import type { CkMilkdownEditorEmits, CkMilkdownEditorProps } from './types';
14
+
15
+ defineOptions({ name: 'CkMilkdownEditor' });
16
+
17
+ const props = withDefaults(defineProps<CkMilkdownEditorProps>(), {
18
+ modelValue: '',
19
+ placeholder: undefined,
20
+ readonly: false,
21
+ disabled: false,
22
+ ariaLabel: undefined,
23
+ classes: () => ({}),
24
+ autofocus: false,
25
+ });
26
+
27
+ const emit = defineEmits<CkMilkdownEditorEmits>();
28
+
29
+ const labels = useChatKitLabels();
30
+ const rootRef = useTemplateRef<HTMLDivElement>('rootRef');
31
+ const isLocked = computed(() => props.readonly || props.disabled);
32
+ // Flips once Crepe has finished creating the view; exposed as a root class so
33
+ // consumers (and tests) can tell the lazy-loaded editor apart from its shell.
34
+ const isReady = ref(false);
35
+
36
+ let editor: CrepeEditorHandle | undefined;
37
+ let destroyed = false;
38
+ // The last markdown we either emitted or applied from the prop. Comparing
39
+ // against it (rather than a boolean "applying" flag) is what makes the echo
40
+ // guard immune to Milkdown's ~200 ms debounce on `markdownUpdated`.
41
+ let lastKnownMarkdown = props.modelValue;
42
+
43
+ function handleMarkdownUpdated(markdown: string): void {
44
+ if (markdown === lastKnownMarkdown) return;
45
+ lastKnownMarkdown = markdown;
46
+ emit('update:modelValue', markdown);
47
+ }
48
+
49
+ onMounted(async () => {
50
+ const root = rootRef.value;
51
+ if (!root) return;
52
+
53
+ // Lazy: Milkdown, ProseMirror and CodeMirror only load once an editor is
54
+ // actually mounted, keeping them out of consumers' initial bundle.
55
+ const { createCrepeEditor } = await import('./createCrepeEditor');
56
+ if (destroyed) return;
57
+
58
+ const initialMarkdown = props.modelValue;
59
+ lastKnownMarkdown = initialMarkdown;
60
+ const handle = await createCrepeEditor({
61
+ root,
62
+ defaultValue: initialMarkdown,
63
+ placeholder: props.placeholder,
64
+ ariaLabel: props.ariaLabel,
65
+ readonly: isLocked.value,
66
+ classes: { ...defaultMarkdownClasses, ...props.classes },
67
+ labels: labels.value,
68
+ onMarkdownUpdated: handleMarkdownUpdated,
69
+ });
70
+
71
+ // Unmounted while `create()` was pending.
72
+ if (destroyed) {
73
+ handle.destroy();
74
+ return;
75
+ }
76
+
77
+ editor = handle;
78
+ // Props may have moved on while the editor was being created; the watchers
79
+ // below could not apply them yet, so catch up here.
80
+ if (props.modelValue !== initialMarkdown)
81
+ handle.setMarkdown(props.modelValue);
82
+ handle.setReadonly(isLocked.value);
83
+ isReady.value = true;
84
+ if (props.autofocus) handle.focus();
85
+ });
86
+
87
+ watch(
88
+ () => props.modelValue,
89
+ (next) => {
90
+ if (next === lastKnownMarkdown) return;
91
+ lastKnownMarkdown = next;
92
+ editor?.setMarkdown(next);
93
+ },
94
+ );
95
+
96
+ watch(isLocked, (locked) => {
97
+ editor?.setReadonly(locked);
98
+ });
99
+
100
+ onBeforeUnmount(() => {
101
+ destroyed = true;
102
+ editor?.destroy();
103
+ editor = undefined;
104
+ });
105
+
106
+ function isInsideRoot(target: EventTarget | null): boolean {
107
+ return target instanceof Node && (rootRef.value?.contains(target) ?? false);
108
+ }
109
+
110
+ // The floating toolbar and the link tooltip live inside the root, so moving
111
+ // focus between them and the text must not count as leaving the editor.
112
+ function onFocusIn(event: FocusEvent): void {
113
+ if (!isInsideRoot(event.relatedTarget)) emit('focus');
114
+ }
115
+
116
+ function onFocusOut(event: FocusEvent): void {
117
+ if (!isInsideRoot(event.relatedTarget)) emit('blur');
118
+ }
119
+
120
+ function focus(): void {
121
+ editor?.focus();
122
+ }
123
+
124
+ function getMarkdown(): string {
125
+ return editor?.getMarkdown() ?? props.modelValue;
126
+ }
127
+
128
+ defineExpose({ focus, getMarkdown });
129
+ </script>
130
+
131
+ <template>
132
+ <div
133
+ ref="rootRef"
134
+ :class="[
135
+ 'ck-milkdown min-w-0 break-words',
136
+ isReady && 'ck-milkdown--ready',
137
+ disabled && 'ck-milkdown--disabled',
138
+ ]"
139
+ :aria-disabled="disabled || undefined"
140
+ @focusin="onFocusIn"
141
+ @focusout="onFocusOut"
142
+ />
143
+ </template>
144
+
145
+ <style scoped>
146
+ .ck-milkdown--disabled {
147
+ opacity: 0.6;
148
+ cursor: not-allowed;
149
+ pointer-events: none;
150
+ }
151
+ </style>