@aleph-alpha/chat-kit 6.4.2 → 6.5.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 (40) hide show
  1. package/dist/{CkHistory.vue_vue_type_script_setup_true_lang-wu1Zrvgc.js → CkHistory.vue_vue_type_script_setup_true_lang-C4XS2QQI.js} +84 -35
  2. package/dist/chat-kit.css +41 -10
  3. package/dist/components/base/CkCitation/CkCitation.vue.d.ts +1 -0
  4. package/dist/components/base/CkCitation/CkCitation.vue.d.ts.map +1 -1
  5. package/dist/components/base/CkCitation/types.d.ts +6 -0
  6. package/dist/components/base/CkCitation/types.d.ts.map +1 -1
  7. package/dist/components/base/CkCitationCard/CkCitationCard.vue.d.ts +1 -0
  8. package/dist/components/base/CkCitationCard/CkCitationCard.vue.d.ts.map +1 -1
  9. package/dist/components/base/CkCitationCard/types.d.ts +6 -0
  10. package/dist/components/base/CkCitationCard/types.d.ts.map +1 -1
  11. package/dist/components/base/CkCodeEditor/CkCodeEditor.vue.d.ts.map +1 -1
  12. package/dist/components/base/CkCodeEditor/createCodeMirror.d.ts +5 -1
  13. package/dist/components/base/CkCodeEditor/createCodeMirror.d.ts.map +1 -1
  14. package/dist/components/base/CkCodeEditor/index.d.ts +1 -0
  15. package/dist/components/base/CkCodeEditor/index.d.ts.map +1 -1
  16. package/dist/components/base/CkCodeEditor/loadCodeMirror.d.ts +17 -0
  17. package/dist/components/base/CkCodeEditor/loadCodeMirror.d.ts.map +1 -0
  18. package/dist/components/base/index.d.ts +1 -1
  19. package/dist/components/base/index.d.ts.map +1 -1
  20. package/dist/components/index.d.ts +1 -1
  21. package/dist/components/index.d.ts.map +1 -1
  22. package/dist/components/index.js +3 -2
  23. package/dist/{createCodeMirror-BO-TWTzn.js → createCodeMirror-CN8xL8qU.js} +20 -10
  24. package/dist/index.d.ts +1 -1
  25. package/dist/index.d.ts.map +1 -1
  26. package/dist/index.js +3 -2
  27. package/package.json +3 -3
  28. package/src/components/base/CkCitation/CkCitation.vue +2 -0
  29. package/src/components/base/CkCitation/types.ts +6 -0
  30. package/src/components/base/CkCitationCard/CkCitationCard.spec.ts +15 -0
  31. package/src/components/base/CkCitationCard/CkCitationCard.vue +2 -1
  32. package/src/components/base/CkCitationCard/types.ts +6 -0
  33. package/src/components/base/CkCodeEditor/CkCodeEditor.spec.ts +71 -0
  34. package/src/components/base/CkCodeEditor/CkCodeEditor.vue +89 -20
  35. package/src/components/base/CkCodeEditor/createCodeMirror.ts +34 -13
  36. package/src/components/base/CkCodeEditor/index.ts +1 -0
  37. package/src/components/base/CkCodeEditor/loadCodeMirror.ts +28 -0
  38. package/src/components/base/index.ts +1 -1
  39. package/src/components/index.ts +1 -0
  40. package/src/index.ts +1 -0
@@ -114,22 +114,22 @@ function baseExtensions(options) {
114
114
  ...options.placeholder ? [placeholder(options.placeholder)] : []
115
115
  ];
116
116
  }
