@atlaskit/editor-plugin-autocomplete 2.4.0 → 2.5.1

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 (31) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/afm-cc/tsconfig.json +3 -0
  3. package/afm-products/tsconfig.json +3 -0
  4. package/dist/cjs/analytics/ufo.js +111 -0
  5. package/dist/cjs/pm-plugins/autocomplete-plugin.js +126 -64
  6. package/dist/cjs/pm-plugins/local-slow-lane-client.js +452 -0
  7. package/dist/cjs/pm-plugins/slow-lane-client.js +45 -16
  8. package/dist/cjs/pm-plugins/text-predictor.js +72 -40
  9. package/dist/es2019/analytics/ufo.js +110 -0
  10. package/dist/es2019/pm-plugins/autocomplete-plugin.js +132 -72
  11. package/dist/es2019/pm-plugins/local-slow-lane-client.js +361 -0
  12. package/dist/es2019/pm-plugins/slow-lane-client.js +29 -2
  13. package/dist/es2019/pm-plugins/text-predictor.js +48 -15
  14. package/dist/esm/analytics/ufo.js +105 -0
  15. package/dist/esm/pm-plugins/autocomplete-plugin.js +126 -64
  16. package/dist/esm/pm-plugins/local-slow-lane-client.js +443 -0
  17. package/dist/esm/pm-plugins/slow-lane-client.js +45 -16
  18. package/dist/esm/pm-plugins/text-predictor.js +73 -40
  19. package/dist/types/analytics/ufo.d.ts +38 -0
  20. package/dist/types/pm-plugins/autocomplete-plugin.d.ts +5 -0
  21. package/dist/types/pm-plugins/local-slow-lane-client.d.ts +102 -0
  22. package/dist/types-ts4.5/analytics/ufo.d.ts +38 -0
  23. package/dist/types-ts4.5/pm-plugins/autocomplete-plugin.d.ts +5 -0
  24. package/dist/types-ts4.5/pm-plugins/local-slow-lane-client.d.ts +102 -0
  25. package/package.json +7 -2
  26. package/src/analytics/ufo.ts +132 -0
  27. package/src/pm-plugins/autocomplete-plugin.ts +125 -64
  28. package/src/pm-plugins/local-slow-lane-client.ts +487 -0
  29. package/src/pm-plugins/slow-lane-client.ts +28 -2
  30. package/src/pm-plugins/text-predictor.ts +42 -12
  31. package/tsconfig.app.json +3 -0
@@ -1,4 +1,5 @@
1
1
  import { ACTION, ACTION_SUBJECT, EVENT_TYPE } from '@atlaskit/editor-common/analytics';
2
+ import { logException } from '@atlaskit/editor-common/monitoring';
2
3
  import { SafePlugin } from '@atlaskit/editor-common/safe-plugin';
3
4
  import type { ExtractInjectionAPI } from '@atlaskit/editor-common/types';
4
5
  import { keydownHandler } from '@atlaskit/editor-prosemirror/keymap';
@@ -14,6 +15,7 @@ import type { EditorView } from '@atlaskit/editor-prosemirror/view';
14
15
  import type { AutocompletePlugin } from '../autocompletePluginType';
15
16
 
16
17
  import { createGhostTextDecorationSet } from './ghost-text-decoration';
18
+ import { createLocalSlowLaneClient, type LocalSlowLaneClient } from './local-slow-lane-client';
17
19
  import { createSlowLaneClient, setDefaultSlowLaneClient, isWordBoundary } from './slow-lane-client';
