@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.
- package/dist/{CkHistory.vue_vue_type_script_setup_true_lang-wu1Zrvgc.js → CkHistory.vue_vue_type_script_setup_true_lang-C4XS2QQI.js} +84 -35
- package/dist/chat-kit.css +41 -10
- package/dist/components/base/CkCitation/CkCitation.vue.d.ts +1 -0
- package/dist/components/base/CkCitation/CkCitation.vue.d.ts.map +1 -1
- package/dist/components/base/CkCitation/types.d.ts +6 -0
- package/dist/components/base/CkCitation/types.d.ts.map +1 -1
- package/dist/components/base/CkCitationCard/CkCitationCard.vue.d.ts +1 -0
- package/dist/components/base/CkCitationCard/CkCitationCard.vue.d.ts.map +1 -1
- package/dist/components/base/CkCitationCard/types.d.ts +6 -0
- package/dist/components/base/CkCitationCard/types.d.ts.map +1 -1
- package/dist/components/base/CkCodeEditor/CkCodeEditor.vue.d.ts.map +1 -1
- package/dist/components/base/CkCodeEditor/createCodeMirror.d.ts +5 -1
- package/dist/components/base/CkCodeEditor/createCodeMirror.d.ts.map +1 -1
- package/dist/components/base/CkCodeEditor/index.d.ts +1 -0
- package/dist/components/base/CkCodeEditor/index.d.ts.map +1 -1
- package/dist/components/base/CkCodeEditor/loadCodeMirror.d.ts +17 -0
- package/dist/components/base/CkCodeEditor/loadCodeMirror.d.ts.map +1 -0
- package/dist/components/base/index.d.ts +1 -1
- package/dist/components/base/index.d.ts.map +1 -1
- package/dist/components/index.d.ts +1 -1
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/index.js +3 -2
- package/dist/{createCodeMirror-BO-TWTzn.js → createCodeMirror-CN8xL8qU.js} +20 -10
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -2
- package/package.json +3 -3
- package/src/components/base/CkCitation/CkCitation.vue +2 -0
- package/src/components/base/CkCitation/types.ts +6 -0
- package/src/components/base/CkCitationCard/CkCitationCard.spec.ts +15 -0
- package/src/components/base/CkCitationCard/CkCitationCard.vue +2 -1
- package/src/components/base/CkCitationCard/types.ts +6 -0
- package/src/components/base/CkCodeEditor/CkCodeEditor.spec.ts +71 -0
- package/src/components/base/CkCodeEditor/CkCodeEditor.vue +89 -20
- package/src/components/base/CkCodeEditor/createCodeMirror.ts +34 -13
- package/src/components/base/CkCodeEditor/index.ts +1 -0
- package/src/components/base/CkCodeEditor/loadCodeMirror.ts +28 -0
- package/src/components/base/index.ts +1 -1
- package/src/components/index.ts +1 -0
- 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
|
-
|
|
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(
|
|
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
|
|
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: () =>
|
|
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';
|
package/dist/index.d.ts.map
CHANGED
|
@@ -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,
|
|
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-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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
|
-
|
|
142
|
-
|
|
143
|
-
const handle = await createCodeMirror({
|
|
144
|
+
lastKnownCode = props.modelValue;
|
|
145
|
+
const handle = createCodeMirror({
|
|
144
146
|
parent,
|
|
145
|
-
doc:
|
|
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
|
|
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
|
|
159
|
+
export function createCodeMirror(
|
|
156
160
|
options: CreateCodeMirrorOptions,
|
|
157
|
-
):
|
|
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(
|
|
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
|
|
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: () =>
|
|
250
|
+
destroy: () => {
|
|
251
|
+
destroyed = true;
|
|
252
|
+
view.destroy();
|
|
253
|
+
},
|
|
233
254
|
};
|
|
234
255
|
}
|
|
@@ -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,
|
package/src/components/index.ts
CHANGED