@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
@@ -0,0 +1,361 @@
1
+ /**
2
+ * Local Slow Lane Client: On-device inference via @mlc-ai/web-llm.
3
+ *
4
+ * Drop-in replacement for the network-based slow-lane-client. Instead of
5
+ * calling a backend API, this client uses MLC WebLLM to run a small language
6
+ * model (SmolLM 135M) directly in the browser via WebGPU.
7
+ *
8
+ * ── Why main thread (no Web Worker)? ─────────────────────────────────────
9
+ * SmolLM 135M is small enough (~270 MB weights, 350-400 MB VRAM) that
10
+ * WebGPU inference on the main thread is production-viable:
11
+ *
12
+ * - WebGPU GPU compute is inherently async (doesn't block the main thread)
13
+ * - CPU overhead (tokenization + post-processing) is only 5-10 ms
14
+ * - Single forward pass latency is 50-150 ms — well within autocomplete
15
+ * expectations (~250 ms between word boundaries)
16
+ *
17
+ * This avoids all the complexity of Web Workers:
18
+ * - No CSP workarounds (blob URLs, inline scripts)
19
+ * - No bundler configuration (worker-plugin, import.meta.url)
20
+ * - No message passing protocol
21
+ * - Standard npm import — just works
22
+ *
23
+ * ── Interface ────────────────────────────────────────────────────────────
24
+ * Same shape as createSlowLaneClient so text-predictor.ts needs zero changes.
25
+ * The client exposes getContextVector() and getLmLogits() which are populated
26
+ * asynchronously after each updateContext() call.
27
+ */
28
+
29
+ import { isAutocompleteDebugEnabled } from './debug-mode';
30
+ import { isWordBoundary } from './slow-lane-client';
31
+ const startsWithAsciiLetter = value => {
32
+ const firstChar = value.charCodeAt(0);
33
+ return firstChar >= 65 && firstChar <= 90 || firstChar >= 97 && firstChar <= 122;
34
+ };
35
+
36
+ // ─── Types ───────────────────────────────────────────────────────────────────
37
+
38
+ // Same return type as createSlowLaneClient for drop-in compatibility
39
+
40
+ // ─── Constants ───────────────────────────────────────────────────────────────
41
+
42
+ const DEFAULT_DEBOUNCE_MS = 300;
43
+ export const LOCAL_MLC_MODEL_ID = 'SmolLM2-135M-Instruct-q0f16-MLC';
44
+
45
+ /** HF root for the default weights (includes `tensor-cache.json` for WebLLM 0.2+). */
46
+ export const LOCAL_MLC_HF_MODEL_REPO = 'https://huggingface.co/mlc-ai/SmolLM2-135M-Instruct-q0f16-MLC';
47
+ export const LOCAL_MLC_MODEL_LIB_WASM_NAME = 'SmolLM2-135M-Instruct-q0f16-ctx4k_cs1k-webgpu.wasm';
48
+
49
+ /**
50
+ * Original target repo (add-basics fine-tune). **Not compatible with WebLLM 0.2.x** (no `tensor-cache.json`).
51
+ * @see module doc above
52
+ */
53
+ export const HUGGINGFACE_TB_SMOLLM_ADD_BASICS_REPO = 'https://huggingface.co/HuggingFaceTB/smollm-135M-instruct-add-basics-q0f16-MLC';
54
+
55
+ // ─── Factory ─────────────────────────────────────────────────────────────────
56
+
57
+ /**
58
+ * Create a local slow-lane client powered by MLC WebLLM.
59
+ *
60
+ * The engine is initialised lazily — model weights are downloaded (and cached
61
+ * in IndexedDB) on first use. Subsequent page loads skip the download.
62
+ *
63
+ * Usage:
64
+ * ```ts
65
+ * const client = createLocalSlowLaneClient({ debounceMs: 300 });
66
+ * // On word boundaries:
67
+ * client.updateContext(docText);
68
+ * // In scoring pipeline:
69
+ * const vec = client.getContextVector();
70
+ * const logits = client.getLmLogits();
71
+ * // On plugin teardown:
72
+ * client.destroy();
73
+ * ```
74
+ */
75
+ export const createLocalSlowLaneClient = (config = {}) => {
76
+ const {
77
+ debounceMs = DEFAULT_DEBOUNCE_MS,
78
+ onUpdate,
79
+ onStatus,
80
+ modelId = LOCAL_MLC_MODEL_ID,
81
+ customModelConfig
82
+ } = config;
83
+
84
+ // ── State ──────────────────────────────────────────────────────────────
85
+ let storedContextVector = null;
86
+ let storedLmLogits = null;
87
+ let debounceTimer = null;
88
+ let lastRequestedText = '';
89
+ let requestCounter = 0;
90
+ let latestRequestId = -1;
91
+ let ready = false;
92
+ let destroyed = false;
93
+ let initFailed = false;
94
+ let engine = null;
95
+ let engineInitPromise = null;
96
+ const unloadEngine = engineToUnload => {
97
+ engineToUnload.unload().catch(error => {
98
+ if (isAutocompleteDebugEnabled()) {
99
+ // eslint-disable-next-line no-console
100
+ console.log('%c[LocalSlowLane] %cFailed to unload engine', 'color: #9c27b0; font-weight: bold;', 'color: inherit;', error);
101
+ }
102
+ });
103
+ };
104
+
105
+ // ── Engine initialisation ──────────────────────────────────────────────
106
+
107
+ const initProgressCallback = progress => {
108
+ const message = `[${(progress.progress * 100).toFixed(0)}%] ${progress.text}`;
109
+ if (isAutocompleteDebugEnabled()) {
110
+ // eslint-disable-next-line no-console
111
+ console.log(`%c[LocalSlowLane] %c🔄 ${message}`, 'color: #9c27b0; font-weight: bold;', 'color: inherit;');
112
+ }
113
+ onStatus === null || onStatus === void 0 ? void 0 : onStatus(message);
114
+ };
115
+ const initEngine = async () => {
116
+ try {
117
+ if (isAutocompleteDebugEnabled()) {
118
+ // eslint-disable-next-line no-console
119
+ console.log(`%c[LocalSlowLane] %c🚀 Initialising MLC engine with model: ${modelId}`, 'color: #9c27b0; font-weight: bold;', 'color: inherit;');
120
+ }
121
+ onStatus === null || onStatus === void 0 ? void 0 : onStatus(`Initialising model: ${modelId}…`);
122
+ if (!('gpu' in navigator)) {
123
+ throw new Error('WebGPU not supported');
124
+ }
125
+
126
+ // eslint-disable-next-line @repo/internal/import/no-unresolved, import/dynamic-import-chunkname -- runtime dependency declared in package.json and lazily loaded for webgpu support
127
+ const {
128
+ CreateMLCEngine,
129
+ prebuiltAppConfig
130
+ } = await import( /* webpackChunkName: "@atlaskit-internal_editor-plugin-autocomplete-mlc-web-llm" */'@mlc-ai/web-llm');
131
+ const customModelRecord = customModelConfig ? {
132
+ model: customModelConfig.model,
133
+ model_id: modelId,
134
+ model_lib: customModelConfig.modelLib,
135
+ low_resource_required: true,
136
+ required_features: ['shader-f16'],
137
+ ...(customModelConfig.vramRequiredMB !== undefined ? {
138
+ vram_required_MB: customModelConfig.vramRequiredMB
139
+ } : {}),
140
+ ...(customModelConfig.contextWindowSize !== undefined ? {
141
+ overrides: {
142
+ context_window_size: customModelConfig.contextWindowSize
143
+ }
144
+ } : {})
145
+ } : undefined;
146
+ const appConfig = {
147
+ model_list: [...prebuiltAppConfig.model_list, ...(customModelRecord ? [customModelRecord] : [])]
148
+ };
149
+ engine = await CreateMLCEngine(modelId, {
150
+ appConfig,
151
+ initProgressCallback
152
+ });
153
+ if (destroyed) {
154
+ // destroy() was called while we were loading — clean up
155
+ unloadEngine(engine);
156
+ engine = null;
157
+ return;
158
+ }
159
+ ready = true;
160
+ if (isAutocompleteDebugEnabled()) {
161
+ // eslint-disable-next-line no-console
162
+ console.log('%c[LocalSlowLane] %c✅ MLC engine loaded and ready', 'color: #9c27b0; font-weight: bold;', 'color: #4caf50;');
163
+ }
164
+ onStatus === null || onStatus === void 0 ? void 0 : onStatus('Model loaded and ready.');
165
+ } catch (err) {
166
+ const errorMsg = err instanceof Error ? err.message : String(err);
167
+ ready = false;
168
+ if (isAutocompleteDebugEnabled()) {
169
+ // eslint-disable-next-line no-console
170
+ console.log(`[LocalSlowLane] Engine initialisation failed: ${errorMsg}`);
171
+ }
172
+ onStatus === null || onStatus === void 0 ? void 0 : onStatus(`Engine initialisation failed: ${errorMsg}`);
173
+ engineInitPromise = null;
174
+ initFailed = true;
175
+ }
176
+ };
177
+ const ensureEngineInitialized = () => {
178
+ if (initFailed) {
179
+ return Promise.resolve();
180
+ }
181
+ if (!engineInitPromise) {
182
+ engineInitPromise = initEngine();
183
+ }
184
+ return engineInitPromise;
185
+ };
186
+
187
+ // ── Inference ──────────────────────────────────────────────────────────
188
+
189
+ /**
190
+ * Run a single forward pass to extract next-token logit probabilities.
191
+ *
192
+ * We use the chat completions API with `max_tokens: 1` and `logprobs: true`
193
+ * to get the model's next-token distribution without generating text.
194
+ * This is the cheapest possible inference call — a single forward pass.
195
+ */
196
+ const runInference = async (text, requestId) => {
197
+ if (!engine || destroyed) {
198
+ return;
199
+ }
200
+ try {
201
+ var _response$choices, _response$choices$, _response$choices$$lo;
202
+ // Use chat completion with logprobs to get next-token distribution
203
+ const response = await engine.chat.completions.create({
204
+ messages: [{
205
+ role: 'user',
206
+ content: text
207
+ }],
208
+ max_tokens: 1,
209
+ logprobs: true,
210
+ top_logprobs: 5,
211
+ temperature: 0
212
+ });
213
+
214
+ // Discard stale results
215
+ if (requestId < latestRequestId || destroyed) {
216
+ return;
217
+ }
218
+
219
+ // ── Extract LM logits ───────────────────────────────────────
220
+ const lmLogits = {};
221
+ const logprobsContent = (_response$choices = response.choices) === null || _response$choices === void 0 ? void 0 : (_response$choices$ = _response$choices[0]) === null || _response$choices$ === void 0 ? void 0 : (_response$choices$$lo = _response$choices$.logprobs) === null || _response$choices$$lo === void 0 ? void 0 : _response$choices$$lo.content;
222
+ if (logprobsContent && logprobsContent.length > 0) {
223
+ const tokenLogprobs = logprobsContent[0];
224
+
225
+ // Add the top token
226
+ if (tokenLogprobs.token) {
227
+ const token = tokenLogprobs.token.trim().toLowerCase();
228
+ if (token.length > 0 && startsWithAsciiLetter(token)) {
229
+ lmLogits[token] = Math.exp(tokenLogprobs.logprob);
230
+ }
231
+ }
232
+
233
+ // Add alternative tokens from top_logprobs
234
+ if (tokenLogprobs.top_logprobs) {
235
+ for (const alt of tokenLogprobs.top_logprobs) {
236
+ const token = alt.token.trim().toLowerCase();
237
+ if (token.length > 0 && startsWithAsciiLetter(token)) {
238
+ lmLogits[token] = Math.exp(alt.logprob);
239
+ }
240
+ }
241
+ }
242
+ }
243
+ storedLmLogits = Object.keys(lmLogits).length > 0 ? lmLogits : null;
244
+
245
+ // ── Semantic vector ─────────────────────────────────────────
246
+ // SmolLM is a generative model, not an embedding model, so we
247
+ // don't get a true semantic vector. We generate a lightweight
248
+ // pseudo-embedding from the logit distribution for compatibility
249
+ // with the existing scoring pipeline.
250
+ //
251
+ // For a production implementation, you would use a dedicated
252
+ // embedding model (e.g. via web-llm's embeddings API with an
253
+ // embedding-specific model).
254
+ if (storedLmLogits) {
255
+ const logitValues = Object.values(storedLmLogits);
256
+ storedContextVector = new Float32Array(logitValues);
257
+ } else {
258
+ storedContextVector = null;
259
+ }
260
+ if (isAutocompleteDebugEnabled()) {
261
+ // eslint-disable-next-line no-console
262
+ console.groupCollapsed(`%c[LocalSlowLane] %c📥 Inference result (request #${requestId})`, 'color: #9c27b0; font-weight: bold;', 'color: inherit;');
263
+ // eslint-disable-next-line no-console
264
+ console.log(storedContextVector ? `✅ pseudo-vector: ${storedContextVector.length} dims` : '❌ No vector');
265
+ // eslint-disable-next-line no-console
266
+ console.log(storedLmLogits ? `✅ lm_logits: ${Object.keys(storedLmLogits).length} tokens` : '❌ No lm_logits');
267
+ if (storedLmLogits) {
268
+ const topTokens = Object.entries(storedLmLogits).sort(([, a], [, b]) => b - a).slice(0, 10);
269
+ // eslint-disable-next-line no-console
270
+ console.log('Top 10 predictions:', topTokens.map(([t, p]) => `${t}: ${(p * 100).toFixed(1)}%`).join(', '));
271
+ }
272
+ // eslint-disable-next-line no-console
273
+ console.groupEnd();
274
+ }
275
+ onUpdate === null || onUpdate === void 0 ? void 0 : onUpdate({
276
+ textLength: text.length,
277
+ hasVector: storedContextVector !== null,
278
+ hasLmLogits: storedLmLogits !== null
279
+ });
280
+ } catch (err) {
281
+ // Discard errors for stale requests
282
+ if (requestId < latestRequestId) {
283
+ return;
284
+ }
285
+ storedContextVector = null;
286
+ storedLmLogits = null;
287
+ onUpdate === null || onUpdate === void 0 ? void 0 : onUpdate({
288
+ textLength: text.length,
289
+ hasVector: false,
290
+ hasLmLogits: false
291
+ });
292
+ const errorMsg = err instanceof Error ? err.message : String(err);
293
+ if (isAutocompleteDebugEnabled()) {
294
+ // eslint-disable-next-line no-console
295
+ console.log(`%c[LocalSlowLane] %c❌ Inference error (request #${requestId}): ${errorMsg}`, 'color: #9c27b0; font-weight: bold;', 'color: #f44336;');
296
+ }
297
+ }
298
+ };
299
+
300
+ // ── Context update (debounced) ─────────────────────────────────────────
301
+
302
+ const doUpdateContext = text => {
303
+ if (destroyed || !text || text.trim().length === 0) {
304
+ return;
305
+ }
306
+ const requestId = ++requestCounter;
307
+ latestRequestId = requestId;
308
+ if (isAutocompleteDebugEnabled()) {
309
+ // eslint-disable-next-line no-console
310
+ console.groupCollapsed(`%c[LocalSlowLane] %c📤 Context update (request #${requestId}) | ${text.length} chars`, 'color: #9c27b0; font-weight: bold;', 'color: inherit;');
311
+ const lines = text.split('\n');
312
+ lines.forEach((line, i) => {
313
+ // eslint-disable-next-line no-console
314
+ console.log(` ${i === lines.length - 1 ? '▶' : ' '} ${line}`);
315
+ });
316
+ // eslint-disable-next-line no-console
317
+ console.groupEnd();
318
+ }
319
+ void ensureEngineInitialized().then(() => runInference(text, requestId)).catch(() => {});
320
+ };
321
+ const updateContextDebounced = text => {
322
+ if (debounceTimer) {
323
+ clearTimeout(debounceTimer);
324
+ }
325
+ lastRequestedText = text;
326
+ debounceTimer = setTimeout(() => {
327
+ debounceTimer = null;
328
+ doUpdateContext(lastRequestedText);
329
+ }, debounceMs);
330
+ };
331
+
332
+ // ── Public API (same shape as createSlowLaneClient) ────────────────────
333
+ return {
334
+ updateContext: updateContextDebounced,
335
+ getContextVector: () => storedContextVector,
336
+ getLmLogits: () => storedLmLogits,
337
+ setContextVector: vector => {
338
+ storedContextVector = vector;
339
+ },
340
+ setLmLogits: logits => {
341
+ storedLmLogits = logits;
342
+ },
343
+ isWordBoundary,
344
+ isReady: () => ready,
345
+ destroy: () => {
346
+ destroyed = true;
347
+ ready = false;
348
+ if (debounceTimer) {
349
+ clearTimeout(debounceTimer);
350
+ }
351
+ if (engine) {
352
+ const engineToUnload = engine;
353
+ engine = null;
354
+ unloadEngine(engineToUnload);
355
+ }
356
+ engineInitPromise = null;
357
+ storedContextVector = null;
358
+ storedLmLogits = null;
359
+ }
360
+ };
361
+ };
@@ -7,6 +7,7 @@
7
7
  * Response: { semantic_vector: number[], lm_logits: Record<string, number> }
