@atlaskit/editor-plugin-autocomplete 2.2.0 → 2.3.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 (30) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/afm-cc/tsconfig.json +0 -1
  3. package/afm-products/tsconfig.json +0 -1
  4. package/build/tsconfig.json +0 -1
  5. package/dist/cjs/pm-plugins/autocomplete-plugin.js +41 -14
  6. package/dist/cjs/pm-plugins/debug-mode.js +24 -0
  7. package/dist/cjs/pm-plugins/slow-lane-client.js +42 -35
  8. package/dist/cjs/pm-plugins/text-predictor.js +12 -32
  9. package/dist/es2019/pm-plugins/autocomplete-plugin.js +41 -14
  10. package/dist/es2019/pm-plugins/debug-mode.js +18 -0
  11. package/dist/es2019/pm-plugins/slow-lane-client.js +29 -23
  12. package/dist/es2019/pm-plugins/text-predictor.js +6 -26
  13. package/dist/esm/pm-plugins/autocomplete-plugin.js +41 -14
  14. package/dist/esm/pm-plugins/debug-mode.js +18 -0
  15. package/dist/esm/pm-plugins/slow-lane-client.js +43 -35
  16. package/dist/esm/pm-plugins/text-predictor.js +11 -31
  17. package/dist/types/pm-plugins/autocomplete-plugin.d.ts +1 -1
  18. package/dist/types/pm-plugins/debug-mode.d.ts +12 -0
  19. package/dist/types/pm-plugins/text-predictor.d.ts +0 -2
  20. package/dist/types-ts4.5/pm-plugins/autocomplete-plugin.d.ts +1 -1
  21. package/dist/types-ts4.5/pm-plugins/debug-mode.d.ts +12 -0
  22. package/dist/types-ts4.5/pm-plugins/text-predictor.d.ts +0 -2
  23. package/package.json +2 -2
  24. package/src/pm-plugins/autocomplete-plugin.ts +48 -12
  25. package/src/pm-plugins/debug-mode.ts +18 -0
  26. package/src/pm-plugins/slow-lane-client.ts +53 -45
  27. package/src/pm-plugins/text-predictor.ts +6 -30
  28. package/tsconfig.app.json +3 -1
  29. package/tsconfig.dev.json +2 -1
  30. package/afm-jira/tsconfig.json +0 -30
@@ -25,7 +25,7 @@ import {
25
25
 
26
26
  const SLOW_LANE_ENDPOINT = '/gateway/api/v1/autocomplete/typeahead-encodings';
27
27
 
28
- export const autocompletePluginKey = new PluginKey('autocomplete');
28
+ export const autocompletePluginKey: PluginKey = new PluginKey('autocomplete');
29
29
 
30
30
  const DEBOUNCE_MS = 150;
31
31
 
@@ -202,7 +202,20 @@ const buildSlowLaneText = (docText: string, context?: AutocompleteContext): stri
202
202
  return lines.join('\n');
203
203
  };
204
204
 