117
- async function createCodeMirror(options) {
117
+ function createCodeMirror(options) {
118
118
  const languageSlot = new Compartment();
119
119
  const readonlySlot = new Compartment();
120
120
  const lineNumbersSlot = new Compartment();
121
121
  const wrappingSlot = new Compartment();
122
122
  const badgeSlot = new Compartment();
123
123
  const lineSeparatorSlot = new Compartment();
124
- const support = await resolveLanguage(options.language);
125
124
  let languageRequestId = 0;
125
+ let destroyed = false;
126
126
  const view = new EditorView({
127
127
  parent: options.parent,
128
128
  state: EditorState.create({
129
129
  doc: options.doc,
130
130
  extensions: [
131
131
  ...baseExtensions(options),
132
- languageSlot.of(support ? [support] : []),
132
+ languageSlot.of([]),
133
133
  readonlySlot.of(readonlyState(options.readonly)),
134
134
  lineNumbersSlot.of(options.showLineNumbers ? lineNumbers() : []),
135
135
  wrappingSlot.of(options.lineWrapping ? EditorView.lineWrapping : []),
@@ -141,8 +141,20 @@ async function createCodeMirror(options) {
141
141
  })
142
142
  });
143
143
  function reconfigure(slot, extension) {
144
+ if (destroyed) return;
144
145
  view.dispatch({ effects: slot.reconfigure(extension) });
145
146
  }
147
+ async function setLanguage(language) {
148
+ const request = ++languageRequestId;
149
+ const next = await resolveLanguage(language);
150
+ if (request !== languageRequestId) return;
151
+ try {
152
+ reconfigure(languageSlot, next ? [next] : []);
153
+ } catch (error) {
154
+ console.warn(`[CkCodeEditor] Could not apply "${language}":`, error);
155
+ }
156
+ }
157
+ setLanguage(options.language).catch(() => void 0);
146
158
  return {
147
159
  view,
148
160
  getCode: () => readDoc(view.state),
@@ -158,12 +170,7 @@ async function createCodeMirror(options) {
158
170
  selection: { anchor: cursor }
159
171
  });
160
172
  },
161
- setLanguage: async (language) => {
162
- const request = ++languageRequestId;
163
- const next = await resolveLanguage(language);
164
- if (request !== languageRequestId) return;
165
- reconfigure(languageSlot, next ? [next] : []);
166
- },
173
+ setLanguage,
167
174
  setReadonly: (readonly) => {
168
175
  reconfigure(readonlySlot, readonlyState(readonly));
169
176
  },
@@ -177,7 +184,10 @@ async function createCodeMirror(options) {
177
184
  reconfigure(badgeSlot, rule ? badgeExtension(rule) : []);
178
185
  },
179
186
  focus: () => view.focus(),
180
- destroy: () => view.destroy()
187
+ destroy: () => {
188
+ destroyed = true;
189
+ view.destroy();
190
+ }
181
191
  };
182
192
  }
183
193
  export {
package/dist/index.d.ts CHANGED
@@ -6,7 +6,7 @@ export { useChat, useConversationSessions, useConversations, } from './composabl
6
6
  export type { ChatActions, ChatState, ConversationSession, CreateConversationInput, MessageHandler, MutationOptions, UseChatReturn, UseConversationSessionsReturn, UseConversationsOptions, UseConversationsReturn, } from './composables';
7
7
  export { SdkError, createDocumentUploadProvider, defaultDocumentUploadConfig, defaultEndpoints, useDocumentUploadWithDefaultConfig, } from './composables/fileUpload';
8
8
  export type { DocumentUploadConfig, DocumentUploadProvider, FileEntry, FileStatus, } from './composables/fileUpload';
9
- export { CkAgentMessage, CkAgentMessageHeader, CkHeaderActivityButton, CkCitation, CkCitationLabel, CkCitationCard, CkConversation, CkConversationLayout, CkCopyMessageButton, CkDropZoneOverlay, CkFeedbackButton, CkFeedbackDialog, CkFileItemsList, CkHistory, CkHistoryItem, CkHistoryWrapper, CkMarkdownRenderer, CkMessageButton, CkModelSelector, CkPromptSendButton, CkPromptMenuButton, CkPromptStopButton, CkPromptInput, CkPromptInputDisclaimer, CkRegenerateMessageButton, CkSendIcon, CkStopIcon, CkUserMessage, CkEditableUserMessage, CkMilkdownEditor, CkCodeEditor, } from './components';
9
+ export { CkAgentMessage, CkAgentMessageHeader, CkHeaderActivityButton, CkCitation, CkCitationLabel, CkCitationCard, CkConversation, CkConversationLayout, CkCopyMessageButton, CkDropZoneOverlay, CkFeedbackButton, CkFeedbackDialog, CkFileItemsList, CkHistory, CkHistoryItem, CkHistoryWrapper, CkMarkdownRenderer, CkMessageButton, CkModelSelector, CkPromptSendButton, CkPromptMenuButton, CkPromptStopButton, CkPromptInput, CkPromptInputDisclaimer, CkRegenerateMessageButton, CkSendIcon, CkStopIcon, CkUserMessage, CkEditableUserMessage, CkMilkdownEditor, CkCodeEditor, preloadCkCodeEditor, } from './components';
10
10
  export type { CkAgentMessageHeaderProps, CkHeaderActivityButtonProps, CkHeaderActivityButtonEmits, CkCitationProps, CkCitationEmits, CkCitationSource, CkCitationLabelProps, CkCitationCardProps, CkCitationCardEmits, CkConversationLayoutProps, CkConversationProps, CkDropZoneOverlayProps, CkDropZoneOverlayEmits, CkDropZoneRejection, CkDropZoneRejectionReason, CkFeedbackButtonProps, CkFeedbackDialogEmits, CkFeedbackDialogProps, CkFeedbackDialogSubmitPayload, CkFeedbackReason, CkFeedbackSentiment, CkFeedbackValue, CkFileEntry, CkFileItemsListEmits, CkFileItemsListProps, CkHistoryEmits, CkHistoryItemEmits, CkHistoryItemProps, CkHistoryProps, CkHistoryWrapperProps, CkMarkdownElementClasses, CkMarkdownRendererProps, CkMarkdownRendererEmits, CkModelOption, CkModelSelectorProps, CkModelSelectorEmits, CkPromptMenuButtonProps, CkUserMessageProps, CkEditableUserMessageProps, CkEditableUserMessageEmits, CkMilkdownEditorEmits, CkMilkdownEditorExposed, CkMilkdownEditorProps, CkCodeEditorEmits, CkCodeEditorExposed, CkCodeEditorFadeSurface, CkCodeEditorProps, } from './components';
11
11
  export { formatRelativeTime, markdownToPlainText } from './helpers';
12
12
  export type { ActiveComparison, BaseMessage, ComparisonLane, Conversation, ConversationTree, MessageContent, MessageContentStatus, MessageMetadata, MessageReasoningContent, MessageRole, MessageStatus, MessageTextContent, MessageToolContent, TreeMessage, } from './types';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAMA,OAAO,iBAAiB,CAAC;AAGzB,OAAO,EACL,KAAK,UAAU,EACf,KAAK,WAAW,EAChB,KAAK,YAAY,EACjB,KAAK,mBAAmB,EACxB,KAAK,UAAU,EACf,KAAK,eAAe,EACpB,KAAK,mBAAmB,EACxB,KAAK,WAAW,GACjB,MAAM,YAAY,CAAC;AASpB,OAAO,EACL,iBAAiB,EACjB,iBAAiB,EACjB,oBAAoB,EACpB,gBAAgB,EAChB,oBAAoB,EACpB,eAAe,GAChB,MAAM,YAAY,CAAC;AACpB,YAAY,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAGhD,OAAO,EACL,OAAO,EACP,uBAAuB,EACvB,gBAAgB,GACjB,MAAM,eAAe,CAAC;AACvB,YAAY,EACV,WAAW,EACX,SAAS,EACT,mBAAmB,EACnB,uBAAuB,EACvB,cAAc,EACd,eAAe,EACf,aAAa,EACb,6BAA6B,EAC7B,uBAAuB,EACvB,sBAAsB,GACvB,MAAM,eAAe,CAAC;AAEvB,OAAO,EACL,QAAQ,EACR,4BAA4B,EAC5B,2BAA2B,EAC3B,gBAAgB,EAChB,kCAAkC,GACnC,MAAM,0BAA0B,CAAC;AAClC,YAAY,EACV,oBAAoB,EACpB,sBAAsB,EACtB,SAAS,EACT,UAAU,GACX,MAAM,0BAA0B,CAAC;AAGlC,OAAO,EACL,cAAc,EACd,oBAAoB,EACpB,sBAAsB,EACtB,UAAU,EACV,eAAe,EACf,cAAc,EACd,cAAc,EACd,oBAAoB,EACpB,mBAAmB,EACnB,iBAAiB,EACjB,gBAAgB,EAChB,gBAAgB,EAChB,eAAe,EACf,SAAS,EACT,aAAa,EACb,gBAAgB,EAChB,kBAAkB,EAClB,eAAe,EACf,eAAe,EACf,kBAAkB,EAClB,kBAAkB,EAClB,kBAAkB,EAClB,aAAa,EACb,uBAAuB,EACvB,yBAAyB,EACzB,UAAU,EACV,UAAU,EACV,aAAa,EACb,qBAAqB,EACrB,gBAAgB,EAChB,YAAY,GACb,MAAM,cAAc,CAAC;AACtB,YAAY,EACV,yBAAyB,EACzB,2BAA2B,EAC3B,2BAA2B,EAC3B,eAAe,EACf,eAAe,EACf,gBAAgB,EAChB,oBAAoB,EACpB,mBAAmB,EACnB,mBAAmB,EACnB,yBAAyB,EACzB,mBAAmB,EACnB,sBAAsB,EACtB,sBAAsB,EACtB,mBAAmB,EACnB,yBAAyB,EACzB,qBAAqB,EACrB,qBAAqB,EACrB,qBAAqB,EACrB,6BAA6B,EAC7B,gBAAgB,EAChB,mBAAmB,EACnB,eAAe,EACf,WAAW,EACX,oBAAoB,EACpB,oBAAoB,EACpB,cAAc,EACd,kBAAkB,EAClB,kBAAkB,EAClB,cAAc,EACd,qBAAqB,EACrB,wBAAwB,EACxB,uBAAuB,EACvB,uBAAuB,EACvB,aAAa,EACb,oBAAoB,EACpB,oBAAoB,EACpB,uBAAuB,EACvB,kBAAkB,EAClB,0BAA0B,EAC1B,0BAA0B,EAC1B,qBAAqB,EACrB,uBAAuB,EACvB,qBAAqB,EACrB,iBAAiB,EACjB,mBAAmB,EACnB,uBAAuB,EACvB,iBAAiB,GAClB,MAAM,cAAc,CAAC;AAGtB,OAAO,EAAE,kBAAkB,EAAE,mBAAmB,EAAE,MAAM,WAAW,CAAC;AAGpE,YAAY,EACV,gBAAgB,EAChB,WAAW,EACX,cAAc,EACd,YAAY,EACZ,gBAAgB,EAChB,cAAc,EACd,oBAAoB,EACpB,eAAe,EACf,uBAAuB,EACvB,WAAW,EACX,aAAa,EACb,kBAAkB,EAClB,kBAAkB,EAClB,WAAW,GACZ,MAAM,SAAS,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAMA,OAAO,iBAAiB,CAAC;AAGzB,OAAO,EACL,KAAK,UAAU,EACf,KAAK,WAAW,EAChB,KAAK,YAAY,EACjB,KAAK,mBAAmB,EACxB,KAAK,UAAU,EACf,KAAK,eAAe,EACpB,KAAK,mBAAmB,EACxB,KAAK,WAAW,GACjB,MAAM,YAAY,CAAC;AASpB,OAAO,EACL,iBAAiB,EACjB,iBAAiB,EACjB,oBAAoB,EACpB,gBAAgB,EAChB,oBAAoB,EACpB,eAAe,GAChB,MAAM,YAAY,CAAC;AACpB,YAAY,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAGhD,OAAO,EACL,OAAO,EACP,uBAAuB,EACvB,gBAAgB,GACjB,MAAM,eAAe,CAAC;AACvB,YAAY,EACV,WAAW,EACX,SAAS,EACT,mBAAmB,EACnB,uBAAuB,EACvB,cAAc,EACd,eAAe,EACf,aAAa,EACb,6BAA6B,EAC7B,uBAAuB,EACvB,sBAAsB,GACvB,MAAM,eAAe,CAAC;AAEvB,OAAO,EACL,QAAQ,EACR,4BAA4B,EAC5B,2BAA2B,EAC3B,gBAAgB,EAChB,kCAAkC,GACnC,MAAM,0BAA0B,CAAC;AAClC,YAAY,EACV,oBAAoB,EACpB,sBAAsB,EACtB,SAAS,EACT,UAAU,GACX,MAAM,0BAA0B,CAAC;AAGlC,OAAO,EACL,cAAc,EACd,oBAAoB,EACpB,sBAAsB,EACtB,UAAU,EACV,eAAe,EACf,cAAc,EACd,cAAc,EACd,oBAAoB,EACpB,mBAAmB,EACnB,iBAAiB,EACjB,gBAAgB,EAChB,gBAAgB,EAChB,eAAe,EACf,SAAS,EACT,aAAa,EACb,gBAAgB,EAChB,kBAAkB,EAClB,eAAe,EACf,eAAe,EACf,kBAAkB,EAClB,kBAAkB,EAClB,kBAAkB,EAClB,aAAa,EACb,uBAAuB,EACvB,yBAAyB,EACzB,UAAU,EACV,UAAU,EACV,aAAa,EACb,qBAAqB,EACrB,gBAAgB,EAChB,YAAY,EACZ,mBAAmB,GACpB,MAAM,cAAc,CAAC;AACtB,YAAY,EACV,yBAAyB,EACzB,2BAA2B,EAC3B,2BAA2B,EAC3B,eAAe,EACf,eAAe,EACf,gBAAgB,EAChB,oBAAoB,EACpB,mBAAmB,EACnB,mBAAmB,EACnB,yBAAyB,EACzB,mBAAmB,EACnB,sBAAsB,EACtB,sBAAsB,EACtB,mBAAmB,EACnB,yBAAyB,EACzB,qBAAqB,EACrB,qBAAqB,EACrB,qBAAqB,EACrB,6BAA6B,EAC7B,gBAAgB,EAChB,mBAAmB,EACnB,eAAe,EACf,WAAW,EACX,oBAAoB,EACpB,oBAAoB,EACpB,cAAc,EACd,kBAAkB,EAClB,kBAAkB,EAClB,cAAc,EACd,qBAAqB,EACrB,wBAAwB,EACxB,uBAAuB,EACvB,uBAAuB,EACvB,aAAa,EACb,oBAAoB,EACpB,oBAAoB,EACpB,uBAAuB,EACvB,kBAAkB,EAClB,0BAA0B,EAC1B,0BAA0B,EAC1B,qBAAqB,EACrB,uBAAuB,EACvB,qBAAqB,EACrB,iBAAiB,EACjB,mBAAmB,EACnB,uBAAuB,EACvB,iBAAiB,GAClB,MAAM,cAAc,CAAC;AAGtB,OAAO,EAAE,kBAAkB,EAAE,mBAAmB,EAAE,MAAM,WAAW,CAAC;AAGpE,YAAY,EACV,gBAAgB,EAChB,WAAW,EACX,cAAc,EACd,YAAY,EACZ,gBAAgB,EAChB,cAAc,EACd,oBAAoB,EACpB,eAAe,EACf,uBAAuB,EACvB,WAAW,EACX,aAAa,EACb,kBAAkB,EAClB,kBAAkB,EAClB,WAAW,GACZ,MAAM,SAAS,CAAC"}
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { u } from "./useMessageHandler-Bkzrdgfj.js";
2
2
  import { C, d, p, u as u2, b } from "./useChatKitLabels-DxW0N5U-.js";
3
3
  import { S, c, d as d2, f, u as u3, a, b as b2, e } from "./index-D0DQjdsq.js";
4
- import { _, a as a2, b as b3, c as c2, C as C2, d as d3, e as e2, f as f2, g, h, i, j, k, l, m, n, o, p as p2, q, r, s, t, u as u4, v, w, x, y, z, A, B, D, E } from "./CkHistory.vue_vue_type_script_setup_true_lang-wu1Zrvgc.js";
4
+ import { _, a as a2, b as b3, c as c2, C as C2, d as d3, e as e2, f as f2, g, h, i, j, k, l, m, n, o, p as p2, q, r, s, t, u as u4, v, w, x, y, z, A, B, D, F, E } from "./CkHistory.vue_vue_type_script_setup_true_lang-C4XS2QQI.js";
5
5
  import { m as m2 } from "./markdownToPlainText-DN_G-I6g.js";
6
6
  export {
7
7
  C as CHAT_KIT_LABELS,
@@ -41,8 +41,9 @@ export {
41
41
  d as defaultChatKitLabels,
42
42
  d2 as defaultDocumentUploadConfig,
43
43
  f as defaultEndpoints,
44
- E as formatRelativeTime,
44
+ F as formatRelativeTime,
45
45
  m2 as markdownToPlainText,
46
+ E as preloadCkCodeEditor,
46
47
  p as provideChatKitLabels,
47
48
  u3 as useChat,
48
49
  u2 as useChatKitLabels,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aleph-alpha/chat-kit",
3
- "version": "6.4.2",
3
+ "version": "6.5.0",
4
4
  "description": "Chat kit package for building chat interfaces with Vue",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -115,9 +115,9 @@
115
115
  "vue": "^3.5.27",
116
116
  "vue-tsc": "^3.2.4",
117
117
  "wait-on": "9.0.3",
118
- "@aleph-alpha/eslint-config-frontend": "0.7.1",
119
118
  "@aleph-alpha/prettier-config-frontend": "2.1.1",
120
- "@aleph-alpha/tsconfig-frontend": "0.7.3"
119
+ "@aleph-alpha/tsconfig-frontend": "0.7.3",
120
+ "@aleph-alpha/eslint-config-frontend": "0.7.1"
121
121
  },
122
122
  "scripts": {
123
123
  "build": "vite build && vue-tsc --project tsconfig.build.json --declaration --emitDeclarationOnly --outDir dist",
@@ -15,6 +15,7 @@ defineOptions({
15
15
 
16
16
  withDefaults(defineProps<CkCitationProps>(), {
17
17
  selected: false,
18
+ icon: undefined,
18
19
  locationLabel: undefined,
19
20
  url: undefined,
20
21
  ariaLabel: undefined,
@@ -135,6 +136,7 @@ onBeforeUnmount(() => {
135
136
  >
136
137
  <CkCitationCard
137
138
  :file-name="fileName"
139
+ :icon="icon"
138
140
  :snippet="snippet"
139
141
  :location-label="locationLabel"
140
142
  :collection-name="collectionName"
@@ -20,6 +20,12 @@ export interface CkCitationSource {
20
20
  * (pass the original name with its extension).
21
21
  */
22
22
  fileName: string;
23
+ /**
24
+ * Explicit Lucide icon for the file-type badge, overriding the one derived
25
+ * from `fileName`. Use for a source that has no meaningful file type, e.g.
26
+ * `"globe"` for a web result.
27
+ */
28
+ icon?: string;
23
29
  /** The cited chunk text, shown in the card body. */
24
30
  snippet: string;
25
31
  /**
@@ -18,6 +18,21 @@ describe('CkCitationCard', () => {
18
18
  expect(screen.getByText('Page 3')).toBeInTheDocument();
19
19
  });
20
20
 
21
+ it('derives the badge icon from the file name by default', () => {
22
+ const { container } = render(CkCitationCard, { props: base });
23
+
24
+ expect(container.querySelector('.lucide-file')).toBeInTheDocument();
25
+ });
26
+
27
+ it('lets an explicit icon override the one derived from the file name', () => {
28
+ const { container } = render(CkCitationCard, {
29
+ props: { ...base, fileName: 'Weather in Berlin', icon: 'globe' },
30
+ });
31
+
32
+ expect(container.querySelector('.lucide-globe')).toBeInTheDocument();
33
+ expect(container.querySelector('.lucide-file')).not.toBeInTheDocument();
34
+ });
35
+
21
36
  it('hides the location footer when no locationLabel is given', () => {
22
37
  render(CkCitationCard, { props: base });
23
38
 
@@ -18,6 +18,7 @@ defineOptions({
18
18
  });
19
19
 
20
20
  const props = withDefaults(defineProps<CkCitationCardProps>(), {
21
+ icon: undefined,
21
22
  locationLabel: undefined,
22
23
  collectionName: undefined,
23
24
  url: undefined,
@@ -60,7 +61,7 @@ function handleOpen(): void {
60
61
  class="ck-citation-card flex w-[22.5rem] max-w-[90vw] flex-col gap-2 rounded-md bg-background-surface-floating px-6 pb-6 pt-4 shadow-md"
61
62
  >
62
63
  <div class="flex min-w-0 items-center gap-3 py-0.5">
63
- <UiFileType :filename="fileName" class="shrink-0" />
64
+ <UiFileType :filename="fileName" :icon="icon" class="shrink-0" />
64
65
  <UiTooltip
65
66
  :content="fileName"
66
67
  :disabled="!isFileNameTruncated"
@@ -20,6 +20,12 @@ export interface CkCitationCardProps {
20
20
  * e.g. `q3-report.pdf`).
21
21
  */
22
22
  fileName: string;
23
+ /**
24
+ * Explicit Lucide icon for the file-type badge, overriding the one derived
25
+ * from `fileName`. Use for a source that has no meaningful file type, e.g.
26
+ * `"globe"` for a web result.
27
+ */
28
+ icon?: string;
23
29
  /** The cited chunk text, shown in the card body. */
24
30
  snippet: string;
25
31
  /**
@@ -1,8 +1,17 @@
1
1
  import { render, screen, waitFor } from '@testing-library/vue';
2
2
  import userEvent from '@testing-library/user-event';
3
+ import type { MockInstance } from 'vitest';
3
4
  import CkCodeEditor from './CkCodeEditor.vue';
4
5
  import { detectLineSeparator, resolveLanguage } from './createCodeMirror';
5
6
  import { matchBadgeRanges } from './badgeDecoration';
7
+ import { loadCodeMirror } from './loadCodeMirror';
8
+
9
+ // Wrapped so a test can make the lazy chunk fail; every other test gets the
10
+ // real module.
11
+ vi.mock('./loadCodeMirror', async (importOriginal) => {
12
+ const actual = await importOriginal<typeof import('./loadCodeMirror')>();
13
+ return { ...actual, loadCodeMirror: vi.fn(actual.loadCodeMirror) };
14
+ });
6
15
 
7
16
  // CodeMirror measures its own layout on construction; jsdom has none of the
8
17
  // geometry APIs it calls. The stubs stay in this spec rather than the global
@@ -353,6 +362,68 @@ describe('CkCodeEditor', () => {
353
362
  const late = await renderEditor({ modelValue: 'y' });
354
363
  expect(() => late.unmount()).not.toThrow();
355
364
  });
365
+
366
+ describe('when CodeMirror fails to load', () => {
367
+ let logError: MockInstance;
368
+
369
+ beforeEach(() => {
370
+ vi.mocked(loadCodeMirror).mockRejectedValueOnce(
371
+ new Error('Failed to fetch dynamically imported module'),
372
+ );
373
+ logError = vi
374
+ .spyOn(globalThis.console, 'error')
375
+ .mockImplementation(() => undefined);
376
+ });
377
+
378
+ afterEach(() => {
379
+ logError.mockRestore();
380
+ });
381
+
382
+ async function renderFallback(
383
+ props: InstanceType<typeof CkCodeEditor>['$props'] = {},
384
+ ) {
385
+ const utils = render(CkCodeEditor, { props });
386
+ const textarea = await waitFor(() => {
387
+ const found = utils.container.querySelector(
388
+ '.ck-code-editor--fallback textarea',
389
+ );
390
+ expect(found).toBeInTheDocument();
391
+ return found as HTMLTextAreaElement;
392
+ });
393
+ return { ...utils, textarea };
394
+ }
395
+
396
+ it('falls back to a textarea with the value, placeholder and label', async () => {
397
+ const { textarea } = await renderFallback({
398
+ modelValue: 'body',
399
+ placeholder: 'Write the skill',
400
+ ariaLabel: 'Skill body',
401
+ });
402
+
403
+ expect(textarea.value).toBe('body');
404
+ expect(textarea.placeholder).toBe('Write the skill');
405
+ expect(textarea.getAttribute('aria-label')).toBe('Skill body');
406
+ expect(logError).toHaveBeenCalled();
407
+ });
408
+
409
+ it('keeps v-model working through the textarea', async () => {
410
+ const { textarea, emitted } = await renderFallback({ modelValue: '' });
411
+
412
+ await userEvent.type(textarea, 'hi');
413
+
414
+ const updates = emitted()['update:modelValue'] as string[][];
415
+ expect(updates.at(-1)?.[0]).toBe('hi');
416
+ });
417
+
418
+ it('locks the textarea when readonly', async () => {
419
+ const { textarea } = await renderFallback({
420
+ modelValue: 'x',
421
+ readonly: true,
422
+ });
423
+
424
+ expect(textarea.readOnly).toBe(true);
425
+ });
426
+ });
356
427
  });
357
428
 
358
429
  describe('resolveLanguage', () => {
@@ -11,6 +11,7 @@ import {
11
11
  import { UiButton, UiFadeMask, UiIcon } from '@aleph-alpha/ui-library';
12
12
  import { useChatKitLabels } from '../../../handlers/useChatKitLabels';
13
13
  import type { CodeMirrorHandle } from './createCodeMirror';
14
+ import { loadCodeMirror } from './loadCodeMirror';
14
15
  import type { CkCodeEditorEmits, CkCodeEditorProps } from './types';
15
16
 
16
17
  defineOptions({ name: 'CkCodeEditor' });
@@ -41,8 +42,12 @@ const emit = defineEmits<CkCodeEditorEmits>();
41
42
  const labels = useChatKitLabels();
42
43
  const rootRef = useTemplateRef<HTMLDivElement>('rootRef');
43
44
  const hostRef = useTemplateRef<HTMLDivElement>('hostRef');
45
+ const fallbackRef = useTemplateRef<HTMLTextAreaElement>('fallbackRef');
44
46
  const isLocked = computed(() => props.readonly || props.disabled);
45
47
  const isReady = ref(false);
48
+ // CodeMirror could not be loaded or built; a plain textarea stands in so the
49
+ // field can still be typed into.
50
+ const isFallback = ref(false);
46
51
  const isCopied = ref(false);
47
52
 
48
53
  // `UiCodeBlock` labels the block with the filename when there is one and hides
@@ -130,19 +135,16 @@ function handleChange(value: string): void {
130
135
  emit('update:modelValue', value);
131
136
  }
132
137
 
133
- onMounted(async () => {
134
- const parent = hostRef.value;
135
- if (!parent) return;
136
-
137
- // Lazy: CodeMirror and its grammars load only once an editor is shown.
138
- const { createCodeMirror } = await import('./createCodeMirror');
138
+ async function mountEditor(parent: HTMLElement): Promise<void> {
139
+ // Lazy: CodeMirror loads only once an editor is shown. The grammar follows in
140
+ // the background, so a slow one delays the colours, not the editor.
141
+ const { createCodeMirror } = await loadCodeMirror();
139
142
  if (destroyed) return;
140
143
 
141
- const initialCode = props.modelValue;
142
- lastKnownCode = initialCode;
143
- const handle = await createCodeMirror({
144
+ lastKnownCode = props.modelValue;
145
+ const handle = createCodeMirror({
144
146
  parent,
145
- doc: initialCode,
147
+ doc: props.modelValue,
146
148
  language: props.language,
147
149
  placeholder: props.placeholder,
148
150
  ariaLabel: props.ariaLabel,
@@ -154,20 +156,39 @@ onMounted(async () => {
154
156
  onFocus: () => emit('focus'),
155
157
  });
156
158
 
157
- // Unmounted while the grammar was loading.
158
- if (destroyed) {
159
- handle.destroy();
160
- return;
161
- }
162
-
163
159
  editor = handle;
164
- // Props may have moved on while the grammar loaded; catch up.
165
- if (props.modelValue !== initialCode) handle.setCode(props.modelValue);
166
160
  watchScrolling(handle.view.scrollDOM, handle.view.contentDOM);
167
161
  isReady.value = true;
168
162
  if (props.autofocus) handle.focus();
163
+ }
164
+
165
+ onMounted(async () => {
166
+ const parent = hostRef.value;
167
+ if (!parent) return;
168
+
169
+ try {
170
+ await mountEditor(parent);
171
+ } catch (error) {
172
+ // A stale or unreachable chunk, or a CodeMirror setup error such as
173
+ // duplicate `@codemirror/state` copies. Left alone, the header would sit
174
+ // over an empty body with no way to type.
175
+ // eslint-disable-next-line no-console
176
+ console.error('[CkCodeEditor] Falling back to a plain textarea:', error);
177
+ if (destroyed) return;
178
+ editor?.destroy();
179
+ editor = undefined;
180
+ isFallback.value = true;
181
+ if (props.autofocus) {
182
+ await nextTick();
183
+ fallbackRef.value?.focus();
184
+ }
185
+ }
169
186
  });
170
187
 
188
+ function onFallbackInput(event: Event): void {
189
+ handleChange((event.target as HTMLTextAreaElement).value);
190
+ }
191
+
171
192
  watch(
172
193
  () => props.modelValue,
173
194
  (next) => {
@@ -239,7 +260,8 @@ function onFocusOut(event: FocusEvent): void {
239
260
  }
240
261
 
241
262
  function focus(): void {
242
- editor?.focus();
263
+ if (editor) editor.focus();
264
+ else fallbackRef.value?.focus();
243
265
  }
244
266
 
245
267
  function getCode(): string {
@@ -255,6 +277,7 @@ defineExpose({ focus, getCode });
255
277
  :class="[
256
278
  'ck-code-editor ck-code-surface border-border-surface-default bg-background-input-default min-w-0 overflow-hidden rounded-xl border',
257
279
  isReady && 'ck-code-editor--ready',
280
+ isFallback && 'ck-code-editor--fallback',
258
281
  isLocked && 'ck-code-editor--readonly',
259
282
  disabled && 'ck-code-editor--disabled',
260
283
  ]"
@@ -288,7 +311,22 @@ defineExpose({ focus, getCode });
288
311
  ref="hostRef"
289
312
  class="ck-code-editor__host"
290
313
  :style="maxHeight ? { '--ck-code-max-height': maxHeight } : undefined"
291
- />
314
+ >
315
+ <textarea
316
+ v-if="isFallback"
317
+ ref="fallbackRef"
318
+ class="ck-code-editor__fallback"
319
+ :value="modelValue"
320
+ :placeholder="placeholder"
321
+ :aria-label="ariaLabel"
322
+ :readonly="readonly"
323
+ :disabled="disabled"
324
+ :wrap="lineWrapping ? 'soft' : 'off'"
325
+ spellcheck="false"
326
+ @input="onFallbackInput"
327
+ @focus="emit('focus')"
328
+ />
329
+ </div>
292
330
  </component>
293
331
  </div>
294
332
  </template>
@@ -344,6 +382,37 @@ defineExpose({ focus, getCode });
344
382
  font-weight: 500;
345
383
  }
346
384
 
385
+ /* A column so the stand-in below grows into a host a consumer has stretched
386
+ * (as a fixed-height dialog does), rather than sitting at its minimum height
387
+ * at the top of an empty panel. */
388
+ .ck-code-editor--fallback .ck-code-editor__host {
389
+ display: flex;
390
+ flex-direction: column;
391
+ }
392
+
393
+ /* Stand-in for when CodeMirror fails to load: same text metrics and insets as
394
+ * the editor without a gutter, so the field reads as the same surface. Where
395
+ * supported, it grows with its content the way the editor does. */
396
+ .ck-code-editor__fallback {
397
+ display: block;
398
+ flex: 1 1 auto;
399
+ width: 100%;
400
+ min-height: 5rem;
401
+ field-sizing: content;
402
+ max-height: var(--ck-code-max-height, none);
403
+ resize: none;
404
+ border: 0;
405
+ outline: none;
406
+ padding: 0.75rem 1.25rem 1rem 1rem;
407
+ background: var(--ck-cm-bg);
408
+ color: var(--ck-cm-foreground);
409
+ font-family: Saans, sans-serif;
410
+ font-size: 0.875rem;
411
+ font-weight: 400;
412
+ line-height: 1.25rem;
413
+ }
414
+
415
+ .ck-code-editor__fallback::placeholder,
347
416
  .ck-code-editor__host :deep(.cm-placeholder) {
348
417
  color: oklch(var(--content-on-surface-muted));
349
418
  }
@@ -151,10 +151,14 @@ function baseExtensions(options: CreateCodeMirrorOptions): Extension[] {
151
151
  /**
152
152
  * Build a CodeMirror view themed like `UiCodeBlock`. Loaded lazily by
153
153
  * `CkCodeEditor` so CodeMirror stays out of the package's main chunk.
154
+ *
155
+ * The view is built at once, as plain text, and the grammar is applied when it
156
+ * arrives: the placeholder, gutter and content must not wait on a network
157
+ * request that may be slow or never finish. Only the syntax colours do.
154
158
  */
155
- export async function createCodeMirror(
159
+ export function createCodeMirror(
156
160
  options: CreateCodeMirrorOptions,
157
- ): Promise<CodeMirrorHandle> {
161
+ ): CodeMirrorHandle {
158
162
  // One set per view: a compartment is the key a reconfigure targets, so
159
163
  // sharing them across editors would make the instances interfere.
160
164
  const languageSlot = new Compartment();
@@ -164,9 +168,10 @@ export async function createCodeMirror(
164
168
  const badgeSlot = new Compartment();
165
169
  const lineSeparatorSlot = new Compartment();
166
170
 
167
- const support = await resolveLanguage(options.language);
168
171
  // Guards against out-of-order grammar loads; see `setLanguage` below.
169
172
  let languageRequestId = 0;
173
+ // A grammar can land after the view is gone; it must not touch it then.
174
+ let destroyed = false;
170
175
 
171
176
  const view = new EditorView({
172
177
  parent: options.parent,
@@ -174,7 +179,7 @@ export async function createCodeMirror(
174
179
  doc: options.doc,
175
180
  extensions: [
176
181
  ...baseExtensions(options),
177
- languageSlot.of(support ? [support] : []),
182
+ languageSlot.of([]),
178
183
  readonlySlot.of(readonlyState(options.readonly)),
179
184
  lineNumbersSlot.of(options.showLineNumbers ? lineNumbers() : []),
180
185
  wrappingSlot.of(options.lineWrapping ? EditorView.lineWrapping : []),
@@ -187,9 +192,29 @@ export async function createCodeMirror(
187
192
  });
188
193
 
189
194
  function reconfigure(slot: Compartment, extension: Extension): void {
195
+ if (destroyed) return;
190
196
  view.dispatch({ effects: slot.reconfigure(extension) });
191
197
  }
192
198
 
199
+ async function setLanguage(language: string): Promise<void> {
200
+ // Grammars load over the network and out of order, so a slow one must not
201
+ // land on top of a language chosen after it.
202
+ const request = ++languageRequestId;
203
+ const next = await resolveLanguage(language);
204
+ if (request !== languageRequestId) return;
205
+ try {
206
+ reconfigure(languageSlot, next ? [next] : []);
207
+ } catch (error) {
208
+ // Typically a grammar built against another `@codemirror/state` copy.
209
+ // The editor keeps working as plain text; say why the colours are gone.
210
+ // eslint-disable-next-line no-console
211
+ console.warn(`[CkCodeEditor] Could not apply "${language}":`, error);
212
+ }
213
+ }
214
+
215
+ // Nothing waits on this; a failed grammar already resolves to plain text.
216
+ setLanguage(options.language).catch(() => undefined);
217
+
193
218
  return {
194
219
  view,
195
220
  getCode: () => readDoc(view.state),
@@ -208,14 +233,7 @@ export async function createCodeMirror(
208
233
  selection: { anchor: cursor },
209
234
  });
210
235
  },
211
- setLanguage: async (language) => {
212
- // Grammars load over the network and out of order, so a slow one must not
213
- // land on top of a language chosen after it.
214
- const request = ++languageRequestId;
215
- const next = await resolveLanguage(language);
216
- if (request !== languageRequestId) return;
217
- reconfigure(languageSlot, next ? [next] : []);
218
- },
236
+ setLanguage,
219
237
  setReadonly: (readonly) => {
220
238
  reconfigure(readonlySlot, readonlyState(readonly));
221
239
  },
@@ -229,6 +247,9 @@ export async function createCodeMirror(
229
247
  reconfigure(badgeSlot, rule ? badgeExtension(rule) : []);
230
248
  },
231
249
  focus: () => view.focus(),
232
- destroy: () => view.destroy(),
250
+ destroy: () => {
251
+ destroyed = true;
252
+ view.destroy();
253
+ },
233
254
  };
234
255
  }
@@ -1,2 +1,3 @@
1
1
  export { default as CkCodeEditor } from './CkCodeEditor.vue';
2
+ export { preloadCkCodeEditor } from './loadCodeMirror';
2
3
  export type * from './types';
@@ -0,0 +1,28 @@
1
+ import type * as CodeMirrorModule from './createCodeMirror';
2
+
3
+ let pending: Promise<typeof CodeMirrorModule> | undefined;
4
+
5
+ /**
6
+ * Import the CodeMirror chunk once and share the result.
7
+ *
8
+ * A failed import is forgotten rather than cached, so the next editor to mount
9
+ * (or the next `preloadCkCodeEditor` call) tries again instead of inheriting the
10
+ * failure.
11
+ */
12
+ export function loadCodeMirror(): Promise<typeof CodeMirrorModule> {
13
+ pending ??= import('./createCodeMirror').catch((error: unknown) => {
14
+ pending = undefined;
15
+ throw error;
16
+ });
17
+ return pending;
18
+ }
19
+
20
+ /**
21
+ * Start fetching `CkCodeEditor`'s CodeMirror chunk ahead of time, for example
22
+ * when a view that will open an editor is shown. The editor then appears
23
+ * without waiting on the network. Safe to call repeatedly; failures are
24
+ * swallowed here and surface, if still failing, when an editor mounts.
25
+ */
26
+ export function preloadCkCodeEditor(): void {
27
+ loadCodeMirror().catch(() => undefined);
28
+ }
@@ -86,7 +86,7 @@ export type {
86
86
  CkMilkdownEditorExposed,
87
87
  CkMilkdownEditorProps,
88
88
  } from './CkMilkdownEditor';
89
- export { CkCodeEditor } from './CkCodeEditor';
89
+ export { CkCodeEditor, preloadCkCodeEditor } from './CkCodeEditor';
90
90
  export type {
91
91
  CkCodeEditorEmits,
92
92
  CkCodeEditorExposed,
@@ -29,6 +29,7 @@ export {
29
29
  CkEditableUserMessage,
30
30
  CkMilkdownEditor,
31
31
  CkCodeEditor,
32
+ preloadCkCodeEditor,
32
33
  } from './base';
33
34
  export type {
34
35
  CkAgentMessageHeaderProps,
package/src/index.ts CHANGED
@@ -101,6 +101,7 @@ export {
101
101
  CkEditableUserMessage,
102
102
  CkMilkdownEditor,
103
103
  CkCodeEditor,
104
+ preloadCkCodeEditor,
104
105
  } from './components';
105
106
  export type {
106
107
  CkAgentMessageHeaderProps,