8
8
  */
9
9
 
10
+ import { abortExp, EXPERIENCE_NAME, failExp, startExp, succeedExp } from '../analytics/ufo';
10
11
  import { isAutocompleteDebugEnabled } from './debug-mode';
11
12
 
12
13
  // ─── Types ───────────────────────────────────────────────────────────────────
@@ -54,7 +55,9 @@ export const createSlowLaneClient = config => {
54
55
  let lastRequestedText = '';
55
56
  let storedContextVector = null;
56
57
  let storedLmLogits = null;
57
- const doUpdateContext = async text => {
58
+ let requestSeq = 0;
59
+ let inflightRequestId = null;
60
+ const doUpdateContext = async (text, requestId) => {
58
61
  if (!text || text.trim().length === 0) {
59
62
  return;
60
63
  }
@@ -65,6 +68,9 @@ export const createSlowLaneClient = config => {
65
68
  text,
66
69
  session_id: sessionId
67
70
  };
71
+ startExp(EXPERIENCE_NAME.SLOW_LANE_FETCH, requestId, {
72
+ textLength: text.length
73
+ });
68
74
  if (isAutocompleteDebugEnabled()) {
69
75
  // eslint-disable-next-line no-console
70
76
  console.groupCollapsed(`%c[SlowLane] %c📤 Sending context | ${text.length} chars`, 'color: #9c27b0; font-weight: bold;', 'color: inherit;');
@@ -84,6 +90,10 @@ export const createSlowLaneClient = config => {
84
90
  if (!res.ok) {
85
91
  storedContextVector = null;
86
92
  storedLmLogits = null;
93
+ failExp(EXPERIENCE_NAME.SLOW_LANE_FETCH, requestId, {
94
+ status: res.status,
95
+ errorType: 'http_error'
96
+ });
87
97
  if (isAutocompleteDebugEnabled()) {
88
98
  // eslint-disable-next-line no-console
89
99
  console.log(`%c[SlowLane] %c❌ Request failed (${res.status})`, 'color: #9c27b0; font-weight: bold;', 'color: #f44336;');
@@ -111,6 +121,11 @@ export const createSlowLaneClient = config => {
111
121
  // eslint-disable-next-line no-console
112
122
  console.groupEnd();
113
123
  }
124
+ succeedExp(EXPERIENCE_NAME.SLOW_LANE_FETCH, requestId, {
125
+ textLength: text.length,
126
+ hasVector: storedContextVector !== null,
127
+ hasLmLogits: storedLmLogits !== null
128
+ });
114
129
  onUpdate === null || onUpdate === void 0 ? void 0 : onUpdate({
115
130
  textLength: text.length,
116
131
  hasVector: storedContextVector !== null,
@@ -120,10 +135,17 @@ export const createSlowLaneClient = config => {
120
135
  } catch (e) {
121
136
  storedContextVector = null;
122
137
  storedLmLogits = null;
138
+ failExp(EXPERIENCE_NAME.SLOW_LANE_FETCH, requestId, {
139
+ errorType: 'network'
140
+ });
123
141
  if (isAutocompleteDebugEnabled()) {
124
142
  // eslint-disable-next-line no-console
125
143
  console.log('%c[SlowLane] %c❌ Network error — context cleared', 'color: #9c27b0; font-weight: bold;', 'color: #f44336;');
126
144
  }
145
+ } finally {
146
+ if (inflightRequestId === requestId) {
147
+ inflightRequestId = null;
148
+ }
127
149
  }
128
150
  };
129
151
  const updateContextDebounced = text => {
@@ -133,7 +155,12 @@ export const createSlowLaneClient = config => {
133
155
  lastRequestedText = text;
134
156
  debounceTimer = setTimeout(() => {
135
157
  debounceTimer = null;
136
- doUpdateContext(lastRequestedText);
158
+ if (inflightRequestId !== null) {
159
+ abortExp(EXPERIENCE_NAME.SLOW_LANE_FETCH, inflightRequestId, 'superseded');
160
+ }
161
+ const requestId = String(++requestSeq);
162
+ inflightRequestId = requestId;
163
+ doUpdateContext(lastRequestedText, requestId);
137
164
  }, debounceMs);
138
165
  };
139
166
  return {
@@ -16,6 +16,8 @@ import _defineProperty from "@babel/runtime/helpers/defineProperty";
16
16
  * via incrementSessionFreq(), called on word boundaries from the plugin.
17
17
  */
18
18
 
19
+ import { EXPERIENCE_NAME, failExp, startExp, succeedExp } from '../analytics/ufo';
20
+
19
21
  // import bigramsData from './data/bigrams.json';
20
22
  import l3VocabularyData from './data/l3_vocabulary.json';
21
23
  import vocabularyData from './data/vocabulary_10k.json';
@@ -566,11 +568,15 @@ export const loadVectorsAsync = async options => {
566
568
  return;
567
569
  }
568
570
  vectorsLoadStarted = true;
571
+ startExp(EXPERIENCE_NAME.LOAD_VECTORS, 'singleton');
569
572
  let url;
570
573
  try {
571
574
  url = await options.getBinaryUrl();
572
575
  } catch (e) {
573
576
  vectorsLoadStarted = false;
577
+ failExp(EXPERIENCE_NAME.LOAD_VECTORS, 'singleton', {
578
+ errorType: 'resolve_url'
579
+ });
574
580
  // eslint-disable-next-line no-console
575
581
  console.warn('[text-predictor] Failed to resolve vectors URL:', e);
576
582
  return;
@@ -579,6 +585,10 @@ export const loadVectorsAsync = async options => {
579
585
  const res = await fetch(url);
580
586
  if (!res.ok) {
581
587
  vectorsLoadStarted = false;
588
+ failExp(EXPERIENCE_NAME.LOAD_VECTORS, 'singleton', {
589
+ status: res.status,
590
+ errorType: 'http_error'
591
+ });
582
592
  // eslint-disable-next-line no-console
583
593
  console.warn(`[text-predictor] Failed to load vectors: ${res.status}`);
584
594
  return;
@@ -593,6 +603,11 @@ export const loadVectorsAsync = async options => {
593
603
  wordIndex,
594
604
  dim
595
605
  };
606
+ succeedExp(EXPERIENCE_NAME.LOAD_VECTORS, 'singleton', {
607
+ wordCount: nWords,
608
+ dim,
609
+ sizeBytes: float32.byteLength
610
+ });
596
611
  if (isAutocompleteDebugEnabled()) {
597
612
  // eslint-disable-next-line no-console
598
613
  console.log('[text-predictor] Vectors loaded:', {
@@ -603,6 +618,9 @@ export const loadVectorsAsync = async options => {
603
618
  }
604
619
  } catch (e) {
605
620
  vectorsLoadStarted = false;
621
+ failExp(EXPERIENCE_NAME.LOAD_VECTORS, 'singleton', {
622
+ errorType: 'network'
623
+ });
606
624
  // eslint-disable-next-line no-console
607
625
  console.warn('[text-predictor] Failed to load vectors:', e);
608
626
  }
@@ -611,19 +629,34 @@ export const initVectors = store => {
611
629
  vectorStore = store;
612
630
  };
613
631
  export const loadDefaultVocabulary = () => {
614
- // 1. Load the Atlassian Domain (L2)
615
- const data = vocabularyData;
616
- const terms = Object.entries(data.words).map(([word, stats]) => ({
617
- word,
618
- freq: stats.freq,
619
- docFreq: stats.doc_freq,
620
- authorFreq: stats.author_freq
621
- }));
622
- initVocabulary({
623
- terms
624
- });
625
-
626
- // 2. Load General English (L3)
627
- const l3Words = l3VocabularyData;
628
- initL3Vocabulary(l3Words);
632
+ if (isInitialized) {
633
+ return;
634
+ }
635
+ startExp(EXPERIENCE_NAME.LOAD_VOCABULARY, 'singleton');
636
+ try {
637
+ // 1. Load the Atlassian Domain (L2)
638
+ const data = vocabularyData;
639
+ const terms = Object.entries(data.words).map(([word, stats]) => ({
640
+ word,
641
+ freq: stats.freq,
642
+ docFreq: stats.doc_freq,
643
+ authorFreq: stats.author_freq
644
+ }));
645
+ initVocabulary({
646
+ terms
647
+ });
648
+
649
+ // 2. Load General English (L3)
650
+ const l3Words = l3VocabularyData;
651
+ initL3Vocabulary(l3Words);
652
+ succeedExp(EXPERIENCE_NAME.LOAD_VOCABULARY, 'singleton', {
653
+ l2WordCount: terms.length,
654
+ l3WordCount: l3Words.length
655
+ });
656
+ } catch (e) {
657
+ failExp(EXPERIENCE_NAME.LOAD_VOCABULARY, 'singleton', {
658
+ errorType: 'parse_error'
659
+ });
660
+ throw e;
661
+ }
629
662
  };
@@ -0,0 +1,105 @@
1
+ import _defineProperty from "@babel/runtime/helpers/defineProperty";
2
+ /**
3
+ * UFO experience tracking helpers for `@atlaskit/editor-plugin-autocomplete`.
4
+ *
5
+ * Pattern follows `packages/linking-platform/smart-card/src/state/analytics/ufoExperiences.ts`
6
+ * (ConcurrentExperience keyed per request/instance) plus a try/catch safety wrap
7
+ * inspired by `packages/editor/collab-provider/src/analytics/ufo.ts` so a UFO
8
+ * runtime error can never break the autocomplete feature.
9
+ *
10
+ * Experience names must be registered in DataPortal's `task` attribute before
11
+ * they can power FE Reliability SLOs:
12
+ * https://hello.atlassian.net/wiki/spaces/AA6/pages/3961393753
13
+ */
14
+
15
+ import { ConcurrentExperience, ExperiencePerformanceTypes, ExperienceTypes } from '@atlaskit/ufo';
16
+
17
+ /**
18
+ * Experience name strings are surfaced downstream by the UFO pipeline as
19
+ * `platform.fe.<type>.<platform.component>.<name>` (e.g.
20
+ * `platform.fe.operation.editor-plugin-autocomplete.slow-lane-fetch`), so
21
+ * names here should be short, dot-free, and must not repeat the component
22
+ * name. This matches the convention used by `@atlaskit/emoji`'s
23
+ * `ufoExperiences` map (e.g. `'emoji-rendered'`).
24
+ */
25
+ /**
26
+ * Only experiences with meaningful async work and a real success/failure
27
+ * outcome belong on UFO (which measures latency and success rate for FE
28
+ * Reliability SLOs). Per-keystroke suggestion lifecycle counters (viewed /
29
+ * inserted / dismissed) are tracked via analytics-next instead — they would
30
+ * otherwise emit zero-duration events on every word boundary.
31
+ */
32
+ export var EXPERIENCE_NAME = {
33
+ SLOW_LANE_FETCH: 'slow-lane-fetch',
34
+ LOAD_VOCABULARY: 'load-vocabulary',
35
+ LOAD_VECTORS: 'load-vectors'
36
+ };
37
+ var isUfoEnabled = function isUfoEnabled() {
38
+ var _process;
39
+ return !((_process = process) !== null && _process !== void 0 && (_process = _process.env) !== null && _process !== void 0 && _process.REACT_SSR);
40
+ };
41
+ var operationConfig = {
42
+ platform: {
43
+ component: 'editor-plugin-autocomplete'
44
+ },
45
+ type: ExperienceTypes.Operation,
46
+ performanceType: ExperiencePerformanceTypes.Custom,
47
+ performanceConfig: {
48
+ histogram: _defineProperty({}, ExperiencePerformanceTypes.Custom, {
49
+ duration: '50_100_250_500_1000_2000_4000'
50
+ })
51
+ }
52
+ };
53
+ var experiences = _defineProperty(_defineProperty(_defineProperty({}, EXPERIENCE_NAME.SLOW_LANE_FETCH, new ConcurrentExperience(EXPERIENCE_NAME.SLOW_LANE_FETCH, operationConfig)), EXPERIENCE_NAME.LOAD_VOCABULARY, new ConcurrentExperience(EXPERIENCE_NAME.LOAD_VOCABULARY, operationConfig)), EXPERIENCE_NAME.LOAD_VECTORS, new ConcurrentExperience(EXPERIENCE_NAME.LOAD_VECTORS, operationConfig));
54
+ export var startExp = function startExp(name, id, metadata) {
55
+ if (!isUfoEnabled()) {
56
+ return;
57
+ }
58
+ try {
59
+ var instance = experiences[name].getInstance(id);
60
+ instance.start();
61
+ if (metadata) {
62
+ instance.addMetadata(metadata);
63
+ }
64
+ } catch (_unused) {
65
+ // UFO errors must never break the plugin
66
+ }
67
+ };
68
+ export var succeedExp = function succeedExp(name, id, metadata) {
69
+ if (!isUfoEnabled()) {
70
+ return;
71
+ }
72
+ try {
73
+ experiences[name].getInstance(id).success({
74
+ metadata: metadata
75
+ });
76
+ } catch (_unused2) {
77
+ // UFO errors must never break the plugin
78
+ }
79
+ };
80
+ export var failExp = function failExp(name, id, metadata) {
81
+ if (!isUfoEnabled()) {
82
+ return;
83
+ }
84
+ try {
85
+ experiences[name].getInstance(id).failure({
86
+ metadata: metadata
87
+ });
88
+ } catch (_unused3) {
89
+ // UFO errors must never break the plugin
90
+ }
91
+ };
92
+ export var abortExp = function abortExp(name, id, reason) {
93
+ if (!isUfoEnabled()) {
94
+ return;
95
+ }
96
+ try {
97
+ experiences[name].getInstance(id).abort(reason ? {
98
+ metadata: {
99
+ reason: reason
100
+ }
101
+ } : undefined);
102
+ } catch (_unused4) {
103
+ // UFO errors must never break the plugin
104
+ }
105
+ };