@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.
- package/dist/{CkHistory.vue_vue_type_script_setup_true_lang-BNcJURkq.js → CkHistory.vue_vue_type_script_setup_true_lang-Cr8vF94e.js} +221 -115
- package/dist/adapters/responses-api.d.ts +1 -1
- package/dist/adapters/responses-api.d.ts.map +1 -1
- package/dist/adapters/responses-api.js +9 -7
- package/dist/apl-fqmucPXA.js +140 -0
- package/dist/asciiarmor-DucZyvP0.js +56 -0
- package/dist/asn1-BnOEsgAm.js +144 -0
- package/dist/asterisk-QAlztEwS.js +345 -0
- package/dist/brainfuck-DZVCuF_t.js +53 -0
- package/dist/chat-kit.css +1177 -12
- package/dist/clike-CTYGlS6x.js +799 -0
- package/dist/clojure-BhXMqnxz.js +849 -0
- package/dist/cmake-BGaNd9E7.js +71 -0
- package/dist/cobol-4yqQntpt.js +120 -0
- package/dist/coffeescript-D2dXvhEc.js +308 -0
- package/dist/commonlisp-CF_VNHQR.js +130 -0
- package/dist/components/base/CkHistoryItem/CkHistoryItem.vue.d.ts.map +1 -1
- package/dist/components/base/CkHistoryWrapper/CkHistoryWrapper.vue.d.ts.map +1 -1
- package/dist/components/base/CkMarkdownRenderer/CkMarkdownRenderer.stories.d.ts.map +1 -1
- package/dist/components/base/CkMilkdownEditor/CkMilkdownEditor.stories.d.ts +41 -0
- package/dist/components/base/CkMilkdownEditor/CkMilkdownEditor.stories.d.ts.map +1 -0
- package/dist/components/base/CkMilkdownEditor/CkMilkdownEditor.vue.d.ts +26 -0
- package/dist/components/base/CkMilkdownEditor/CkMilkdownEditor.vue.d.ts.map +1 -0
- package/dist/components/base/CkMilkdownEditor/codeMirrorTheme.d.ts +20 -0
- package/dist/components/base/CkMilkdownEditor/codeMirrorTheme.d.ts.map +1 -0
- package/dist/components/base/CkMilkdownEditor/createCrepeEditor.d.ts +42 -0
- package/dist/components/base/CkMilkdownEditor/createCrepeEditor.d.ts.map +1 -0
- package/dist/components/base/CkMilkdownEditor/crepeIcons.d.ts +33 -0
- package/dist/components/base/CkMilkdownEditor/crepeIcons.d.ts.map +1 -0
- package/dist/components/base/CkMilkdownEditor/index.d.ts +3 -0
- package/dist/components/base/CkMilkdownEditor/index.d.ts.map +1 -0
- package/dist/components/base/CkMilkdownEditor/markdownClassAttrs.d.ts +16 -0
- package/dist/components/base/CkMilkdownEditor/markdownClassAttrs.d.ts.map +1 -0
- package/dist/components/base/CkMilkdownEditor/types.d.ts +98 -0
- package/dist/components/base/CkMilkdownEditor/types.d.ts.map +1 -0
- package/dist/components/base/index.d.ts +2 -0
- package/dist/components/base/index.d.ts.map +1 -1
- package/dist/components/index.d.ts +2 -2
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/index.js +12 -11
- package/dist/composables/fileUpload/useFileUpload.d.ts.map +1 -1
- package/dist/composables/index.js +1 -1
- package/dist/composables/useChat.d.ts.map +1 -1
- package/dist/composables/useConversations.d.ts.map +1 -1
- package/dist/createCrepeEditor-CFbvSH3s.js +74141 -0
- package/dist/crystal-DyuLTqLs.js +398 -0
- package/dist/css-DkSyTW67.js +1772 -0
- package/dist/cypher-Dlu_3r4V.js +121 -0
- package/dist/d-UURgV0Ux.js +179 -0
- package/dist/diff-B_Bi2Crb.js +25 -0
- package/dist/dockerfile-Bvk733Ga.js +201 -0
- package/dist/dtd-Dy74G54E.js +114 -0
- package/dist/dylan-TSb-Nfix.js +314 -0
- package/dist/ebnf-4fKAGW3a.js +140 -0
- package/dist/ecl-B59qGGVg.js +178 -0
- package/dist/eiffel-Dze7nlu3.js +134 -0
- package/dist/elm-DG7jkhNZ.js +176 -0
- package/dist/erlang-BO6gOnGA.js +674 -0
- package/dist/factor-CMxFHDqz.js +65 -0
- package/dist/fcl-CDDUNjTj.js +141 -0
- package/dist/forth-B9D2JCeE.js +116 -0
- package/dist/fortran-CAG2BFbe.js +467 -0
- package/dist/gas-0Aw8zDr5.js +293 -0
- package/dist/gherkin-DhZlEZiy.js +115 -0
- package/dist/groovy-CpwJiBl7.js +223 -0
- package/dist/handlers/index.js +1 -1
- package/dist/handlers/useChatKitLabels.d.ts +62 -1
- package/dist/handlers/useChatKitLabels.d.ts.map +1 -1
- package/dist/haskell-ySd-OUo8.js +459 -0
- package/dist/haxe-7MlzfeYV.js +514 -0
- package/dist/http-BqypyemW.js +79 -0
- package/dist/idl-4HIGJlDI.js +985 -0
- package/dist/index-A5rWjRtZ.js +689 -0
- package/dist/index-B0ldhzaj.js +385 -0
- package/dist/index-BVOjyn3y.js +2490 -0
- package/dist/index-BWvkps_G.js +309 -0
- package/dist/index-BaS8OW8V.js +151 -0
- package/dist/index-BlHHEbV-.js +406 -0
- package/dist/index-BoqzHVNB.js +82 -0
- package/dist/index-C6zYjwIQ.js +704 -0
- package/dist/{index-Dn4ctxk8.js → index-C72e57-5.js} +48 -24
- package/dist/index-CMJGuXap.js +330 -0
- package/dist/index-CRfGWVdQ.js +116 -0
- package/dist/index-C_gTb50u.js +81 -0
- package/dist/index-DSU7cBGz.js +178 -0
- package/dist/index-DSq3gvqU.js +1767 -0
- package/dist/index-Da3777l2.js +289 -0
- package/dist/index-DdlzabmR.js +1021 -0
- package/dist/index-DhY_9bdS.js +61 -0
- package/dist/index-Dm6C1blN.js +1052 -0
- package/dist/index-Tj3pzx-E.js +327 -0
- package/dist/index-ZeRS5rER.js +97 -0
- package/dist/index-gzleljO6.js +157 -0
- package/dist/index-naWlQpaL.js +642 -0
- package/dist/index-sKO1lHHC.js +312 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +15 -14
- package/dist/javascript-Bt8B7yTi.js +992 -0
- package/dist/julia-Bs6JJhYG.js +407 -0
- package/dist/livescript-DmzgM3Yt.js +296 -0
- package/dist/lua-Cvg8VrAA.js +256 -0
- package/dist/mathematica-DNLOL9PQ.js +110 -0
- package/dist/mbox-Ga7d4MMN.js +117 -0
- package/dist/mirc-Dma3B8rS.js +107 -0
- package/dist/mllike-DHn7xckP.js +334 -0
- package/dist/modelica-0d55jYY0.js +147 -0
- package/dist/mscgen-DdqZYINH.js +135 -0
- package/dist/mumps-Btr8VblO.js +93 -0
- package/dist/nginx-DTDtBDVN.js +141 -0
- package/dist/nsis-CyKBdPWp.js +62 -0
- package/dist/ntriples-CvgOYMpL.js +153 -0
- package/dist/octave-DYBj3-tl.js +200 -0
- package/dist/oz-R_e8WMIi.js +231 -0
- package/dist/pascal-GD8iposT.js +105 -0
- package/dist/perl-DL9mHpoi.js +1105 -0
- package/dist/pig-C_4T4YIV.js +101 -0
- package/dist/powershell-B0suO7Vd.js +328 -0
- package/dist/properties-BR-vP1aU.js +58 -0
- package/dist/protobuf-BxgpyhoW.js +77 -0
- package/dist/pug-vHnWR0UE.js +405 -0
- package/dist/puppet-Bdao66PW.js +137 -0
- package/dist/python-BFGRmuZ9.js +426 -0
- package/dist/q-CrbCVq4a.js +131 -0
- package/dist/r-V7nswm59.js +170 -0
- package/dist/rpm-C-DLY-If.js +109 -0
- package/dist/ruby-CQTpZw6f.js +331 -0
- package/dist/sas-D2UG-yhZ.js +207 -0
- package/dist/scheme-BKzrkGJD.js +222 -0
- package/dist/shell-BlsXDxCn.js +222 -0
- package/dist/sieve-CjwBwOY5.js +135 -0
- package/dist/simple-mode-DMneyfDu.js +130 -0
- package/dist/smalltalk-BOIGQuhN.js +121 -0
- package/dist/solr-CwD7U71z.js +69 -0
- package/dist/sparql-DYskk2vE.js +249 -0
- package/dist/spreadsheet-Bgtt3oLP.js +87 -0
- package/dist/sql-Cei9CMfk.js +342 -0
- package/dist/stex-C1nZSzAw.js +230 -0
- package/dist/stylus-BkS-boTH.js +565 -0
- package/dist/swift-FRZi1uvB.js +291 -0
- package/dist/tcl-CUcaCdmq.js +114 -0
- package/dist/test-utils/createTextPlugin.d.ts +7 -1
- package/dist/test-utils/createTextPlugin.d.ts.map +1 -1
- package/dist/textile-BnFpjsrl.js +414 -0
- package/dist/tiddlywiki-CjprD-Qp.js +218 -0
- package/dist/tiki-DK9DOeWn.js +268 -0
- package/dist/toml-BOuWGMcf.js +76 -0
- package/dist/troff-E1bJ0PPL.js +61 -0
- package/dist/ttcn-cfg-Dc39-fIP.js +133 -0
- package/dist/ttcn-tKd4HLu4.js +192 -0
- package/dist/turtle-Dq7-1WAf.js +124 -0
- package/dist/{useChatKitLabels-CMOr3Kjq.js → useChatKitLabels-Ww1Q7xUr.js} +26 -3
- package/dist/vb-Dp90gtsv.js +196 -0
- package/dist/vbscript-Bfn8O8I7.js +478 -0
- package/dist/velocity-BwIZK1TH.js +149 -0
- package/dist/verilog-CnT9bMk0.js +429 -0
- package/dist/vhdl-DCkMIyT9.js +158 -0
- package/dist/webidl-BTLTThCm.js +204 -0
- package/dist/xquery-BrBUuxMR.js +525 -0
- package/dist/yacas-b5lAVEIl.js +130 -0
- package/dist/z80-ClgwfNdB.js +92 -0
- package/package.json +11 -5
- package/src/adapters/responses-api.spec.ts +86 -0
- package/src/adapters/responses-api.ts +13 -9
- package/src/components/base/CkCitationCard/CkCitationCard.vue +1 -1
- package/src/components/base/CkCitationLabel/CkCitationLabel.vue +1 -1
- package/src/components/base/CkConversationLayout/CkConversationLayout.spec.ts +1 -5
- package/src/components/base/CkCopyMessageButton/CkCopyMessageButton.spec.ts +1 -1
- package/src/components/base/CkFeedbackButton/CkFeedbackButton.vue +3 -3
- package/src/components/base/CkFeedbackDialog/CkFeedbackDialog.vue +9 -9
- package/src/components/base/CkHistoryItem/CkHistoryItem.vue +5 -7
- package/src/components/base/CkHistoryWrapper/CkHistoryWrapper.vue +2 -1
- package/src/components/base/CkMarkdownRenderer/CkMarkdownRenderer.stories.ts +9 -4
- package/src/components/base/CkMarkdownRenderer/CkMarkdownRenderer.vue +1 -1
- package/src/components/base/CkMilkdownEditor/CkMilkdownEditor.spec.ts +221 -0
- package/src/components/base/CkMilkdownEditor/CkMilkdownEditor.stories.ts +274 -0
- package/src/components/base/CkMilkdownEditor/CkMilkdownEditor.vue +151 -0
- package/src/components/base/CkMilkdownEditor/codeMirrorTheme.ts +132 -0
- package/src/components/base/CkMilkdownEditor/createCrepeEditor.ts +192 -0
- package/src/components/base/CkMilkdownEditor/crepeIcons.ts +77 -0
- package/src/components/base/CkMilkdownEditor/index.ts +2 -0
- package/src/components/base/CkMilkdownEditor/markdownClassAttrs.ts +64 -0
- package/src/components/base/CkMilkdownEditor/milkdown-theme.css +441 -0
- package/src/components/base/CkMilkdownEditor/types.ts +100 -0
- package/src/components/base/CkModelSelector/CkModelSelector.vue +3 -3
- package/src/components/base/CkPromptInputDisclaimer/CkPromptInputDisclaimer.stories.ts +2 -2
- package/src/components/base/index.ts +6 -0
- package/src/components/index.ts +4 -0
- package/src/composables/fileUpload/useFileUpload.ts +45 -30
- package/src/composables/fileUpload/useFileUploadWrapper.spec.ts +1 -1
- package/src/composables/fileUpload/useFileUploadWrapper.ts +3 -3
- package/src/composables/useChat.ts +3 -1
- package/src/composables/useConversations.ts +8 -5
- package/src/handlers/useChatKitLabels.spec.ts +0 -1
- package/src/handlers/useChatKitLabels.ts +29 -2
- package/src/handlers/useMarkdownParser.ts +1 -1
- package/src/index.ts +4 -0
- package/src/test-setup.ts +3 -3
- 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>
|