18
20
  import {
19
21
  predict,
@@ -23,12 +25,14 @@ import {
23
25
  ingestDocumentPage,
24
26
  } from './text-predictor';
25
27
 
26
- const SLOW_LANE_ENDPOINT = '/gateway/api/v1/autocomplete/typeahead-encodings';
27
-
28
28
  export const autocompletePluginKey: PluginKey = new PluginKey('autocomplete');
29
29
 
30
30
  const DEBOUNCE_MS = 150;
31
31
 
32
+ const hasDestroy = (
33
+ client: ReturnType<typeof createSlowLaneClient> | LocalSlowLaneClient,
34
+ ): client is LocalSlowLaneClient => 'destroy' in client && typeof client.destroy === 'function';
35
+
32
36
  export interface AutocompletePluginState {
33
37
  /** The decoration set containing the ghost text widget */
34
38
  decorationSet: DecorationSet;
@@ -97,14 +101,18 @@ const setAutocompleteMeta = (
97
101
  let lastShownGhostText = '';
98
102
 
99
103
  const showGhostText = (view: EditorView, text: string, position: number): void => {
100
- const { state, dispatch } = view;
101
- const decorationSet = createGhostTextDecorationSet(state, position, text);
102
- const tr = setAutocompleteMeta(state.tr, {
103
- ghostText: text,
104
- ghostPosition: position,
105
- decorationSet,
106
- });
107
- dispatch(tr);
104
+ try {
105
+ const { state, dispatch } = view;
106
+ const decorationSet = createGhostTextDecorationSet(state, position, text);
107
+ const tr = setAutocompleteMeta(state.tr, {
108
+ ghostText: text,
109
+ ghostPosition: position,
110
+ decorationSet,
111
+ });
112
+ dispatch(tr);
113
+ } catch (error) {
114
+ logException(error as Error, { location: 'editor-plugin-autocomplete/showGhostText' });
115
+ }
108
116
  };
109
117
 
110
118
  /**
@@ -117,12 +125,17 @@ const clearGhostText = (state: EditorState, dispatch?: (tr: Transaction) => void
117
125
  }
118
126
 
119
127
  if (dispatch) {
120
- const tr = setAutocompleteMeta(state.tr, {
121
- ghostText: '',
122
- ghostPosition: -1,
123
- decorationSet: DecorationSet.empty,
124
- });
125
- dispatch(tr);
128
+ try {
129
+ const tr = setAutocompleteMeta(state.tr, {
130
+ ghostText: '',
131
+ ghostPosition: -1,
132
+ decorationSet: DecorationSet.empty,
133
+ });
134
+ dispatch(tr);
135
+ } catch (error) {
136
+ logException(error as Error, { location: 'editor-plugin-autocomplete/clearGhostText' });
137
+ return false;
138
+ }
126
139
  }
127
140
  return true;
128
141
  };
@@ -137,14 +150,19 @@ const acceptGhostText = (state: EditorState, dispatch?: (tr: Transaction) => voi
137
150
  }
138
151
 
139
152
  if (dispatch) {
140
- const { ghostText, ghostPosition } = pluginState;
141
- let tr = state.tr.insertText(ghostText, ghostPosition);
142
- tr = setAutocompleteMeta(tr, {
143
- ghostText: '',
144
- ghostPosition: -1,
145
- decorationSet: DecorationSet.empty,
146
- });
147
- dispatch(tr);
153
+ try {
154
+ const { ghostText, ghostPosition } = pluginState;
155
+ let tr = state.tr.insertText(ghostText, ghostPosition);
156
+ tr = setAutocompleteMeta(tr, {
157
+ ghostText: '',
158
+ ghostPosition: -1,
159
+ decorationSet: DecorationSet.empty,
160
+ });
161
+ dispatch(tr);
162
+ } catch (error) {
163
+ logException(error as Error, { location: 'editor-plugin-autocomplete/acceptGhostText' });
164
+ return false;
165
+ }
148
166
  }
149
167
  return true;
150
168
  };
@@ -178,6 +196,11 @@ export interface AutocompletePluginOptions {
178
196
  * Use this to serve vectors from a CDN or media service in production.
179
197
  */
180
198
  getVectorsBinaryUrl?: () => Promise<string>;
199
+ /**
200
+ * When true, uses on-device inference via WebGPU (MLC WebLLM) instead of
201
+ * the network-based slow-lane backend. Defaults to false (network client).
202
+ */
203
+ useLocalModel?: boolean;
181
204
  }
182
205
 
183
206
  /**
@@ -235,6 +258,15 @@ export const createAutocompletePlugin = (
235
258
  let lastSuggestionTypedLength = 0;
236
259
  let lastSuggestionLength = 0;
237
260
 
261
+ const fireSuggestionDismissedAnalytics = (reason: 'escape' | 'blur'): void => {
262
+ api?.analytics?.actions.fireAnalyticsEvent({
263
+ action: ACTION.SUGGESTION_DISMISSED,
264
+ actionSubject: ACTION_SUBJECT.CONTEXTUAL_TYPEAHEAD,
265
+ eventType: EVENT_TYPE.TRACK,
266
+ attributes: { reason },
267
+ });
268
+ };
269
+
238
270
  const fireSuggestionInsertedAnalytics = (ghostText: string): void => {
239
271
  const typedLength = lastSuggestionTypedLength;
240
272
  const suggestionLength = lastSuggestionLength || typedLength + ghostText.length;
@@ -252,10 +284,14 @@ export const createAutocompletePlugin = (
252
284
  });
253
285
  };
254
286
 
255
- const slowLaneClient = createSlowLaneClient({
256
- baseUrl: '',
257
- endpoint: SLOW_LANE_ENDPOINT,
258
- });
287
+ const slowLaneClient = options?.useLocalModel
288
+ ? createLocalSlowLaneClient({
289
+ debounceMs: 300,
290
+ })
291
+ : createSlowLaneClient({
292
+ baseUrl: '',
293
+ debounceMs: 300,
294
+ });
259
295
  setDefaultSlowLaneClient(slowLaneClient);
260
296
 
261
297
  /**
@@ -269,45 +305,51 @@ export const createAutocompletePlugin = (
269
305
  }
270
306
 
271
307
  debounceTimer = setTimeout(() => {
272
- const { state } = view;
273
- const { selection } = state;
308
+ try {
309
+ const { state } = view;
310
+ const { selection } = state;
274
311
 
275
- // Only predict for cursor selections (not range selections)
276
- if (!selection.empty) {
277
- return;
278
- }
312
+ // Only predict for cursor selections (not range selections)
313
+ if (!selection.empty) {
314
+ return;
315
+ }
279
316
 
280
- const textBefore = getTextBeforeCursor(state);
317
+ const textBefore = getTextBeforeCursor(state);
281
318
 
282
- // Suppress re-showing the same suggestion the user just dismissed.
283
- // Once the text context changes (user types or deletes), this clears automatically.
284
- if (textBefore === dismissedContext) {
285
- return;
286
- }
287
- dismissedContext = null;
319
+ // Suppress re-showing the same suggestion the user just dismissed.
320
+ // Once the text context changes (user types or deletes), this clears automatically.
321
+ if (textBefore === dismissedContext) {
322
+ return;
323
+ }
324
+ dismissedContext = null;
288
325
 
289
- // Don't predict if there's not enough context
290
- if (textBefore.trim().length < 3) {
291
- return;
292
- }
326
+ // Don't predict if there's not enough context
327
+ if (textBefore.trim().length < 3) {
328
+ return;
329
+ }
293
330
 
294
- // Tier 1 prediction is synchronous -- no async needed
295
- const prediction = predict(textBefore);
296
-
297
- if (prediction && prediction.length > 0) {
298
- const typedLength = getTypedLengthForPrediction(textBefore);
299
- lastSuggestionTypedLength = typedLength;
300
- lastSuggestionLength = typedLength + prediction.length;
301
-
302
- showGhostText(view, prediction, selection.from);
303
- if (prediction !== lastShownGhostText) {
304
- lastShownGhostText = prediction;
305
- api?.analytics?.actions.fireAnalyticsEvent({
306
- action: ACTION.SUGGESTION_VIEWED,
307
- actionSubject: ACTION_SUBJECT.CONTEXTUAL_TYPEAHEAD,
308
- eventType: EVENT_TYPE.TRACK,
309
- });
331
+ // Tier 1 prediction is synchronous -- no async needed
332
+ const prediction = predict(textBefore);
333
+
334
+ if (prediction && prediction.length > 0) {
335
+ const typedLength = getTypedLengthForPrediction(textBefore);
336
+ lastSuggestionTypedLength = typedLength;
337
+ lastSuggestionLength = typedLength + prediction.length;
338
+
339
+ showGhostText(view, prediction, selection.from);
340
+ if (prediction !== lastShownGhostText) {
341
+ lastShownGhostText = prediction;
342
+ api?.analytics?.actions.fireAnalyticsEvent({
343
+ action: ACTION.SUGGESTION_VIEWED,
344
+ actionSubject: ACTION_SUBJECT.CONTEXTUAL_TYPEAHEAD,
345
+ eventType: EVENT_TYPE.TRACK,
346
+ });
347
+ }
310
348
  }
349
+ } catch (error) {
350
+ logException(error as Error, {
351
+ location: 'editor-plugin-autocomplete/schedulePrediction',
352
+ });
311
353
  }
312
354
  }, DEBOUNCE_MS);
313
355
  };
@@ -424,6 +466,7 @@ export const createAutocompletePlugin = (
424
466
  const didClear = clearGhostText(state, dispatch);
425
467
  if (didClear) {
426
468
  dismissedContext = getTextBeforeCursor(state);
469
+ fireSuggestionDismissedAnalytics('escape');
427
470
  }
428
471
  return didClear;
429
472
  },
@@ -436,12 +479,23 @@ export const createAutocompletePlugin = (
436
479
  | undefined;
437
480
  if (pluginState?.ghostText) {
438
481
  clearGhostText(view.state, view.dispatch);
482
+ fireSuggestionDismissedAnalytics('blur');
439
483
  }
440
484
  return false;
441
485
  },
442
486
  focus: () => {
443
- loadDefaultVocabulary();
444
- loadVectorsAsync({ getBinaryUrl: options?.getVectorsBinaryUrl }).catch(() => {});
487
+ try {
488
+ loadDefaultVocabulary();
489
+ } catch (error) {
490
+ logException(error as Error, {
491
+ location: 'editor-plugin-autocomplete/loadDefaultVocabulary',
492
+ });
493
+ }
494
+ loadVectorsAsync({ getBinaryUrl: options?.getVectorsBinaryUrl }).catch((error) => {
495
+ logException(error as Error, {
496
+ location: 'editor-plugin-autocomplete/loadVectorsAsync',
497
+ });
498
+ });
445
499
  if (!hasIngestedPage) {
446
500
  hasIngestedPage = true;
447
501
  if (options?.getContext) {
@@ -461,7 +515,11 @@ export const createAutocompletePlugin = (
461
515
  }
462
516
  context.siblingCommentsContents?.forEach(ingestDocumentPage);
463
517
  })
464
- .catch(() => {});
518
+ .catch((error) => {
519
+ logException(error as Error, {
520
+ location: 'editor-plugin-autocomplete/getContext',
521
+ });
522
+ });
465
523
  }
466
524
  }
467
525
  return false;
@@ -503,6 +561,9 @@ export const createAutocompletePlugin = (
503
561
  if (debounceTimer) {
504
562
  clearTimeout(debounceTimer);
505
563
  }
564
+ if (hasDestroy(slowLaneClient)) {
565
+ slowLaneClient.destroy();
566
+ }
506
567
  setDefaultSlowLaneClient(null);
507
568
  },
508
569
  }),