@aleph-alpha/chat-kit 6.2.0 → 6.4.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-DPCpIxWc.js → CkHistory.vue_vue_type_script_setup_true_lang-B0WUzTHa.js} +351 -164
- package/dist/chat-kit.css +111 -28
- package/dist/code-palette-DH_G7mZ5.js +21599 -0
- package/dist/components/base/CkCodeEditor/CkCodeEditor.stories.d.ts +49 -0
- package/dist/components/base/CkCodeEditor/CkCodeEditor.stories.d.ts.map +1 -0
- package/dist/components/base/CkCodeEditor/CkCodeEditor.vue.d.ts +40 -0
- package/dist/components/base/CkCodeEditor/CkCodeEditor.vue.d.ts.map +1 -0
- package/dist/components/base/CkCodeEditor/badgeDecoration.d.ts +47 -0
- package/dist/components/base/CkCodeEditor/badgeDecoration.d.ts.map +1 -0
- package/dist/components/base/CkCodeEditor/createCodeMirror.d.ts +48 -0
- package/dist/components/base/CkCodeEditor/createCodeMirror.d.ts.map +1 -0
- package/dist/components/base/CkCodeEditor/index.d.ts +3 -0
- package/dist/components/base/CkCodeEditor/index.d.ts.map +1 -0
- package/dist/components/base/CkCodeEditor/types.d.ts +149 -0
- package/dist/components/base/CkCodeEditor/types.d.ts.map +1 -0
- package/dist/components/base/CkMilkdownEditor/CkMilkdownEditor.stories.d.ts +6 -0
- package/dist/components/base/CkMilkdownEditor/CkMilkdownEditor.stories.d.ts.map +1 -1
- package/dist/components/base/CkMilkdownEditor/CkMilkdownEditor.vue.d.ts +1 -0
- package/dist/components/base/CkMilkdownEditor/CkMilkdownEditor.vue.d.ts.map +1 -1
- package/dist/components/base/CkMilkdownEditor/createCrepeEditor.d.ts +1 -0
- package/dist/components/base/CkMilkdownEditor/createCrepeEditor.d.ts.map +1 -1
- package/dist/components/base/CkMilkdownEditor/types.d.ts +8 -0
- package/dist/components/base/CkMilkdownEditor/types.d.ts.map +1 -1
- 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 +27 -26
- package/dist/createCodeMirror-5iswYetS.js +169 -0
- package/dist/{createCrepeEditor-C9seqnzm.js → createCrepeEditor-C-AQ1w_e.js} +1433 -24739
- package/dist/handlers/index.js +1 -1
- package/dist/handlers/useChatKitLabels.d.ts +6 -0
- package/dist/handlers/useChatKitLabels.d.ts.map +1 -1
- package/dist/{components/base/CkMilkdownEditor/codeMirrorTheme.d.ts → helpers/codemirror/theme.d.ts} +1 -1
- package/dist/helpers/codemirror/theme.d.ts.map +1 -0
- package/dist/{index-DO8xk00f.js → index-ABUMl7H3.js} +3 -3
- package/dist/{index-DfvAv_Y2.js → index-BlIc9r0r.js} +3 -2
- package/dist/{index-VbqLf82s.js → index-BqWO6mvz.js} +4 -4
- package/dist/{index-Bxg0Q_mk.js → index-CFGO7LRz.js} +2 -2
- package/dist/index-CG8e3146.js +1825 -0
- package/dist/{index-C90_LSx6.js → index-CIF4qQzR.js} +4 -4
- package/dist/{index-DXTRMb09.js → index-CR2kGrpY.js} +3 -3
- package/dist/{index-B0nlcqqF.js → index-CWp_t8Wl.js} +2 -2
- package/dist/{index-CHtY7ztn.js → index-D0IZW-Up.js} +2 -2
- package/dist/{index-By7WdPbk.js → index-DKwEe_Tu.js} +4 -4
- package/dist/{index-DINJn4Mn.js → index-DLjsqQHx.js} +3 -3
- package/dist/{index-eZunhxNH.js → index-DPgw0Cf_.js} +2 -2
- package/dist/{index-D85elIIr.js → index-Db5-PJG-.js} +2 -2
- package/dist/{index-CFdcEeXx.js → index-Dbi75pKH.js} +3 -2
- package/dist/{index-8mYiDHFC.js → index-DfLCrnVq.js} +2 -2
- package/dist/{index-6PiPEjJd.js → index-GpSpJ3ti.js} +3 -2
- package/dist/{index-reQfKGuJ.js → index-Mh3WUU-6.js} +1 -1
- package/dist/{index-DLZcs4H8.js → index-QAs789O5.js} +2 -2
- package/dist/{index-DzpUblMx.js → index-S4ylc8JK.js} +3 -3
- package/dist/{index-DbVRgXxM.js → index-XZeturb3.js} +3 -3
- package/dist/{index-U4LYutvz.js → index-g2X42srv.js} +3 -2
- package/dist/{index-DNxEOsDD.js → index-hEJ_Bavx.js} +2 -2
- package/dist/{index-C5UC1wP7.js → index-hbS5PDHj.js} +3 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +29 -28
- package/dist/{useChatKitLabels-Bz58tas9.js → useChatKitLabels-CatQSpfL.js} +2 -0
- package/package.json +4 -2
- package/src/components/base/CkCodeEditor/CkCodeEditor.spec.ts +284 -0
- package/src/components/base/CkCodeEditor/CkCodeEditor.stories.ts +405 -0
- package/src/components/base/CkCodeEditor/CkCodeEditor.vue +244 -0
- package/src/components/base/CkCodeEditor/badgeDecoration.ts +173 -0
- package/src/components/base/CkCodeEditor/createCodeMirror.ts +197 -0
- package/src/components/base/CkCodeEditor/index.ts +2 -0
- package/src/components/base/CkCodeEditor/types.ts +154 -0
- package/src/components/base/CkMilkdownEditor/CkMilkdownEditor.spec.ts +13 -0
- package/src/components/base/CkMilkdownEditor/CkMilkdownEditor.stories.ts +34 -0
- package/src/components/base/CkMilkdownEditor/CkMilkdownEditor.vue +3 -1
- package/src/components/base/CkMilkdownEditor/createCrepeEditor.ts +2 -1
- package/src/components/base/CkMilkdownEditor/milkdown-theme.css +39 -26
- package/src/components/base/CkMilkdownEditor/types.ts +8 -0
- package/src/components/base/index.ts +6 -0
- package/src/components/index.ts +4 -0
- package/src/handlers/useChatKitLabels.ts +6 -0
- package/src/helpers/codemirror/code-palette.css +49 -0
- package/src/{components/base/CkMilkdownEditor/codeMirrorTheme.ts → helpers/codemirror/theme.ts} +23 -12
- package/src/index.ts +4 -0
- package/dist/components/base/CkMilkdownEditor/codeMirrorTheme.d.ts.map +0 -1
|
@@ -0,0 +1,405 @@
|
|
|
1
|
+
import { ref } from 'vue';
|
|
2
|
+
import { UiCodeBlock } from '@aleph-alpha/ui-library';
|
|
3
|
+
import type { Meta, StoryObj } from '@storybook/vue3-vite';
|
|
4
|
+
import CkCodeEditor from './CkCodeEditor.vue';
|
|
5
|
+
import CkMarkdownRenderer from '../CkMarkdownRenderer/CkMarkdownRenderer.vue';
|
|
6
|
+
|
|
7
|
+
const folderIcon =
|
|
8
|
+
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" ' +
|
|
9
|
+
'stroke="currentColor" stroke-width="2" stroke-linecap="round" ' +
|
|
10
|
+
'stroke-linejoin="round" width="12" height="12" aria-hidden="true">' +
|
|
11
|
+
'<path d="M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z"/></svg>';
|
|
12
|
+
|
|
13
|
+
/** Matches `@collection:<id>` and draws each one as a folder pill. */
|
|
14
|
+
const collectionBadge = {
|
|
15
|
+
pattern: /@collection:([\w-]+)/g,
|
|
16
|
+
render: (match: RegExpExecArray) => ({
|
|
17
|
+
label: (match[1] ?? '').replace(/-/g, ' '),
|
|
18
|
+
icon: folderIcon,
|
|
19
|
+
title: match[0],
|
|
20
|
+
}),
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/** Matches `{{ name }}`, showing that the trigger is whatever you pattern for. */
|
|
24
|
+
const handlebarsBadge = {
|
|
25
|
+
pattern: /\{\{\s*([\w-]+)\s*\}\}/g,
|
|
26
|
+
render: (match: RegExpExecArray) => ({
|
|
27
|
+
label: match[1] ?? '',
|
|
28
|
+
icon: folderIcon,
|
|
29
|
+
title: match[0],
|
|
30
|
+
}),
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const meta: Meta<typeof CkCodeEditor> = {
|
|
34
|
+
title: 'Base/CkCodeEditor',
|
|
35
|
+
component: CkCodeEditor,
|
|
36
|
+
tags: ['autodocs'],
|
|
37
|
+
argTypes: {
|
|
38
|
+
modelValue: { control: 'text', description: 'The code (`v-model`).' },
|
|
39
|
+
language: {
|
|
40
|
+
control: 'text',
|
|
41
|
+
description:
|
|
42
|
+
'Language id for highlighting and the header label, matched on names, aliases and extensions.',
|
|
43
|
+
},
|
|
44
|
+
filename: {
|
|
45
|
+
control: 'text',
|
|
46
|
+
description: 'Shown in the header instead of the language.',
|
|
47
|
+
},
|
|
48
|
+
maxHeight: {
|
|
49
|
+
control: 'text',
|
|
50
|
+
description: 'CSS length capping the scrollable area, e.g. `24rem`.',
|
|
51
|
+
},
|
|
52
|
+
placeholder: { control: 'text' },
|
|
53
|
+
readonly: { control: 'boolean' },
|
|
54
|
+
disabled: { control: 'boolean' },
|
|
55
|
+
showHeader: { control: 'boolean' },
|
|
56
|
+
showCopyButton: { control: 'boolean' },
|
|
57
|
+
showLineNumbers: { control: 'boolean' },
|
|
58
|
+
lineWrapping: { control: 'boolean' },
|
|
59
|
+
autofocus: { control: 'boolean' },
|
|
60
|
+
// A regex plus a callback cannot be typed into a control, so the rule is
|
|
61
|
+
// offered as named presets instead. Without this, badges could only be seen
|
|
62
|
+
// on the one story that hard-codes a rule.
|
|
63
|
+
badge: {
|
|
64
|
+
options: ['none', 'collections', 'handlebars'],
|
|
65
|
+
mapping: {
|
|
66
|
+
none: undefined,
|
|
67
|
+
collections: collectionBadge,
|
|
68
|
+
handlebars: handlebarsBadge,
|
|
69
|
+
},
|
|
70
|
+
control: { type: 'radio' },
|
|
71
|
+
description:
|
|
72
|
+
'Pattern plus a render callback turning matched text into inline pills. Pick a preset to try it on any story.',
|
|
73
|
+
},
|
|
74
|
+
writeToClipboard: {
|
|
75
|
+
control: false,
|
|
76
|
+
description:
|
|
77
|
+
'Clipboard writer for tests or hosts that proxy clipboard access.',
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
export default meta;
|
|
83
|
+
type Story = StoryObj<typeof meta>;
|
|
84
|
+
|
|
85
|
+
const typescriptSample = [
|
|
86
|
+
'export interface Greeting {',
|
|
87
|
+
' name: string;',
|
|
88
|
+
' formal?: boolean;',
|
|
89
|
+
'}',
|
|
90
|
+
'',
|
|
91
|
+
'// Returns a greeting suited to the audience.',
|
|
92
|
+
'export function greet({ name, formal = false }: Greeting): string {',
|
|
93
|
+
' const opener = formal ? "Good day" : "Hello";',
|
|
94
|
+
' return `${opener}, ${name}!`;',
|
|
95
|
+
'}',
|
|
96
|
+
].join('\n');
|
|
97
|
+
|
|
98
|
+
const pythonSample = [
|
|
99
|
+
'from dataclasses import dataclass',
|
|
100
|
+
'',
|
|
101
|
+
'',
|
|
102
|
+
'@dataclass',
|
|
103
|
+
'class Greeting:',
|
|
104
|
+
' name: str',
|
|
105
|
+
' formal: bool = False',
|
|
106
|
+
'',
|
|
107
|
+
' def render(self) -> str:',
|
|
108
|
+
' opener = "Good day" if self.formal else "Hello"',
|
|
109
|
+
' return f"{opener}, {self.name}!"',
|
|
110
|
+
].join('\n');
|
|
111
|
+
|
|
112
|
+
const badgeSample = [
|
|
113
|
+
'You improve and draft work writing: emails, project summaries and status',
|
|
114
|
+
'updates, tightening structure and matching register to the audience.',
|
|
115
|
+
'',
|
|
116
|
+
'For analyses, use @collection:collection-name-3 and @collection:collection-name-4',
|
|
117
|
+
'and distinguish an opinion from a fact.',
|
|
118
|
+
].join('\n');
|
|
119
|
+
|
|
120
|
+
const defaultTemplateSource = `<script setup lang="ts">
|
|
121
|
+
import { ref } from 'vue';
|
|
122
|
+
import { CkCodeEditor } from '@aleph-alpha/chat-kit';
|
|
123
|
+
|
|
124
|
+
const code = ref('export const answer = 42;');
|
|
125
|
+
</script>
|
|
126
|
+
|
|
127
|
+
<template>
|
|
128
|
+
<CkCodeEditor v-model="code" language="typescript" aria-label="Code" />
|
|
129
|
+
</template>`;
|
|
130
|
+
|
|
131
|
+
const withFilenameTemplateSource = `<script setup lang="ts">
|
|
132
|
+
import { ref } from 'vue';
|
|
133
|
+
import { CkCodeEditor } from '@aleph-alpha/chat-kit';
|
|
134
|
+
|
|
135
|
+
const code = ref('print("hello")');
|
|
136
|
+
</script>
|
|
137
|
+
|
|
138
|
+
<template>
|
|
139
|
+
<CkCodeEditor
|
|
140
|
+
v-model="code"
|
|
141
|
+
language="python"
|
|
142
|
+
filename="main.py"
|
|
143
|
+
aria-label="main.py"
|
|
144
|
+
/>
|
|
145
|
+
</template>`;
|
|
146
|
+
|
|
147
|
+
const plainTextTemplateSource = `<script setup lang="ts">
|
|
148
|
+
import { ref } from 'vue';
|
|
149
|
+
import { CkCodeEditor } from '@aleph-alpha/chat-kit';
|
|
150
|
+
|
|
151
|
+
const notes = ref('');
|
|
152
|
+
</script>
|
|
153
|
+
|
|
154
|
+
<template>
|
|
155
|
+
<CkCodeEditor
|
|
156
|
+
v-model="notes"
|
|
157
|
+
placeholder="Paste your snippet…"
|
|
158
|
+
:show-line-numbers="false"
|
|
159
|
+
aria-label="Snippet"
|
|
160
|
+
/>
|
|
161
|
+
</template>`;
|
|
162
|
+
|
|
163
|
+
const readonlyTemplateSource = `<script setup lang="ts">
|
|
164
|
+
import { CkCodeEditor } from '@aleph-alpha/chat-kit';
|
|
165
|
+
|
|
166
|
+
const code = 'export const answer = 42;';
|
|
167
|
+
</script>
|
|
168
|
+
|
|
169
|
+
<template>
|
|
170
|
+
<CkCodeEditor :model-value="code" language="typescript" readonly aria-label="Code" />
|
|
171
|
+
</template>`;
|
|
172
|
+
|
|
173
|
+
const bareTemplateSource = `<script setup lang="ts">
|
|
174
|
+
import { ref } from 'vue';
|
|
175
|
+
import { CkCodeEditor } from '@aleph-alpha/chat-kit';
|
|
176
|
+
|
|
177
|
+
const code = ref('SELECT * FROM users;');
|
|
178
|
+
</script>
|
|
179
|
+
|
|
180
|
+
<template>
|
|
181
|
+
<CkCodeEditor
|
|
182
|
+
v-model="code"
|
|
183
|
+
language="sql"
|
|
184
|
+
:show-header="false"
|
|
185
|
+
aria-label="Query"
|
|
186
|
+
/>
|
|
187
|
+
</template>`;
|
|
188
|
+
|
|
189
|
+
const scrollingTemplateSource = `<script setup lang="ts">
|
|
190
|
+
import { ref } from 'vue';
|
|
191
|
+
import { CkCodeEditor } from '@aleph-alpha/chat-kit';
|
|
192
|
+
|
|
193
|
+
const code = ref(longFile);
|
|
194
|
+
</script>
|
|
195
|
+
|
|
196
|
+
<template>
|
|
197
|
+
<CkCodeEditor v-model="code" language="typescript" max-height="16rem" line-wrapping />
|
|
198
|
+
</template>`;
|
|
199
|
+
|
|
200
|
+
const withBadgesTemplateSource = `<script setup lang="ts">
|
|
201
|
+
import { ref } from 'vue';
|
|
202
|
+
import { CkCodeEditor } from '@aleph-alpha/chat-kit';
|
|
203
|
+
|
|
204
|
+
const prompt = ref('For analyses, use @collection:sales and @collection:support.');
|
|
205
|
+
|
|
206
|
+
// The document keeps the raw text; only the drawing is replaced.
|
|
207
|
+
const badge = {
|
|
208
|
+
pattern: /@collection:([\\w-]+)/g,
|
|
209
|
+
render: (m) => ({ label: m[1], icon: folderIcon, title: m[0] }),
|
|
210
|
+
};
|
|
211
|
+
</script>
|
|
212
|
+
|
|
213
|
+
<template>
|
|
214
|
+
<CkCodeEditor
|
|
215
|
+
v-model="prompt"
|
|
216
|
+
language="markdown"
|
|
217
|
+
:badge="badge"
|
|
218
|
+
:line-wrapping="true"
|
|
219
|
+
aria-label="Prompt"
|
|
220
|
+
/>
|
|
221
|
+
</template>`;
|
|
222
|
+
|
|
223
|
+
const matchesRendererTemplateSource = `<script setup lang="ts">
|
|
224
|
+
import { ref } from 'vue';
|
|
225
|
+
import { CkCodeEditor, CkMarkdownRenderer } from '@aleph-alpha/chat-kit';
|
|
226
|
+
import { UiCodeBlock } from '@aleph-alpha/ui-library';
|
|
227
|
+
|
|
228
|
+
const code = ref('export const answer = 42;');
|
|
229
|
+
const asMarkdown = computed(() => '\\\`\\\`\\\`typescript\\n' + code.value + '\\n\\\`\\\`\\\`');
|
|
230
|
+
</script>
|
|
231
|
+
|
|
232
|
+
<template>
|
|
233
|
+
<div class="text-content-on-surface-primary grid gap-6 text-base leading-7 md:grid-cols-2">
|
|
234
|
+
<CkCodeEditor v-model="code" language="typescript" aria-label="Editor" />
|
|
235
|
+
<CkMarkdownRenderer :content="asMarkdown">
|
|
236
|
+
<template #code-block="{ code: c, language }">
|
|
237
|
+
<UiCodeBlock :code="c" :language="language || 'text'" />
|
|
238
|
+
</template>
|
|
239
|
+
</CkMarkdownRenderer>
|
|
240
|
+
</div>
|
|
241
|
+
</template>`;
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* A TypeScript snippet with the header, copy button and line numbers. Type in
|
|
245
|
+
* it — changes are emitted synchronously, with no debounce.
|
|
246
|
+
*/
|
|
247
|
+
export const Default: Story = {
|
|
248
|
+
render: (args) => ({
|
|
249
|
+
components: { CkCodeEditor },
|
|
250
|
+
setup() {
|
|
251
|
+
const code = ref(args.modelValue ?? '');
|
|
252
|
+
return { args, code };
|
|
253
|
+
},
|
|
254
|
+
template: `<CkCodeEditor v-bind="args" v-model="code" aria-label="Code" />`,
|
|
255
|
+
}),
|
|
256
|
+
args: { modelValue: typescriptSample, language: 'typescript' },
|
|
257
|
+
parameters: { docs: { source: { code: defaultTemplateSource } } },
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* `filename` replaces the language in the header, matching the prop of the same
|
|
262
|
+
* name on `UiCodeBlock`. The grammar still comes from `language`.
|
|
263
|
+
*/
|
|
264
|
+
export const WithFilename: Story = {
|
|
265
|
+
render: (args) => ({
|
|
266
|
+
components: { CkCodeEditor },
|
|
267
|
+
setup() {
|
|
268
|
+
const code = ref(args.modelValue ?? '');
|
|
269
|
+
return { args, code };
|
|
270
|
+
},
|
|
271
|
+
template: `<CkCodeEditor v-bind="args" v-model="code" aria-label="main.py" />`,
|
|
272
|
+
}),
|
|
273
|
+
args: { modelValue: pythonSample, language: 'python', filename: 'main.py' },
|
|
274
|
+
parameters: { docs: { source: { code: withFilenameTemplateSource } } },
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* With no language the header label is blank and nothing is highlighted, the
|
|
279
|
+
* same treatment `UiCodeBlock` gives plain text. Shown here with a placeholder
|
|
280
|
+
* and no gutter, as a lightweight snippet field.
|
|
281
|
+
*/
|
|
282
|
+
export const PlainText: Story = {
|
|
283
|
+
render: (args) => ({
|
|
284
|
+
components: { CkCodeEditor },
|
|
285
|
+
setup() {
|
|
286
|
+
const code = ref(args.modelValue ?? '');
|
|
287
|
+
return { args, code };
|
|
288
|
+
},
|
|
289
|
+
template: `<CkCodeEditor v-bind="args" v-model="code" aria-label="Snippet" />`,
|
|
290
|
+
}),
|
|
291
|
+
args: {
|
|
292
|
+
modelValue: '',
|
|
293
|
+
placeholder: 'Paste your snippet…',
|
|
294
|
+
showLineNumbers: false,
|
|
295
|
+
},
|
|
296
|
+
parameters: { docs: { source: { code: plainTextTemplateSource } } },
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* `readonly` keeps the code selectable and copyable but drops the caret and the
|
|
301
|
+
* active-line band. Use it while a request is in flight.
|
|
302
|
+
*/
|
|
303
|
+
export const Readonly: Story = {
|
|
304
|
+
args: {
|
|
305
|
+
modelValue: typescriptSample,
|
|
306
|
+
language: 'typescript',
|
|
307
|
+
readonly: true,
|
|
308
|
+
},
|
|
309
|
+
parameters: { docs: { source: { code: readonlyTemplateSource } } },
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* `showHeader: false` gives a bare editing surface, for forms that already
|
|
314
|
+
* label the field themselves.
|
|
315
|
+
*/
|
|
316
|
+
export const WithoutHeader: Story = {
|
|
317
|
+
args: {
|
|
318
|
+
modelValue: 'SELECT id, email\nFROM users\nWHERE active = true;',
|
|
319
|
+
language: 'sql',
|
|
320
|
+
showHeader: false,
|
|
321
|
+
},
|
|
322
|
+
parameters: { docs: { source: { code: bareTemplateSource } } },
|
|
323
|
+
};
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* `maxHeight` caps the scrollable area and `lineWrapping` folds long lines
|
|
327
|
+
* instead of scrolling sideways.
|
|
328
|
+
*/
|
|
329
|
+
export const ScrollingAndWrapping: Story = {
|
|
330
|
+
render: (args) => ({
|
|
331
|
+
components: { CkCodeEditor },
|
|
332
|
+
setup() {
|
|
333
|
+
const code = ref(args.modelValue ?? '');
|
|
334
|
+
return { args, code };
|
|
335
|
+
},
|
|
336
|
+
template: `<CkCodeEditor v-bind="args" v-model="code" aria-label="Long file" />`,
|
|
337
|
+
}),
|
|
338
|
+
args: {
|
|
339
|
+
modelValue: [
|
|
340
|
+
typescriptSample,
|
|
341
|
+
'',
|
|
342
|
+
'// A deliberately long line that has to wrap rather than scroll sideways, so you can see how lineWrapping behaves in a narrow column.',
|
|
343
|
+
'',
|
|
344
|
+
pythonSample,
|
|
345
|
+
].join('\n'),
|
|
346
|
+
language: 'typescript',
|
|
347
|
+
maxHeight: '16rem',
|
|
348
|
+
lineWrapping: true,
|
|
349
|
+
},
|
|
350
|
+
parameters: { docs: { source: { code: scrollingTemplateSource } } },
|
|
351
|
+
};
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* `badge` draws matched references as inline pills, the way a mention renders in
|
|
355
|
+
* prose. The document still holds the raw `@collection:…` text, so `getCode()`
|
|
356
|
+
* and copy give the real reference; a pill deletes as one unit.
|
|
357
|
+
*/
|
|
358
|
+
export const WithBadges: Story = {
|
|
359
|
+
render: (args) => ({
|
|
360
|
+
components: { CkCodeEditor },
|
|
361
|
+
setup() {
|
|
362
|
+
const code = ref(args.modelValue ?? '');
|
|
363
|
+
return { args, code };
|
|
364
|
+
},
|
|
365
|
+
template: `<CkCodeEditor v-bind="args" v-model="code" aria-label="Prompt" />`,
|
|
366
|
+
}),
|
|
367
|
+
args: {
|
|
368
|
+
modelValue: badgeSample,
|
|
369
|
+
language: 'markdown',
|
|
370
|
+
badge: collectionBadge,
|
|
371
|
+
lineWrapping: true,
|
|
372
|
+
},
|
|
373
|
+
parameters: { docs: { source: { code: withBadgesTemplateSource } } },
|
|
374
|
+
};
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* Parity check: the editor on the left, the same snippet rendered through
|
|
378
|
+
* `CkMarkdownRenderer` and `UiCodeBlock` on the right. Border, radius, header,
|
|
379
|
+
* font and syntax colours should read as one component in both themes.
|
|
380
|
+
*/
|
|
381
|
+
export const MatchesTheRenderer: Story = {
|
|
382
|
+
render: (args) => ({
|
|
383
|
+
components: { CkCodeEditor, CkMarkdownRenderer, UiCodeBlock },
|
|
384
|
+
setup() {
|
|
385
|
+
const code = ref(args.modelValue ?? '');
|
|
386
|
+
return { args, code };
|
|
387
|
+
},
|
|
388
|
+
template: `
|
|
389
|
+
<div class="text-content-on-surface-primary grid gap-6 text-base leading-7 md:grid-cols-2">
|
|
390
|
+
<CkCodeEditor v-bind="args" v-model="code" aria-label="Editor" />
|
|
391
|
+
<CkMarkdownRenderer :content="'\\\`\\\`\\\`typescript\\n' + code + '\\n\\\`\\\`\\\`'">
|
|
392
|
+
<template #code-block="{ code: c, language }">
|
|
393
|
+
<UiCodeBlock :code="c" :language="language || 'text'" />
|
|
394
|
+
</template>
|
|
395
|
+
</CkMarkdownRenderer>
|
|
396
|
+
</div>
|
|
397
|
+
`,
|
|
398
|
+
}),
|
|
399
|
+
args: {
|
|
400
|
+
modelValue: typescriptSample,
|
|
401
|
+
language: 'typescript',
|
|
402
|
+
showLineNumbers: false,
|
|
403
|
+
},
|
|
404
|
+
parameters: { docs: { source: { code: matchesRendererTemplateSource } } },
|
|
405
|
+
};
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import {
|
|
3
|
+
computed,
|
|
4
|
+
onBeforeUnmount,
|
|
5
|
+
onMounted,
|
|
6
|
+
ref,
|
|
7
|
+
useTemplateRef,
|
|
8
|
+
watch,
|
|
9
|
+
} from 'vue';
|
|
10
|
+
import { UiButton, UiIcon } from '@aleph-alpha/ui-library';
|
|
11
|
+
import { useChatKitLabels } from '../../../handlers/useChatKitLabels';
|
|
12
|
+
import type { CodeMirrorHandle } from './createCodeMirror';
|
|
13
|
+
import type { CkCodeEditorEmits, CkCodeEditorProps } from './types';
|
|
14
|
+
|
|
15
|
+
defineOptions({ name: 'CkCodeEditor' });
|
|
16
|
+
|
|
17
|
+
const props = withDefaults(defineProps<CkCodeEditorProps>(), {
|
|
18
|
+
modelValue: '',
|
|
19
|
+
language: 'text',
|
|
20
|
+
filename: undefined,
|
|
21
|
+
placeholder: undefined,
|
|
22
|
+
readonly: false,
|
|
23
|
+
disabled: false,
|
|
24
|
+
ariaLabel: undefined,
|
|
25
|
+
showHeader: true,
|
|
26
|
+
showCopyButton: true,
|
|
27
|
+
showLineNumbers: true,
|
|
28
|
+
lineWrapping: false,
|
|
29
|
+
maxHeight: undefined,
|
|
30
|
+
autofocus: false,
|
|
31
|
+
badge: undefined,
|
|
32
|
+
writeToClipboard: undefined,
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
const emit = defineEmits<CkCodeEditorEmits>();
|
|
36
|
+
|
|
37
|
+
const labels = useChatKitLabels();
|
|
38
|
+
const rootRef = useTemplateRef<HTMLDivElement>('rootRef');
|
|
39
|
+
const hostRef = useTemplateRef<HTMLDivElement>('hostRef');
|
|
40
|
+
const isLocked = computed(() => props.readonly || props.disabled);
|
|
41
|
+
const isReady = ref(false);
|
|
42
|
+
const isCopied = ref(false);
|
|
43
|
+
|
|
44
|
+
// `UiCodeBlock` labels the block with the filename when there is one and hides
|
|
45
|
+
// the label entirely for plain text; mirror that so the two headers match.
|
|
46
|
+
const headerLabel = computed(() => {
|
|
47
|
+
const label = props.filename || props.language;
|
|
48
|
+
return label === 'text' ? '' : label;
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
const copyLabel = computed(() =>
|
|
52
|
+
isCopied.value
|
|
53
|
+
? (labels.value.codeEditorCopied ?? 'Copied')
|
|
54
|
+
: (labels.value.codeEditorCopy ?? 'Copy code'),
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
let editor: CodeMirrorHandle | undefined;
|
|
58
|
+
let destroyed = false;
|
|
59
|
+
// Last value emitted or applied, so an echo from the parent is not re-applied.
|
|
60
|
+
let lastKnownCode = props.modelValue;
|
|
61
|
+
let copyTimer: ReturnType<typeof setTimeout> | undefined;
|
|
62
|
+
|
|
63
|
+
function handleChange(value: string): void {
|
|
64
|
+
if (value === lastKnownCode) return;
|
|
65
|
+
lastKnownCode = value;
|
|
66
|
+
emit('update:modelValue', value);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
onMounted(async () => {
|
|
70
|
+
const parent = hostRef.value;
|
|
71
|
+
if (!parent) return;
|
|
72
|
+
|
|
73
|
+
// Lazy: CodeMirror and its grammars load only once an editor is shown.
|
|
74
|
+
const { createCodeMirror } = await import('./createCodeMirror');
|
|
75
|
+
if (destroyed) return;
|
|
76
|
+
|
|
77
|
+
const initialCode = props.modelValue;
|
|
78
|
+
lastKnownCode = initialCode;
|
|
79
|
+
const handle = await createCodeMirror({
|
|
80
|
+
parent,
|
|
81
|
+
doc: initialCode,
|
|
82
|
+
language: props.language,
|
|
83
|
+
placeholder: props.placeholder,
|
|
84
|
+
ariaLabel: props.ariaLabel,
|
|
85
|
+
readonly: isLocked.value,
|
|
86
|
+
showLineNumbers: props.showLineNumbers,
|
|
87
|
+
lineWrapping: props.lineWrapping,
|
|
88
|
+
badge: props.badge,
|
|
89
|
+
onChange: handleChange,
|
|
90
|
+
onFocus: () => emit('focus'),
|
|
91
|
+
onBlur: () => emit('blur'),
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
// Unmounted while the grammar was loading.
|
|
95
|
+
if (destroyed) {
|
|
96
|
+
handle.destroy();
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
editor = handle;
|
|
101
|
+
// Props may have moved on while the grammar loaded; catch up.
|
|
102
|
+
if (props.modelValue !== initialCode) handle.setCode(props.modelValue);
|
|
103
|
+
isReady.value = true;
|
|
104
|
+
if (props.autofocus) handle.focus();
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
watch(
|
|
108
|
+
() => props.modelValue,
|
|
109
|
+
(next) => {
|
|
110
|
+
if (next === lastKnownCode) return;
|
|
111
|
+
lastKnownCode = next;
|
|
112
|
+
editor?.setCode(next);
|
|
113
|
+
},
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
watch(
|
|
117
|
+
() => props.language,
|
|
118
|
+
(next) => {
|
|
119
|
+
// Loading a grammar is async; nothing waits on it, and a failure already
|
|
120
|
+
// falls back to plain text inside `setLanguage`.
|
|
121
|
+
editor?.setLanguage(next).catch(() => undefined);
|
|
122
|
+
},
|
|
123
|
+
);
|
|
124
|
+
watch(isLocked, (locked) => editor?.setReadonly(locked));
|
|
125
|
+
watch(
|
|
126
|
+
() => props.showLineNumbers,
|
|
127
|
+
(show) => editor?.setShowLineNumbers(show),
|
|
128
|
+
);
|
|
129
|
+
watch(
|
|
130
|
+
() => props.lineWrapping,
|
|
131
|
+
(wrap) => editor?.setLineWrapping(wrap),
|
|
132
|
+
);
|
|
133
|
+
watch(
|
|
134
|
+
() => props.badge,
|
|
135
|
+
(rule) => editor?.setBadge(rule),
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
onBeforeUnmount(() => {
|
|
139
|
+
destroyed = true;
|
|
140
|
+
clearTimeout(copyTimer);
|
|
141
|
+
editor?.destroy();
|
|
142
|
+
editor = undefined;
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
async function handleCopy(): Promise<void> {
|
|
146
|
+
const write =
|
|
147
|
+
props.writeToClipboard ??
|
|
148
|
+
((text: string) => globalThis.navigator.clipboard.writeText(text));
|
|
149
|
+
try {
|
|
150
|
+
await write(getCode());
|
|
151
|
+
isCopied.value = true;
|
|
152
|
+
clearTimeout(copyTimer);
|
|
153
|
+
copyTimer = setTimeout(() => {
|
|
154
|
+
isCopied.value = false;
|
|
155
|
+
}, 2000);
|
|
156
|
+
emit('copy', { success: true });
|
|
157
|
+
} catch {
|
|
158
|
+
emit('copy', { success: false });
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function isInsideRoot(target: EventTarget | null): boolean {
|
|
163
|
+
return target instanceof Node && (rootRef.value?.contains(target) ?? false);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// CodeMirror reports focus on its own content; the header's copy button is
|
|
167
|
+
// outside that, so only a move out of the whole component counts as a blur.
|
|
168
|
+
function onFocusOut(event: FocusEvent): void {
|
|
169
|
+
if (!isInsideRoot(event.relatedTarget)) emit('blur');
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function focus(): void {
|
|
173
|
+
editor?.focus();
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function getCode(): string {
|
|
177
|
+
return editor?.getCode() ?? props.modelValue;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
defineExpose({ focus, getCode });
|
|
181
|
+
</script>
|
|
182
|
+
|
|
183
|
+
<template>
|
|
184
|
+
<div
|
|
185
|
+
ref="rootRef"
|
|
186
|
+
:class="[
|
|
187
|
+
'ck-code-editor ck-code-surface border-border-surface-default min-w-0 overflow-hidden rounded-lg border',
|
|
188
|
+
isReady && 'ck-code-editor--ready',
|
|
189
|
+
isLocked && 'ck-code-editor--readonly',
|
|
190
|
+
disabled && 'ck-code-editor--disabled',
|
|
191
|
+
]"
|
|
192
|
+
:aria-disabled="disabled || undefined"
|
|
193
|
+
@focusout="onFocusOut"
|
|
194
|
+
>
|
|
195
|
+
<div
|
|
196
|
+
v-if="showHeader"
|
|
197
|
+
class="bg-background-surface-muted-subtle flex h-9 items-center justify-between px-3"
|
|
198
|
+
>
|
|
199
|
+
<span class="text-content-on-surface-secondary label-12-regular">
|
|
200
|
+
{{ headerLabel }}
|
|
201
|
+
</span>
|
|
202
|
+
<UiButton
|
|
203
|
+
v-if="showCopyButton"
|
|
204
|
+
variant="ghost"
|
|
205
|
+
size="icon-sm"
|
|
206
|
+
:aria-label="copyLabel"
|
|
207
|
+
@click="handleCopy"
|
|
208
|
+
>
|
|
209
|
+
<UiIcon :name="isCopied ? 'check' : 'copy'" :size="14" />
|
|
210
|
+
</UiButton>
|
|
211
|
+
</div>
|
|
212
|
+
|
|
213
|
+
<div
|
|
214
|
+
ref="hostRef"
|
|
215
|
+
class="ck-code-editor__host"
|
|
216
|
+
:style="maxHeight ? { '--ck-code-max-height': maxHeight } : undefined"
|
|
217
|
+
/>
|
|
218
|
+
</div>
|
|
219
|
+
</template>
|
|
220
|
+
|
|
221
|
+
<style scoped>
|
|
222
|
+
/* The editor fills the panel; `:deep` is needed because CodeMirror builds its
|
|
223
|
+
* own DOM, which scoped styles do not reach. */
|
|
224
|
+
.ck-code-editor__host :deep(.cm-editor) {
|
|
225
|
+
background: var(--ck-cm-bg);
|
|
226
|
+
color: var(--ck-cm-foreground);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
.ck-code-editor__host :deep(.cm-scroller) {
|
|
230
|
+
max-height: var(--ck-code-max-height, none);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/* Nothing is being edited, so the active-line band would only be noise. */
|
|
234
|
+
.ck-code-editor--readonly .ck-code-editor__host :deep(.cm-activeLine),
|
|
235
|
+
.ck-code-editor--readonly .ck-code-editor__host :deep(.cm-activeLineGutter) {
|
|
236
|
+
background: transparent;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
.ck-code-editor--disabled {
|
|
240
|
+
opacity: 0.6;
|
|
241
|
+
cursor: not-allowed;
|
|
242
|
+
pointer-events: none;
|
|
243
|
+
}
|
|
244
|
+
</style>
|