@atlaskit/editor-plugin-autocomplete 2.3.0 → 2.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/CHANGELOG.md +42 -0
- package/afm-cc/tsconfig.json +3 -0
- package/afm-products/tsconfig.json +3 -0
- package/autocompletePlugin/package.json +15 -0
- package/autocompletePluginType/package.json +15 -0
- package/dist/cjs/analytics/ufo.js +111 -0
- package/dist/cjs/entry-points/autocompletePlugin.js +12 -0
- package/dist/cjs/entry-points/autocompletePluginType.js +1 -0
- package/dist/cjs/entry-points/src-pm-plugins-autocomplete-plugin.js +18 -0
- package/dist/cjs/entry-points/src-pm-plugins-slow-lane-client.js +36 -0
- package/dist/cjs/entry-points/src-pm-plugins-text-predictor.js +66 -0
- package/dist/cjs/pm-plugins/autocomplete-plugin.js +126 -64
- package/dist/cjs/pm-plugins/local-slow-lane-client.js +447 -0
- package/dist/cjs/pm-plugins/slow-lane-client.js +45 -16
- package/dist/cjs/pm-plugins/text-predictor.js +72 -40
- package/dist/es2019/analytics/ufo.js +110 -0
- package/dist/es2019/entry-points/autocompletePlugin.js +2 -0
- package/dist/es2019/entry-points/autocompletePluginType.js +0 -0
- package/dist/es2019/entry-points/src-pm-plugins-autocomplete-plugin.js +2 -0
- package/dist/es2019/entry-points/src-pm-plugins-slow-lane-client.js +2 -0
- package/dist/es2019/entry-points/src-pm-plugins-text-predictor.js +2 -0
- package/dist/es2019/pm-plugins/autocomplete-plugin.js +132 -72
- package/dist/es2019/pm-plugins/local-slow-lane-client.js +355 -0
- package/dist/es2019/pm-plugins/slow-lane-client.js +29 -2
- package/dist/es2019/pm-plugins/text-predictor.js +48 -15
- package/dist/esm/analytics/ufo.js +105 -0
- package/dist/esm/entry-points/autocompletePlugin.js +2 -0
- package/dist/esm/entry-points/autocompletePluginType.js +0 -0
- package/dist/esm/entry-points/src-pm-plugins-autocomplete-plugin.js +2 -0
- package/dist/esm/entry-points/src-pm-plugins-slow-lane-client.js +2 -0
- package/dist/esm/entry-points/src-pm-plugins-text-predictor.js +2 -0
- package/dist/esm/pm-plugins/autocomplete-plugin.js +126 -64
- package/dist/esm/pm-plugins/local-slow-lane-client.js +439 -0
- package/dist/esm/pm-plugins/slow-lane-client.js +45 -16
- package/dist/esm/pm-plugins/text-predictor.js +73 -40
- package/dist/types/analytics/ufo.d.ts +38 -0
- package/dist/types/entry-points/autocompletePlugin.d.ts +1 -0
- package/dist/types/entry-points/autocompletePluginType.d.ts +1 -0
- package/dist/types/entry-points/src-pm-plugins-autocomplete-plugin.d.ts +2 -0
- package/dist/types/entry-points/src-pm-plugins-slow-lane-client.d.ts +2 -0
- package/dist/types/entry-points/src-pm-plugins-text-predictor.d.ts +2 -0
- package/dist/types/pm-plugins/autocomplete-plugin.d.ts +5 -0
- package/dist/types/pm-plugins/local-slow-lane-client.d.ts +102 -0
- package/dist/types-ts4.5/analytics/ufo.d.ts +38 -0
- package/dist/types-ts4.5/entry-points/autocompletePlugin.d.ts +1 -0
- package/dist/types-ts4.5/entry-points/autocompletePluginType.d.ts +1 -0
- package/dist/types-ts4.5/entry-points/src-pm-plugins-autocomplete-plugin.d.ts +2 -0
- package/dist/types-ts4.5/entry-points/src-pm-plugins-slow-lane-client.d.ts +2 -0
- package/dist/types-ts4.5/entry-points/src-pm-plugins-text-predictor.d.ts +2 -0
- package/dist/types-ts4.5/pm-plugins/autocomplete-plugin.d.ts +5 -0
- package/dist/types-ts4.5/pm-plugins/local-slow-lane-client.d.ts +102 -0
- package/package.json +8 -3
- package/src/analytics/ufo.ts +139 -0
- package/src/entry-points/autocompletePlugin.tsx +2 -0
- package/src/entry-points/autocompletePluginType.ts +2 -0
- package/src/entry-points/src-pm-plugins-autocomplete-plugin.ts +7 -0
- package/src/entry-points/src-pm-plugins-slow-lane-client.ts +13 -0
- package/src/entry-points/src-pm-plugins-text-predictor.ts +14 -0
- package/src/pm-plugins/autocomplete-plugin/package.json +5 -5
- package/src/pm-plugins/autocomplete-plugin.ts +126 -64
- package/src/pm-plugins/local-slow-lane-client.ts +480 -0
- package/src/pm-plugins/slow-lane-client/package.json +5 -5
- package/src/pm-plugins/slow-lane-client.ts +28 -2
- package/src/pm-plugins/text-predictor/package.json +5 -5
- package/src/pm-plugins/text-predictor.ts +42 -12
- package/tsconfig.app.json +3 -0
|
@@ -0,0 +1,355 @@
|
|
|
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
|
+
|
|
32
|
+
// ─── Types ───────────────────────────────────────────────────────────────────
|
|
33
|
+
|
|
34
|
+
// Same return type as createSlowLaneClient for drop-in compatibility
|
|
35
|
+
|
|
36
|
+
// ─── Constants ───────────────────────────────────────────────────────────────
|
|
37
|
+
|
|
38
|
+
const DEFAULT_DEBOUNCE_MS = 300;
|
|
39
|
+
export const LOCAL_MLC_MODEL_ID = 'SmolLM2-135M-Instruct-q0f16-MLC';
|
|
40
|
+
|
|
41
|
+
/** HF root for the default weights (includes `tensor-cache.json` for WebLLM 0.2+). */
|
|
42
|
+
export const LOCAL_MLC_HF_MODEL_REPO = 'https://huggingface.co/mlc-ai/SmolLM2-135M-Instruct-q0f16-MLC';
|
|
43
|
+
export const LOCAL_MLC_MODEL_LIB_WASM_NAME = 'SmolLM2-135M-Instruct-q0f16-ctx4k_cs1k-webgpu.wasm';
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Original target repo (add-basics fine-tune). **Not compatible with WebLLM 0.2.x** (no `tensor-cache.json`).
|
|
47
|
+
* @see module doc above
|
|
48
|
+
*/
|
|
49
|
+
export const HUGGINGFACE_TB_SMOLLM_ADD_BASICS_REPO = 'https://huggingface.co/HuggingFaceTB/smollm-135M-instruct-add-basics-q0f16-MLC';
|
|
50
|
+
|
|
51
|
+
// ─── Factory ─────────────────────────────────────────────────────────────────
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Create a local slow-lane client powered by MLC WebLLM.
|
|
55
|
+
*
|
|
56
|
+
* The engine is initialised lazily — model weights are downloaded (and cached
|
|
57
|
+
* in IndexedDB) on first use. Subsequent page loads skip the download.
|
|
58
|
+
*
|
|
59
|
+
* Usage:
|
|
60
|
+
* ```ts
|
|
61
|
+
* const client = createLocalSlowLaneClient({ debounceMs: 300 });
|
|
62
|
+
* // On word boundaries:
|
|
63
|
+
* client.updateContext(docText);
|
|
64
|
+
* // In scoring pipeline:
|
|
65
|
+
* const vec = client.getContextVector();
|
|
66
|
+
* const logits = client.getLmLogits();
|
|
67
|
+
* // On plugin teardown:
|
|
68
|
+
* client.destroy();
|
|
69
|
+
* ```
|
|
70
|
+
*/
|
|
71
|
+
export const createLocalSlowLaneClient = (config = {}) => {
|
|
72
|
+
const {
|
|
73
|
+
debounceMs = DEFAULT_DEBOUNCE_MS,
|
|
74
|
+
onUpdate,
|
|
75
|
+
onStatus,
|
|
76
|
+
modelId = LOCAL_MLC_MODEL_ID,
|
|
77
|
+
customModelConfig
|
|
78
|
+
} = config;
|
|
79
|
+
|
|
80
|
+
// ── State ──────────────────────────────────────────────────────────────
|
|
81
|
+
let storedContextVector = null;
|
|
82
|
+
let storedLmLogits = null;
|
|
83
|
+
let debounceTimer = null;
|
|
84
|
+
let lastRequestedText = '';
|
|
85
|
+
let requestCounter = 0;
|
|
86
|
+
let latestRequestId = -1;
|
|
87
|
+
let ready = false;
|
|
88
|
+
let destroyed = false;
|
|
89
|
+
let initFailed = false;
|
|
90
|
+
let engine = null;
|
|
91
|
+
let engineInitPromise = null;
|
|
92
|
+
const unloadEngine = engineToUnload => {
|
|
93
|
+
engineToUnload.unload().catch(error => {
|
|
94
|
+
if (isAutocompleteDebugEnabled()) {
|
|
95
|
+
// eslint-disable-next-line no-console
|
|
96
|
+
console.log('%c[LocalSlowLane] %cFailed to unload engine', 'color: #9c27b0; font-weight: bold;', 'color: inherit;', error);
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
// ── Engine initialisation ──────────────────────────────────────────────
|
|
102
|
+
|
|
103
|
+
const initProgressCallback = progress => {
|
|
104
|
+
const message = `[${(progress.progress * 100).toFixed(0)}%] ${progress.text}`;
|
|
105
|
+
if (isAutocompleteDebugEnabled()) {
|
|
106
|
+
// eslint-disable-next-line no-console
|
|
107
|
+
console.log(`%c[LocalSlowLane] %c🔄 ${message}`, 'color: #9c27b0; font-weight: bold;', 'color: inherit;');
|
|
108
|
+
}
|
|
109
|
+
onStatus === null || onStatus === void 0 ? void 0 : onStatus(message);
|
|
110
|
+
};
|
|
111
|
+
const initEngine = async () => {
|
|
112
|
+
try {
|
|
113
|
+
if (isAutocompleteDebugEnabled()) {
|
|
114
|
+
// eslint-disable-next-line no-console
|
|
115
|
+
console.log(`%c[LocalSlowLane] %c🚀 Initialising MLC engine with model: ${modelId}`, 'color: #9c27b0; font-weight: bold;', 'color: inherit;');
|
|
116
|
+
}
|
|
117
|
+
onStatus === null || onStatus === void 0 ? void 0 : onStatus(`Initialising model: ${modelId}…`);
|
|
118
|
+
if (!('gpu' in navigator)) {
|
|
119
|
+
throw new Error('WebGPU not supported');
|
|
120
|
+
}
|
|
121
|
+
const {
|
|
122
|
+
CreateMLCEngine,
|
|
123
|
+
prebuiltAppConfig
|
|
124
|
+
} = await import( /* webpackChunkName: "@atlaskit-internal_editor-plugin-autocomplete-mlc-web-llm" */'@mlc-ai/web-llm');
|
|
125
|
+
const customModelRecord = customModelConfig ? {
|
|
126
|
+
model: customModelConfig.model,
|
|
127
|
+
model_id: modelId,
|
|
128
|
+
model_lib: customModelConfig.modelLib,
|
|
129
|
+
low_resource_required: true,
|
|
130
|
+
required_features: ['shader-f16'],
|
|
131
|
+
...(customModelConfig.vramRequiredMB !== undefined ? {
|
|
132
|
+
vram_required_MB: customModelConfig.vramRequiredMB
|
|
133
|
+
} : {}),
|
|
134
|
+
...(customModelConfig.contextWindowSize !== undefined ? {
|
|
135
|
+
overrides: {
|
|
136
|
+
context_window_size: customModelConfig.contextWindowSize
|
|
137
|
+
}
|
|
138
|
+
} : {})
|
|
139
|
+
} : undefined;
|
|
140
|
+
const appConfig = {
|
|
141
|
+
model_list: [...prebuiltAppConfig.model_list, ...(customModelRecord ? [customModelRecord] : [])]
|
|
142
|
+
};
|
|
143
|
+
engine = await CreateMLCEngine(modelId, {
|
|
144
|
+
appConfig,
|
|
145
|
+
initProgressCallback
|
|
146
|
+
});
|
|
147
|
+
if (destroyed) {
|
|
148
|
+
// destroy() was called while we were loading — clean up
|
|
149
|
+
unloadEngine(engine);
|
|
150
|
+
engine = null;
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
ready = true;
|
|
154
|
+
if (isAutocompleteDebugEnabled()) {
|
|
155
|
+
// eslint-disable-next-line no-console
|
|
156
|
+
console.log('%c[LocalSlowLane] %c✅ MLC engine loaded and ready', 'color: #9c27b0; font-weight: bold;', 'color: #4caf50;');
|
|
157
|
+
}
|
|
158
|
+
onStatus === null || onStatus === void 0 ? void 0 : onStatus('Model loaded and ready.');
|
|
159
|
+
} catch (err) {
|
|
160
|
+
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
161
|
+
ready = false;
|
|
162
|
+
if (isAutocompleteDebugEnabled()) {
|
|
163
|
+
// eslint-disable-next-line no-console
|
|
164
|
+
console.log(`[LocalSlowLane] Engine initialisation failed: ${errorMsg}`);
|
|
165
|
+
}
|
|
166
|
+
onStatus === null || onStatus === void 0 ? void 0 : onStatus(`Engine initialisation failed: ${errorMsg}`);
|
|
167
|
+
engineInitPromise = null;
|
|
168
|
+
initFailed = true;
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
const ensureEngineInitialized = () => {
|
|
172
|
+
if (initFailed) {
|
|
173
|
+
return Promise.resolve();
|
|
174
|
+
}
|
|
175
|
+
if (!engineInitPromise) {
|
|
176
|
+
engineInitPromise = initEngine();
|
|
177
|
+
}
|
|
178
|
+
return engineInitPromise;
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
// ── Inference ──────────────────────────────────────────────────────────
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Run a single forward pass to extract next-token logit probabilities.
|
|
185
|
+
*
|
|
186
|
+
* We use the chat completions API with `max_tokens: 1` and `logprobs: true`
|
|
187
|
+
* to get the model's next-token distribution without generating text.
|
|
188
|
+
* This is the cheapest possible inference call — a single forward pass.
|
|
189
|
+
*/
|
|
190
|
+
const runInference = async (text, requestId) => {
|
|
191
|
+
if (!engine || destroyed) {
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
try {
|
|
195
|
+
var _response$choices, _response$choices$, _response$choices$$lo;
|
|
196
|
+
// Use chat completion with logprobs to get next-token distribution
|
|
197
|
+
const response = await engine.chat.completions.create({
|
|
198
|
+
messages: [{
|
|
199
|
+
role: 'user',
|
|
200
|
+
content: text
|
|
201
|
+
}],
|
|
202
|
+
max_tokens: 1,
|
|
203
|
+
logprobs: true,
|
|
204
|
+
top_logprobs: 5,
|
|
205
|
+
temperature: 0
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
// Discard stale results
|
|
209
|
+
if (requestId < latestRequestId || destroyed) {
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// ── Extract LM logits ───────────────────────────────────────
|
|
214
|
+
const lmLogits = {};
|
|
215
|
+
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;
|
|
216
|
+
if (logprobsContent && logprobsContent.length > 0) {
|
|
217
|
+
const tokenLogprobs = logprobsContent[0];
|
|
218
|
+
|
|
219
|
+
// Add the top token
|
|
220
|
+
if (tokenLogprobs.token) {
|
|
221
|
+
const token = tokenLogprobs.token.trim().toLowerCase();
|
|
222
|
+
if (token.length > 0 && /^[a-z]/iu.test(token)) {
|
|
223
|
+
lmLogits[token] = Math.exp(tokenLogprobs.logprob);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Add alternative tokens from top_logprobs
|
|
228
|
+
if (tokenLogprobs.top_logprobs) {
|
|
229
|
+
for (const alt of tokenLogprobs.top_logprobs) {
|
|
230
|
+
const token = alt.token.trim().toLowerCase();
|
|
231
|
+
if (token.length > 0 && /^[a-z]/iu.test(token)) {
|
|
232
|
+
lmLogits[token] = Math.exp(alt.logprob);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
storedLmLogits = Object.keys(lmLogits).length > 0 ? lmLogits : null;
|
|
238
|
+
|
|
239
|
+
// ── Semantic vector ─────────────────────────────────────────
|
|
240
|
+
// SmolLM is a generative model, not an embedding model, so we
|
|
241
|
+
// don't get a true semantic vector. We generate a lightweight
|
|
242
|
+
// pseudo-embedding from the logit distribution for compatibility
|
|
243
|
+
// with the existing scoring pipeline.
|
|
244
|
+
//
|
|
245
|
+
// For a production implementation, you would use a dedicated
|
|
246
|
+
// embedding model (e.g. via web-llm's embeddings API with an
|
|
247
|
+
// embedding-specific model).
|
|
248
|
+
if (storedLmLogits) {
|
|
249
|
+
const logitValues = Object.values(storedLmLogits);
|
|
250
|
+
storedContextVector = new Float32Array(logitValues);
|
|
251
|
+
} else {
|
|
252
|
+
storedContextVector = null;
|
|
253
|
+
}
|
|
254
|
+
if (isAutocompleteDebugEnabled()) {
|
|
255
|
+
// eslint-disable-next-line no-console
|
|
256
|
+
console.groupCollapsed(`%c[LocalSlowLane] %c📥 Inference result (request #${requestId})`, 'color: #9c27b0; font-weight: bold;', 'color: inherit;');
|
|
257
|
+
// eslint-disable-next-line no-console
|
|
258
|
+
console.log(storedContextVector ? `✅ pseudo-vector: ${storedContextVector.length} dims` : '❌ No vector');
|
|
259
|
+
// eslint-disable-next-line no-console
|
|
260
|
+
console.log(storedLmLogits ? `✅ lm_logits: ${Object.keys(storedLmLogits).length} tokens` : '❌ No lm_logits');
|
|
261
|
+
if (storedLmLogits) {
|
|
262
|
+
const topTokens = Object.entries(storedLmLogits).sort(([, a], [, b]) => b - a).slice(0, 10);
|
|
263
|
+
// eslint-disable-next-line no-console
|
|
264
|
+
console.log('Top 10 predictions:', topTokens.map(([t, p]) => `${t}: ${(p * 100).toFixed(1)}%`).join(', '));
|
|
265
|
+
}
|
|
266
|
+
// eslint-disable-next-line no-console
|
|
267
|
+
console.groupEnd();
|
|
268
|
+
}
|
|
269
|
+
onUpdate === null || onUpdate === void 0 ? void 0 : onUpdate({
|
|
270
|
+
textLength: text.length,
|
|
271
|
+
hasVector: storedContextVector !== null,
|
|
272
|
+
hasLmLogits: storedLmLogits !== null
|
|
273
|
+
});
|
|
274
|
+
} catch (err) {
|
|
275
|
+
// Discard errors for stale requests
|
|
276
|
+
if (requestId < latestRequestId) {
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
storedContextVector = null;
|
|
280
|
+
storedLmLogits = null;
|
|
281
|
+
onUpdate === null || onUpdate === void 0 ? void 0 : onUpdate({
|
|
282
|
+
textLength: text.length,
|
|
283
|
+
hasVector: false,
|
|
284
|
+
hasLmLogits: false
|
|
285
|
+
});
|
|
286
|
+
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
287
|
+
if (isAutocompleteDebugEnabled()) {
|
|
288
|
+
// eslint-disable-next-line no-console
|
|
289
|
+
console.log(`%c[LocalSlowLane] %c❌ Inference error (request #${requestId}): ${errorMsg}`, 'color: #9c27b0; font-weight: bold;', 'color: #f44336;');
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
};
|
|
293
|
+
|
|
294
|
+
// ── Context update (debounced) ─────────────────────────────────────────
|
|
295
|
+
|
|
296
|
+
const doUpdateContext = text => {
|
|
297
|
+
if (destroyed || !text || text.trim().length === 0) {
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
const requestId = ++requestCounter;
|
|
301
|
+
latestRequestId = requestId;
|
|
302
|
+
if (isAutocompleteDebugEnabled()) {
|
|
303
|
+
// eslint-disable-next-line no-console
|
|
304
|
+
console.groupCollapsed(`%c[LocalSlowLane] %c📤 Context update (request #${requestId}) | ${text.length} chars`, 'color: #9c27b0; font-weight: bold;', 'color: inherit;');
|
|
305
|
+
const lines = text.split('\n');
|
|
306
|
+
lines.forEach((line, i) => {
|
|
307
|
+
// eslint-disable-next-line no-console
|
|
308
|
+
console.log(` ${i === lines.length - 1 ? '▶' : ' '} ${line}`);
|
|
309
|
+
});
|
|
310
|
+
// eslint-disable-next-line no-console
|
|
311
|
+
console.groupEnd();
|
|
312
|
+
}
|
|
313
|
+
void ensureEngineInitialized().then(() => runInference(text, requestId)).catch(() => {});
|
|
314
|
+
};
|
|
315
|
+
const updateContextDebounced = text => {
|
|
316
|
+
if (debounceTimer) {
|
|
317
|
+
clearTimeout(debounceTimer);
|
|
318
|
+
}
|
|
319
|
+
lastRequestedText = text;
|
|
320
|
+
debounceTimer = setTimeout(() => {
|
|
321
|
+
debounceTimer = null;
|
|
322
|
+
doUpdateContext(lastRequestedText);
|
|
323
|
+
}, debounceMs);
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
// ── Public API (same shape as createSlowLaneClient) ────────────────────
|
|
327
|
+
return {
|
|
328
|
+
updateContext: updateContextDebounced,
|
|
329
|
+
getContextVector: () => storedContextVector,
|
|
330
|
+
getLmLogits: () => storedLmLogits,
|
|
331
|
+
setContextVector: vector => {
|
|
332
|
+
storedContextVector = vector;
|
|
333
|
+
},
|
|
334
|
+
setLmLogits: logits => {
|
|
335
|
+
storedLmLogits = logits;
|
|
336
|
+
},
|
|
337
|
+
isWordBoundary,
|
|
338
|
+
isReady: () => ready,
|
|
339
|
+
destroy: () => {
|
|
340
|
+
destroyed = true;
|
|
341
|
+
ready = false;
|
|
342
|
+
if (debounceTimer) {
|
|
343
|
+
clearTimeout(debounceTimer);
|
|
344
|
+
}
|
|
345
|
+
if (engine) {
|
|
346
|
+
const engineToUnload = engine;
|
|
347
|
+
engine = null;
|
|
348
|
+
unloadEngine(engineToUnload);
|
|
349
|
+
}
|
|
350
|
+
engineInitPromise = null;
|
|
351
|
+
storedContextVector = null;
|
|
352
|
+
storedLmLogits = null;
|
|
353
|
+
}
|
|
354
|
+
};
|
|
355
|
+
};
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
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
|
+
};
|
|
File without changes
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
/* eslint-disable @atlaskit/editor/no-re-export */
|
|
2
|
+
export { getLastPredictionDebug, getPredictorStatus, incrementSessionFreq, ingestDocumentPage, initL3Vocabulary, initVectors, initVocabulary, loadDefaultVocabulary, loadVectorsAsync, predict } from '../pm-plugins/text-predictor';
|