205
- export const createAutocompletePlugin = (options?: AutocompletePluginOptions, api?: ExtractInjectionAPI<AutocompletePlugin>) => {
205
+ const getTypedLengthForPrediction = (textBefore: string): number => {
206
+ if (isWordBoundary(textBefore)) {
207
+ return 0;
208
+ }
209
+
210
+ // eslint-disable-next-line require-unicode-regexp
211
+ const trailingToken = textBefore.match(/([^\s.,;:!?]*)$/)?.[1] ?? '';
212
+ return trailingToken.length;
213
+ };
214
+
215
+ export const createAutocompletePlugin = (
216
+ options?: AutocompletePluginOptions,
217
+ api?: ExtractInjectionAPI<AutocompletePlugin>,
218
+ ): SafePlugin<AutocompletePluginState> => {
206
219
  let debounceTimer: ReturnType<typeof setTimeout> | null = null;
207
220
  let hasIngestedPage = false;
208
221
  let resolvedContext: AutocompleteContext | undefined;
@@ -219,6 +232,25 @@ export const createAutocompletePlugin = (options?: AutocompletePluginOptions, ap
219
232
  * re-showing the same suggestion. Resets to null as soon as the text changes.
220
233
  */
221
234
  let dismissedContext: string | null = null;
235
+ let lastSuggestionTypedLength = 0;
236
+ let lastSuggestionLength = 0;
237
+
238
+ const fireSuggestionInsertedAnalytics = (ghostText: string): void => {
239
+ const typedLength = lastSuggestionTypedLength;
240
+ const suggestionLength = lastSuggestionLength || typedLength + ghostText.length;
241
+ const kssDelta = suggestionLength - typedLength;
242
+
243
+ api?.analytics?.actions.fireAnalyticsEvent({
244
+ action: ACTION.SUGGESTION_INSERTED,
245
+ actionSubject: ACTION_SUBJECT.CONTEXTUAL_TYPEAHEAD,
246
+ eventType: EVENT_TYPE.TRACK,
247
+ attributes: {
248
+ suggestionLength,
249
+ typedLength,
250
+ kssDelta,
251
+ },
252
+ });
253
+ };
222
254
 
223
255
  const slowLaneClient = createSlowLaneClient({
224
256
  baseUrl: '',
@@ -263,6 +295,10 @@ export const createAutocompletePlugin = (options?: AutocompletePluginOptions, ap
263
295
  const prediction = predict(textBefore);
264
296
 
265
297
  if (prediction && prediction.length > 0) {
298
+ const typedLength = getTypedLengthForPrediction(textBefore);
299
+ lastSuggestionTypedLength = typedLength;
300
+ lastSuggestionLength = typedLength + prediction.length;
301
+
266
302
  showGhostText(view, prediction, selection.from);
267
303
  if (prediction !== lastShownGhostText) {
268
304
  lastShownGhostText = prediction;
@@ -359,28 +395,28 @@ export const createAutocompletePlugin = (options?: AutocompletePluginOptions, ap
359
395
 
360
396
  handleKeyDown: keydownHandler({
361
397
  Tab: (state: EditorState, dispatch?: (tr: Transaction) => void) => {
398
+ const pluginState = autocompletePluginKey.getState(state) as
399
+ | AutocompletePluginState
400
+ | undefined;
401
+ const ghostText = pluginState?.ghostText ?? '';
362
402
  const accepted = acceptGhostText(state, dispatch);
363
403
  if (accepted) {
364
404
  justAccepted = true;
365
405
  lastShownGhostText = '';
366
- api?.analytics?.actions.fireAnalyticsEvent({
367
- action: ACTION.SUGGESTION_INSERTED,
368
- actionSubject: ACTION_SUBJECT.CONTEXTUAL_TYPEAHEAD,
369
- eventType: EVENT_TYPE.TRACK,
370
- });
406
+ fireSuggestionInsertedAnalytics(ghostText);
371
407
  }
372
408
  return accepted;
373
409
  },
374
410
  ArrowRight: (state: EditorState, dispatch?: (tr: Transaction) => void) => {
411
+ const pluginState = autocompletePluginKey.getState(state) as
412
+ | AutocompletePluginState
413
+ | undefined;
414
+ const ghostText = pluginState?.ghostText ?? '';
375
415
  const accepted = acceptGhostText(state, dispatch);
376
416
  if (accepted) {
377
417
  justAccepted = true;
378
418
  lastShownGhostText = '';
379
- api?.analytics?.actions.fireAnalyticsEvent({
380
- action: ACTION.SUGGESTION_INSERTED,
381
- actionSubject: ACTION_SUBJECT.CONTEXTUAL_TYPEAHEAD,
382
- eventType: EVENT_TYPE.TRACK,
383
- });
419
+ fireSuggestionInsertedAnalytics(ghostText);
384
420
  }
385
421
  return accepted;
386
422
  },
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Contextual Typeahead Completions (CTC) debug logging utility.
3
+ *
4
+ * Logs are silent by default. To enable in any environment (dev, staging, prod):
5
+ *
6
+ * localStorage.setItem('atl-ctc-dbg', '1')
7
+ *
8
+ * Then reload the page. To disable:
9
+ *
10
+ * localStorage.removeItem('atl-ctc-dbg')
11
+ */
12
+ export const isAutocompleteDebugEnabled = (): boolean => {
13
+ try {
14
+ return typeof localStorage !== 'undefined' && localStorage.getItem('atl-ctc-dbg') === '1';
15
+ } catch {
16
+ return false;
17
+ }
18
+ };
@@ -7,6 +7,8 @@
7
7
  * Response: { semantic_vector: number[], lm_logits: Record<string, number> }
8
8
  */
9
9
 
10
+ import { isAutocompleteDebugEnabled } from './debug-mode';
11
+
10
12
  // ─── Types ───────────────────────────────────────────────────────────────────
11
13
 
12
14
  /** Request payload for typeahead-encodings endpoint. */
@@ -95,19 +97,20 @@ export const createSlowLaneClient = (
95
97
  session_id: sessionId,
96
98
  };
97
99
 
98
- // Log the exact payload being sent so the comment/reply structure is visible
99
- // eslint-disable-next-line no-console
100
- console.groupCollapsed(
101
- `%c[SlowLane] %c📤 Sending context | ${text.length} chars`,
102
- 'color: #9c27b0; font-weight: bold;',
103
- 'color: inherit;',
104
- );
105
- text.split('\n').forEach((line, i, arr) => {
100
+ if (isAutocompleteDebugEnabled()) {
101
+ // eslint-disable-next-line no-console
102
+ console.groupCollapsed(
103
+ `%c[SlowLane] %c📤 Sending context | ${text.length} chars`,
104
+ 'color: #9c27b0; font-weight: bold;',
105
+ 'color: inherit;',
106
+ );
107
+ text.split('\n').forEach((line, i, arr) => {
108
+ // eslint-disable-next-line no-console
109
+ console.log(` ${i === arr.length - 1 ? '▶' : ' '} ${line}`);
110
+ });
106
111
  // eslint-disable-next-line no-console
107
- console.log(` ${i === arr.length - 1 ? '▶' : ' '} ${line}`);
108
- });
109
- // eslint-disable-next-line no-console
110
- console.groupEnd();
112
+ console.groupEnd();
113
+ }
111
114
 
112
115
  try {
113
116
  const res = await fetchFn(url, {
@@ -119,12 +122,14 @@ export const createSlowLaneClient = (
119
122
  if (!res.ok) {
120
123
  storedContextVector = null;
121
124
  storedLmLogits = null;
122
- // eslint-disable-next-line no-console
123
- console.log(
124
- `%c[SlowLane] %c❌ Request failed (${res.status})`,
125
- 'color: #9c27b0; font-weight: bold;',
126
- 'color: #f44336;',
127
- );
125
+ if (isAutocompleteDebugEnabled()) {
126
+ // eslint-disable-next-line no-console
127
+ console.log(
128
+ `%c[SlowLane] %c❌ Request failed (${res.status})`,
129
+ 'color: #9c27b0; font-weight: bold;',
130
+ 'color: #f44336;',
131
+ );
132
+ }
128
133
  return;
129
134
  }
130
135
 
@@ -142,27 +147,28 @@ export const createSlowLaneClient = (
142
147
  storedLmLogits = null;
143
148
  }
144
149
 
145
- // Log what came back so it can be correlated with the next prediction group
146
- // eslint-disable-next-line no-console
147
- console.groupCollapsed(
148
- `%c[SlowLane] %c📥 Response received`,
149
- 'color: #9c27b0; font-weight: bold;',
150
- 'color: inherit;',
151
- );
152
- // eslint-disable-next-line no-console
153
- console.log(
154
- storedContextVector
155
- ? `✅ semantic_vector: ${storedContextVector.length} dims`
156
- : '❌ No semantic_vector',
157
- );
158
- // eslint-disable-next-line no-console
159
- console.log(
160
- storedLmLogits
161
- ? `✅ lm_logits: ${Object.keys(storedLmLogits).length} tokens`
162
- : '❌ No lm_logits',
163
- );
164
- // eslint-disable-next-line no-console
165
- console.groupEnd();
150
+ if (isAutocompleteDebugEnabled()) {
151
+ // eslint-disable-next-line no-console
152
+ console.groupCollapsed(
153
+ `%c[SlowLane] %c📥 Response received`,
154
+ 'color: #9c27b0; font-weight: bold;',
155
+ 'color: inherit;',
156
+ );
157
+ // eslint-disable-next-line no-console
158
+ console.log(
159
+ storedContextVector
160
+ ? `✅ semantic_vector: ${storedContextVector.length} dims`
161
+ : '❌ No semantic_vector',
162
+ );
163
+ // eslint-disable-next-line no-console
164
+ console.log(
165
+ storedLmLogits
166
+ ? `✅ lm_logits: ${Object.keys(storedLmLogits).length} tokens`
167
+ : '❌ No lm_logits',
168
+ );
169
+ // eslint-disable-next-line no-console
170
+ console.groupEnd();
171
+ }
166
172
 
167
173
  onUpdate?.({
168
174
  textLength: text.length,
@@ -173,12 +179,14 @@ export const createSlowLaneClient = (
173
179
  } catch (e) {
174
180
  storedContextVector = null;
175
181
  storedLmLogits = null;
176
- // eslint-disable-next-line no-console
177
- console.log(
178
- '%c[SlowLane] %c❌ Network error — context cleared',
179
- 'color: #9c27b0; font-weight: bold;',
180
- 'color: #f44336;',
181
- );
182
+ if (isAutocompleteDebugEnabled()) {
183
+ // eslint-disable-next-line no-console
184
+ console.log(
185
+ '%c[SlowLane] %c❌ Network error — context cleared',
186
+ 'color: #9c27b0; font-weight: bold;',
187
+ 'color: #f44336;',
188
+ );
189
+ }
182
190
  }
183
191
  };
184
192
 
@@ -20,6 +20,7 @@ import l3VocabularyData from './data/l3_vocabulary.json';
20
20
  import vocabularyData from './data/vocabulary_10k.json';
21
21
  import wordIndexData from './data/word_index_10k.json';
22
22
  // import { rankCandidates, isGrammarAllowed } from './scoring-pipeline';
23
+ import { isAutocompleteDebugEnabled } from './debug-mode';
23
24
  import { rankCandidates, STAGE1_WEIGHT, STAGE2_WEIGHT, MIN_STAGE1_SCORE } from './scoring-pipeline';
24
25
  import type { ScoringCandidate } from './scoring-pipeline';
25
26
  import { getStoredContextVector, getStoredLmLogits } from './slow-lane-client';
@@ -183,7 +184,7 @@ export const initL3Vocabulary = (l3Words: string[]): void => {
183
184
  // loses to any domain word in Stage 1, but still scores above 0.
184
185
  l3Trie.insert(word, L3_BASELINE_FREQ, 0, 0);
185
186
  }
186
- if (debugMode) {
187
+ if (isAutocompleteDebugEnabled()) {
187
188
  // eslint-disable-next-line no-console
188
189
  console.log(`[text-predictor] L3 General English loaded: ${l3Words.length} words`);
189
190
  }
@@ -199,8 +200,6 @@ let vectorStore: VectorStore | null = null;
199
200
 
200
201
  let vectorsLoadStarted = false;
201
202
 
202
- let debugMode = true;
203
-
204
203
  let lastPredictionDebug: {
205
204
  contextWords: string[];
206
205
  currentWord: string;
@@ -216,8 +215,6 @@ let lastPredictionDebug: {
216
215
  }>;
217
216
  } | null = null;
218
217
 
219
- let hasLoggedSemanticActive = false;
220
-
221
218
  /** Get vector for a word from the store. */
222
219
  const getWordVector = (word: string): Float32Array | null => {
223
220
  if (!vectorStore) {
@@ -306,18 +303,6 @@ const extractPreviousWord = (text: string): string => {
306
303
 
307
304
  // ─── Debug Helpers ───────────────────────────────────────────────────────────
308
305
 
309
- /** Enable or disable debug logging. Also checks localStorage key `autocomplete-debug`. */
310
- export const setDebugMode = (enabled: boolean): void => {
311
- debugMode = enabled;
312
- };
313
-
314
- /** Check localStorage for autocomplete-debug on first access. */
315
- const ensureDebugModeFromStorage = (): void => {
316
- if (typeof localStorage !== 'undefined' && localStorage.getItem('autocomplete-debug') === '1') {
317
- debugMode = true;
318
- }
319
- };
320
-
321
306
  /**
322
307
  * Get predictor status for debugging.
323
308
  * vectorsLoaded: true when semantic scoring is active
@@ -329,7 +314,6 @@ export const getPredictorStatus = (): {
329
314
  vectorsLoadStarted: boolean;
330
315
  wordCount: number;
331
316
  } => {
332
- ensureDebugModeFromStorage();
333
317
  return {
334
318
  vectorsLoaded: vectorStore !== null,
335
319
  wordCount: vectorStore ? Object.keys(vectorStore.wordIndex).length : 0,
@@ -356,7 +340,6 @@ export const getLastPredictionDebug = (): {
356
340
  word: string;
357
341
  }>;
358
342
  } | null => {
359
- ensureDebugModeFromStorage();
360
343
  return lastPredictionDebug;
361
344
  };
362
345
 
@@ -401,7 +384,7 @@ export const ingestDocumentPage = (pageContent: string | undefined): void => {
401
384
  }
402
385
  }
403
386
 
404
- if (debugMode && validBoostedWords.size > 0) {
387
+ if (isAutocompleteDebugEnabled() && validBoostedWords.size > 0) {
405
388
  // eslint-disable-next-line no-console
406
389
  console.groupCollapsed(
407
390
  `%c[L1 Session] %cPrimed ${validBoostedWords.size} valid dictionary words from page`,
@@ -416,8 +399,6 @@ export const ingestDocumentPage = (pageContent: string | undefined): void => {
416
399
  };
417
400
 
418
401
  export const predict = (textBefore: string): string | null => {
419
- ensureDebugModeFromStorage();
420
-
421
402
  if (!isInitialized) {
422
403
  loadDefaultVocabulary();
423
404
  }
@@ -506,7 +487,7 @@ export const predict = (textBefore: string): string | null => {
506
487
  const lmLogits = getStoredLmLogits();
507
488
 
508
489
  // Raw LM Output Logger
509
- if (debugMode && lmLogits && Object.keys(lmLogits).length > 0) {
490
+ if (isAutocompleteDebugEnabled() && lmLogits && Object.keys(lmLogits).length > 0) {
510
491
  const rawLmTop = Object.entries(lmLogits)
511
492
  .sort((a, b) => b[1] - a[1])
512
493
  .slice(0, 5)
@@ -520,10 +501,6 @@ export const predict = (textBefore: string): string | null => {
520
501
 
521
502
  const mode: 'cold' | 'warm' = contextVector ? 'warm' : 'cold';
522
503
 
523
- if (debugMode && contextVector && !hasLoggedSemanticActive) {
524
- hasLoggedSemanticActive = true;
525
- }
526
-
527
504
  // Build ScoringCandidate array from TrieNodes
528
505
  const scoringCandidates: ScoringCandidate[] = candidates.map(({ word, node }) => ({
529
506
  word,
@@ -556,7 +533,7 @@ export const predict = (textBefore: string): string | null => {
556
533
  const suggestion =
557
534
  best && best.finalScore >= MIN_SCORE_THRESHOLD ? best.word.slice(currentWord.length) : null;
558
535
 
559
- if (debugMode) {
536
+ if (isAutocompleteDebugEnabled()) {
560
537
  const latencyMs = performance.now() - t0;
561
538
  const tokens = tokenize(trimmed);
562
539
  const contextWords = tokens.slice(-CONTEXT_WORDS);
@@ -772,8 +749,7 @@ export const loadVectorsAsync = async (options?: {
772
749
  const dim = float32.length / nWords;
773
750
 
774
751
  vectorStore = { float32, wordIndex, dim };
775
- ensureDebugModeFromStorage();
776
- if (debugMode) {
752
+ if (isAutocompleteDebugEnabled()) {
777
753
  // eslint-disable-next-line no-console
778
754
  console.log('[text-predictor] Vectors loaded:', {
779
755
  wordCount: nWords,
package/tsconfig.app.json CHANGED
@@ -2,6 +2,7 @@
2
2
  "extends": "../../../tsconfig.base.json",
3
3
  "include": ["./src/**/*.ts", "./src/**/*.tsx", "./src/**/*.json"],
4
4
  "exclude": [
5
+ "**/*.docs.tsx",
5
6
  "**/docs/**/*",
6
7
  "**/__tests__/**/*",
7
8
  "**/vr-tests/**/*",
@@ -29,7 +30,8 @@
29
30
  "compilerOptions": {
30
31
  "composite": true,
31
32
  "outDir": "../../../tsDist/@atlaskit__editor-plugin-autocomplete/app",
32
- "resolveJsonModule": true
33
+ "resolveJsonModule": true,
34
+ "isolatedDeclarations": true
33
35
  },
34
36
  "references": [
35
37
  {
package/tsconfig.dev.json CHANGED
@@ -28,7 +28,8 @@
28
28
  "exclude": ["./dist/**/*", "./build/**/*", "./node_modules/**/*"],
29
29
  "compilerOptions": {
30
30
  "composite": true,
31
- "outDir": "../../../tsDist/@atlaskit__editor-plugin-autocomplete/dev"
31
+ "outDir": "../../../tsDist/@atlaskit__editor-plugin-autocomplete/dev",
32
+ "isolatedDeclarations": true
32
33
  },
33
34
  "references": [
34
35
  {
@@ -1,30 +0,0 @@
1
- {
2
- "extends": "../../../../tsconfig.local-consumption.json",
3
- "compilerOptions": {
4
- "target": "es5",
5
- "outDir": "../../../../../jira/tsDist/@atlaskit__editor-plugin-autocomplete/app",
6
- "rootDir": "../",
7
- "composite": true,
8
- "noCheck": true,
9
- "resolveJsonModule": true
10
- },
11
- "include": [
12
- "../src/**/*.ts",
13
- "../src/**/*.tsx"
14
- ],
15
- "exclude": [
16
- "../src/**/__tests__/*",
17
- "../src/**/*.test.*",
18
- "../src/**/test.*",
19
- "../src/**/mocks/*",
20
- "../src/**/examples.*"
21
- ],
22
- "references": [
23
- {
24
- "path": "../../editor-prosemirror/afm-jira/tsconfig.json"
25
- },
26
- {
27
- "path": "../../editor-common/afm-jira/tsconfig.json"
28
- }
29
- ]
30
- }