@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.
- package/CHANGELOG.md +39 -0
- package/afm-cc/tsconfig.json +3 -0
- package/afm-products/tsconfig.json +3 -0
- package/dist/cjs/analytics/ufo.js +111 -0
- package/dist/cjs/pm-plugins/autocomplete-plugin.js +126 -64
- package/dist/cjs/pm-plugins/local-slow-lane-client.js +452 -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/pm-plugins/autocomplete-plugin.js +132 -72
- package/dist/es2019/pm-plugins/local-slow-lane-client.js +361 -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/pm-plugins/autocomplete-plugin.js +126 -64
- package/dist/esm/pm-plugins/local-slow-lane-client.js +443 -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/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/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 +7 -2
- package/src/analytics/ufo.ts +132 -0
- package/src/pm-plugins/autocomplete-plugin.ts +125 -64
- package/src/pm-plugins/local-slow-lane-client.ts +487 -0
- package/src/pm-plugins/slow-lane-client.ts +28 -2
- package/src/pm-plugins/text-predictor.ts +42 -12
- package/tsconfig.app.json +3 -0
|
@@ -0,0 +1,487 @@
|
|
|
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 type { MLCEngine, InitProgressReport, AppConfig } from '@mlc-ai/web-llm';
|
|
30
|
+
|
|
31
|
+
import { isAutocompleteDebugEnabled } from './debug-mode';
|
|
32
|
+
import { isWordBoundary } from './slow-lane-client';
|
|
33
|
+
|
|
34
|
+
type WebLlmModelRecord = NonNullable<AppConfig['model_list']>[number];
|
|
35
|
+
|
|
36
|
+
const startsWithAsciiLetter = (value: string): boolean => {
|
|
37
|
+
const firstChar = value.charCodeAt(0);
|
|
38
|
+
|
|
39
|
+
return (firstChar >= 65 && firstChar <= 90) || (firstChar >= 97 && firstChar <= 122);
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
// ─── Types ───────────────────────────────────────────────────────────────────
|
|
43
|
+
|
|
44
|
+
export interface LocalSlowLaneClientConfig {
|
|
45
|
+
/**
|
|
46
|
+
* Optional custom model registration for models not in web-llm's
|
|
47
|
+
* built-in list. When provided, the model is appended to the app
|
|
48
|
+
* config before engine creation.
|
|
49
|
+
*/
|
|
50
|
+
customModelConfig?: {
|
|
51
|
+
/** Context window size override (optional) */
|
|
52
|
+
contextWindowSize?: number;
|
|
53
|
+
/** HuggingFace URL to the model weights (e.g. "https://huggingface.co/HuggingFaceTB/smollm-135M-instruct-add-basics-q0f16-MLC") */
|
|
54
|
+
model: string;
|
|
55
|
+
/** URL to the compiled WASM library for this model architecture */
|
|
56
|
+
modelLib: string;
|
|
57
|
+
/** VRAM required in MB (optional, for resource planning) */
|
|
58
|
+
vramRequiredMB?: number;
|
|
59
|
+
};
|
|
60
|
+
/** Debounce interval in ms before sending context for inference. */
|
|
61
|
+
debounceMs?: number;
|
|
62
|
+
/**
|
|
63
|
+
* MLC model identifier.
|
|
64
|
+
* Defaults to the built-in SmolLM2-135M-Instruct-q0f16-MLC.
|
|
65
|
+
*
|
|
66
|
+
* To use a custom HuggingFace model, provide both `modelId` and
|
|
67
|
+
* `customModelConfig` with the model URL and WASM library URL.
|
|
68
|
+
*/
|
|
69
|
+
modelId?: string;
|
|
70
|
+
/** Callback fired with status messages (model loading progress, etc.). */
|
|
71
|
+
onStatus?: (message: string) => void;
|
|
72
|
+
/** Callback fired when inference returns new results. */
|
|
73
|
+
onUpdate?: (opts: { hasLmLogits: boolean; hasVector: boolean; textLength: number }) => void;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Same return type as createSlowLaneClient for drop-in compatibility
|
|
77
|
+
export interface LocalSlowLaneClient {
|
|
78
|
+
/** Clean up resources. */
|
|
79
|
+
destroy: () => void;
|
|
80
|
+
getContextVector: () => Float32Array | null;
|
|
81
|
+
getLmLogits: () => Record<string, number> | null;
|
|
82
|
+
/** Whether the model is loaded and ready for inference. */
|
|
83
|
+
isReady: () => boolean;
|
|
84
|
+
isWordBoundary: (text: string) => boolean;
|
|
85
|
+
setContextVector: (vector: Float32Array | null) => void;
|
|
86
|
+
setLmLogits: (logits: Record<string, number> | null) => void;
|
|
87
|
+
updateContext: (text: string) => void;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// ─── Constants ───────────────────────────────────────────────────────────────
|
|
91
|
+
|
|
92
|
+
const DEFAULT_DEBOUNCE_MS = 300;
|
|
93
|
+
|
|
94
|
+
export const LOCAL_MLC_MODEL_ID = 'SmolLM2-135M-Instruct-q0f16-MLC';
|
|
95
|
+
|
|
96
|
+
/** HF root for the default weights (includes `tensor-cache.json` for WebLLM 0.2+). */
|
|
97
|
+
export const LOCAL_MLC_HF_MODEL_REPO =
|
|
98
|
+
'https://huggingface.co/mlc-ai/SmolLM2-135M-Instruct-q0f16-MLC';
|
|
99
|
+
|
|
100
|
+
export const LOCAL_MLC_MODEL_LIB_WASM_NAME = 'SmolLM2-135M-Instruct-q0f16-ctx4k_cs1k-webgpu.wasm';
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Original target repo (add-basics fine-tune). **Not compatible with WebLLM 0.2.x** (no `tensor-cache.json`).
|
|
104
|
+
* @see module doc above
|
|
105
|
+
*/
|
|
106
|
+
export const HUGGINGFACE_TB_SMOLLM_ADD_BASICS_REPO =
|
|
107
|
+
'https://huggingface.co/HuggingFaceTB/smollm-135M-instruct-add-basics-q0f16-MLC';
|
|
108
|
+
|
|
109
|
+
// ─── Factory ─────────────────────────────────────────────────────────────────
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Create a local slow-lane client powered by MLC WebLLM.
|
|
113
|
+
*
|
|
114
|
+
* The engine is initialised lazily — model weights are downloaded (and cached
|
|
115
|
+
* in IndexedDB) on first use. Subsequent page loads skip the download.
|
|
116
|
+
*
|
|
117
|
+
* Usage:
|
|
118
|
+
* ```ts
|
|
119
|
+
* const client = createLocalSlowLaneClient({ debounceMs: 300 });
|
|
120
|
+
* // On word boundaries:
|
|
121
|
+
* client.updateContext(docText);
|
|
122
|
+
* // In scoring pipeline:
|
|
123
|
+
* const vec = client.getContextVector();
|
|
124
|
+
* const logits = client.getLmLogits();
|
|
125
|
+
* // On plugin teardown:
|
|
126
|
+
* client.destroy();
|
|
127
|
+
* ```
|
|
128
|
+
*/
|
|
129
|
+
export const createLocalSlowLaneClient = (
|
|
130
|
+
config: LocalSlowLaneClientConfig = {},
|
|
131
|
+
): LocalSlowLaneClient => {
|
|
132
|
+
const {
|
|
133
|
+
debounceMs = DEFAULT_DEBOUNCE_MS,
|
|
134
|
+
onUpdate,
|
|
135
|
+
onStatus,
|
|
136
|
+
modelId = LOCAL_MLC_MODEL_ID,
|
|
137
|
+
customModelConfig,
|
|
138
|
+
} = config;
|
|
139
|
+
|
|
140
|
+
// ── State ──────────────────────────────────────────────────────────────
|
|
141
|
+
let storedContextVector: Float32Array | null = null;
|
|
142
|
+
let storedLmLogits: Record<string, number> | null = null;
|
|
143
|
+
let debounceTimer: ReturnType<typeof setTimeout> | null = null;
|
|
144
|
+
let lastRequestedText = '';
|
|
145
|
+
let requestCounter = 0;
|
|
146
|
+
let latestRequestId = -1;
|
|
147
|
+
let ready = false;
|
|
148
|
+
let destroyed = false;
|
|
149
|
+
let initFailed = false;
|
|
150
|
+
let engine: MLCEngine | null = null;
|
|
151
|
+
let engineInitPromise: Promise<void> | null = null;
|
|
152
|
+
|
|
153
|
+
const unloadEngine = (engineToUnload: MLCEngine): void => {
|
|
154
|
+
engineToUnload.unload().catch((error: unknown) => {
|
|
155
|
+
if (isAutocompleteDebugEnabled()) {
|
|
156
|
+
// eslint-disable-next-line no-console
|
|
157
|
+
console.log(
|
|
158
|
+
'%c[LocalSlowLane] %cFailed to unload engine',
|
|
159
|
+
'color: #9c27b0; font-weight: bold;',
|
|
160
|
+
'color: inherit;',
|
|
161
|
+
error,
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
// ── Engine initialisation ──────────────────────────────────────────────
|
|
168
|
+
|
|
169
|
+
const initProgressCallback = (progress: InitProgressReport): void => {
|
|
170
|
+
const message = `[${(progress.progress * 100).toFixed(0)}%] ${progress.text}`;
|
|
171
|
+
if (isAutocompleteDebugEnabled()) {
|
|
172
|
+
// eslint-disable-next-line no-console
|
|
173
|
+
console.log(
|
|
174
|
+
`%c[LocalSlowLane] %c🔄 ${message}`,
|
|
175
|
+
'color: #9c27b0; font-weight: bold;',
|
|
176
|
+
'color: inherit;',
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
onStatus?.(message);
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
const initEngine = async (): Promise<void> => {
|
|
183
|
+
try {
|
|
184
|
+
if (isAutocompleteDebugEnabled()) {
|
|
185
|
+
// eslint-disable-next-line no-console
|
|
186
|
+
console.log(
|
|
187
|
+
`%c[LocalSlowLane] %c🚀 Initialising MLC engine with model: ${modelId}`,
|
|
188
|
+
'color: #9c27b0; font-weight: bold;',
|
|
189
|
+
'color: inherit;',
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
onStatus?.(`Initialising model: ${modelId}…`);
|
|
193
|
+
|
|
194
|
+
if (!('gpu' in navigator)) {
|
|
195
|
+
throw new Error('WebGPU not supported');
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// 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
|
|
199
|
+
const { CreateMLCEngine, prebuiltAppConfig } = await import(
|
|
200
|
+
/* webpackChunkName: "@atlaskit-internal_editor-plugin-autocomplete-mlc-web-llm" */ '@mlc-ai/web-llm'
|
|
201
|
+
);
|
|
202
|
+
|
|
203
|
+
const customModelRecord: WebLlmModelRecord | undefined = customModelConfig
|
|
204
|
+
? {
|
|
205
|
+
model: customModelConfig.model,
|
|
206
|
+
model_id: modelId,
|
|
207
|
+
model_lib: customModelConfig.modelLib,
|
|
208
|
+
low_resource_required: true,
|
|
209
|
+
required_features: ['shader-f16'],
|
|
210
|
+
...(customModelConfig.vramRequiredMB !== undefined
|
|
211
|
+
? {
|
|
212
|
+
vram_required_MB: customModelConfig.vramRequiredMB,
|
|
213
|
+
}
|
|
214
|
+
: {}),
|
|
215
|
+
...(customModelConfig.contextWindowSize !== undefined
|
|
216
|
+
? {
|
|
217
|
+
overrides: {
|
|
218
|
+
context_window_size: customModelConfig.contextWindowSize,
|
|
219
|
+
},
|
|
220
|
+
}
|
|
221
|
+
: {}),
|
|
222
|
+
}
|
|
223
|
+
: undefined;
|
|
224
|
+
|
|
225
|
+
const appConfig: AppConfig = {
|
|
226
|
+
model_list: [
|
|
227
|
+
...prebuiltAppConfig.model_list,
|
|
228
|
+
...(customModelRecord ? [customModelRecord] : []),
|
|
229
|
+
],
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
engine = await CreateMLCEngine(modelId, {
|
|
233
|
+
appConfig,
|
|
234
|
+
initProgressCallback,
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
if (destroyed) {
|
|
238
|
+
// destroy() was called while we were loading — clean up
|
|
239
|
+
unloadEngine(engine);
|
|
240
|
+
engine = null;
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
ready = true;
|
|
245
|
+
|
|
246
|
+
if (isAutocompleteDebugEnabled()) {
|
|
247
|
+
// eslint-disable-next-line no-console
|
|
248
|
+
console.log(
|
|
249
|
+
'%c[LocalSlowLane] %c✅ MLC engine loaded and ready',
|
|
250
|
+
'color: #9c27b0; font-weight: bold;',
|
|
251
|
+
'color: #4caf50;',
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
onStatus?.('Model loaded and ready.');
|
|
255
|
+
} catch (err) {
|
|
256
|
+
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
257
|
+
ready = false;
|
|
258
|
+
if (isAutocompleteDebugEnabled()) {
|
|
259
|
+
// eslint-disable-next-line no-console
|
|
260
|
+
console.log(`[LocalSlowLane] Engine initialisation failed: ${errorMsg}`);
|
|
261
|
+
}
|
|
262
|
+
onStatus?.(`Engine initialisation failed: ${errorMsg}`);
|
|
263
|
+
engineInitPromise = null;
|
|
264
|
+
initFailed = true;
|
|
265
|
+
}
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
const ensureEngineInitialized = (): Promise<void> => {
|
|
269
|
+
if (initFailed) {
|
|
270
|
+
return Promise.resolve();
|
|
271
|
+
}
|
|
272
|
+
if (!engineInitPromise) {
|
|
273
|
+
engineInitPromise = initEngine();
|
|
274
|
+
}
|
|
275
|
+
return engineInitPromise;
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
// ── Inference ──────────────────────────────────────────────────────────
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Run a single forward pass to extract next-token logit probabilities.
|
|
282
|
+
*
|
|
283
|
+
* We use the chat completions API with `max_tokens: 1` and `logprobs: true`
|
|
284
|
+
* to get the model's next-token distribution without generating text.
|
|
285
|
+
* This is the cheapest possible inference call — a single forward pass.
|
|
286
|
+
*/
|
|
287
|
+
const runInference = async (text: string, requestId: number): Promise<void> => {
|
|
288
|
+
if (!engine || destroyed) {
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
try {
|
|
293
|
+
// Use chat completion with logprobs to get next-token distribution
|
|
294
|
+
const response = await engine.chat.completions.create({
|
|
295
|
+
messages: [
|
|
296
|
+
{
|
|
297
|
+
role: 'user',
|
|
298
|
+
content: text,
|
|
299
|
+
},
|
|
300
|
+
],
|
|
301
|
+
max_tokens: 1,
|
|
302
|
+
logprobs: true,
|
|
303
|
+
top_logprobs: 5,
|
|
304
|
+
temperature: 0,
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
// Discard stale results
|
|
308
|
+
if (requestId < latestRequestId || destroyed) {
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// ── Extract LM logits ───────────────────────────────────────
|
|
313
|
+
const lmLogits: Record<string, number> = {};
|
|
314
|
+
|
|
315
|
+
const logprobsContent = response.choices?.[0]?.logprobs?.content;
|
|
316
|
+
if (logprobsContent && logprobsContent.length > 0) {
|
|
317
|
+
const tokenLogprobs = logprobsContent[0];
|
|
318
|
+
|
|
319
|
+
// Add the top token
|
|
320
|
+
if (tokenLogprobs.token) {
|
|
321
|
+
const token = tokenLogprobs.token.trim().toLowerCase();
|
|
322
|
+
if (token.length > 0 && startsWithAsciiLetter(token)) {
|
|
323
|
+
lmLogits[token] = Math.exp(tokenLogprobs.logprob);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// Add alternative tokens from top_logprobs
|
|
328
|
+
if (tokenLogprobs.top_logprobs) {
|
|
329
|
+
for (const alt of tokenLogprobs.top_logprobs) {
|
|
330
|
+
const token = alt.token.trim().toLowerCase();
|
|
331
|
+
if (token.length > 0 && startsWithAsciiLetter(token)) {
|
|
332
|
+
lmLogits[token] = Math.exp(alt.logprob);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
storedLmLogits = Object.keys(lmLogits).length > 0 ? lmLogits : null;
|
|
339
|
+
|
|
340
|
+
// ── Semantic vector ─────────────────────────────────────────
|
|
341
|
+
// SmolLM is a generative model, not an embedding model, so we
|
|
342
|
+
// don't get a true semantic vector. We generate a lightweight
|
|
343
|
+
// pseudo-embedding from the logit distribution for compatibility
|
|
344
|
+
// with the existing scoring pipeline.
|
|
345
|
+
//
|
|
346
|
+
// For a production implementation, you would use a dedicated
|
|
347
|
+
// embedding model (e.g. via web-llm's embeddings API with an
|
|
348
|
+
// embedding-specific model).
|
|
349
|
+
if (storedLmLogits) {
|
|
350
|
+
const logitValues = Object.values(storedLmLogits);
|
|
351
|
+
storedContextVector = new Float32Array(logitValues);
|
|
352
|
+
} else {
|
|
353
|
+
storedContextVector = null;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
if (isAutocompleteDebugEnabled()) {
|
|
357
|
+
// eslint-disable-next-line no-console
|
|
358
|
+
console.groupCollapsed(
|
|
359
|
+
`%c[LocalSlowLane] %c📥 Inference result (request #${requestId})`,
|
|
360
|
+
'color: #9c27b0; font-weight: bold;',
|
|
361
|
+
'color: inherit;',
|
|
362
|
+
);
|
|
363
|
+
// eslint-disable-next-line no-console
|
|
364
|
+
console.log(
|
|
365
|
+
storedContextVector
|
|
366
|
+
? `✅ pseudo-vector: ${storedContextVector.length} dims`
|
|
367
|
+
: '❌ No vector',
|
|
368
|
+
);
|
|
369
|
+
// eslint-disable-next-line no-console
|
|
370
|
+
console.log(
|
|
371
|
+
storedLmLogits
|
|
372
|
+
? `✅ lm_logits: ${Object.keys(storedLmLogits).length} tokens`
|
|
373
|
+
: '❌ No lm_logits',
|
|
374
|
+
);
|
|
375
|
+
if (storedLmLogits) {
|
|
376
|
+
const topTokens = Object.entries(storedLmLogits)
|
|
377
|
+
.sort(([, a], [, b]) => b - a)
|
|
378
|
+
.slice(0, 10);
|
|
379
|
+
// eslint-disable-next-line no-console
|
|
380
|
+
console.log(
|
|
381
|
+
'Top 10 predictions:',
|
|
382
|
+
topTokens.map(([t, p]) => `${t}: ${(p * 100).toFixed(1)}%`).join(', '),
|
|
383
|
+
);
|
|
384
|
+
}
|
|
385
|
+
// eslint-disable-next-line no-console
|
|
386
|
+
console.groupEnd();
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
onUpdate?.({
|
|
390
|
+
textLength: text.length,
|
|
391
|
+
hasVector: storedContextVector !== null,
|
|
392
|
+
hasLmLogits: storedLmLogits !== null,
|
|
393
|
+
});
|
|
394
|
+
} catch (err) {
|
|
395
|
+
// Discard errors for stale requests
|
|
396
|
+
if (requestId < latestRequestId) {
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
storedContextVector = null;
|
|
401
|
+
storedLmLogits = null;
|
|
402
|
+
onUpdate?.({ textLength: text.length, hasVector: false, hasLmLogits: false });
|
|
403
|
+
|
|
404
|
+
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
405
|
+
if (isAutocompleteDebugEnabled()) {
|
|
406
|
+
// eslint-disable-next-line no-console
|
|
407
|
+
console.log(
|
|
408
|
+
`%c[LocalSlowLane] %c❌ Inference error (request #${requestId}): ${errorMsg}`,
|
|
409
|
+
'color: #9c27b0; font-weight: bold;',
|
|
410
|
+
'color: #f44336;',
|
|
411
|
+
);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
};
|
|
415
|
+
|
|
416
|
+
// ── Context update (debounced) ─────────────────────────────────────────
|
|
417
|
+
|
|
418
|
+
const doUpdateContext = (text: string): void => {
|
|
419
|
+
if (destroyed || !text || text.trim().length === 0) {
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
const requestId = ++requestCounter;
|
|
424
|
+
latestRequestId = requestId;
|
|
425
|
+
|
|
426
|
+
if (isAutocompleteDebugEnabled()) {
|
|
427
|
+
// eslint-disable-next-line no-console
|
|
428
|
+
console.groupCollapsed(
|
|
429
|
+
`%c[LocalSlowLane] %c📤 Context update (request #${requestId}) | ${text.length} chars`,
|
|
430
|
+
'color: #9c27b0; font-weight: bold;',
|
|
431
|
+
'color: inherit;',
|
|
432
|
+
);
|
|
433
|
+
const lines = text.split('\n');
|
|
434
|
+
lines.forEach((line, i) => {
|
|
435
|
+
// eslint-disable-next-line no-console
|
|
436
|
+
console.log(` ${i === lines.length - 1 ? '▶' : ' '} ${line}`);
|
|
437
|
+
});
|
|
438
|
+
// eslint-disable-next-line no-console
|
|
439
|
+
console.groupEnd();
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
void ensureEngineInitialized()
|
|
443
|
+
.then(() => runInference(text, requestId))
|
|
444
|
+
.catch(() => {});
|
|
445
|
+
};
|
|
446
|
+
|
|
447
|
+
const updateContextDebounced = (text: string): void => {
|
|
448
|
+
if (debounceTimer) {
|
|
449
|
+
clearTimeout(debounceTimer);
|
|
450
|
+
}
|
|
451
|
+
lastRequestedText = text;
|
|
452
|
+
debounceTimer = setTimeout(() => {
|
|
453
|
+
debounceTimer = null;
|
|
454
|
+
doUpdateContext(lastRequestedText);
|
|
455
|
+
}, debounceMs);
|
|
456
|
+
};
|
|
457
|
+
|
|
458
|
+
// ── Public API (same shape as createSlowLaneClient) ────────────────────
|
|
459
|
+
return {
|
|
460
|
+
updateContext: updateContextDebounced,
|
|
461
|
+
getContextVector: () => storedContextVector,
|
|
462
|
+
getLmLogits: () => storedLmLogits,
|
|
463
|
+
setContextVector: (vector) => {
|
|
464
|
+
storedContextVector = vector;
|
|
465
|
+
},
|
|
466
|
+
setLmLogits: (logits) => {
|
|
467
|
+
storedLmLogits = logits;
|
|
468
|
+
},
|
|
469
|
+
isWordBoundary,
|
|
470
|
+
isReady: () => ready,
|
|
471
|
+
destroy: () => {
|
|
472
|
+
destroyed = true;
|
|
473
|
+
ready = false;
|
|
474
|
+
if (debounceTimer) {
|
|
475
|
+
clearTimeout(debounceTimer);
|
|
476
|
+
}
|
|
477
|
+
if (engine) {
|
|
478
|
+
const engineToUnload = engine;
|
|
479
|
+
engine = null;
|
|
480
|
+
unloadEngine(engineToUnload);
|
|
481
|
+
}
|
|
482
|
+
engineInitPromise = null;
|
|
483
|
+
storedContextVector = null;
|
|
484
|
+
storedLmLogits = null;
|
|
485
|
+
},
|
|
486
|
+
};
|
|
487
|
+
};
|
|
@@ -7,6 +7,8 @@
|
|
|
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';
|
|
11
|
+
|
|
10
12
|
import { isAutocompleteDebugEnabled } from './debug-mode';
|
|
11
13
|
|
|
12
14
|
// ─── Types ───────────────────────────────────────────────────────────────────
|
|
@@ -84,8 +86,10 @@ export const createSlowLaneClient = (
|
|
|
84
86
|
let lastRequestedText = '';
|
|
85
87
|
let storedContextVector: Float32Array | null = null;
|
|
86
88
|
let storedLmLogits: Record<string, number> | null = null;
|
|
89
|
+
let requestSeq = 0;
|
|
90
|
+
let inflightRequestId: string | null = null;
|
|
87
91
|
|
|
88
|
-
const doUpdateContext = async (text: string): Promise<void> => {
|
|
92
|
+
const doUpdateContext = async (text: string, requestId: string): Promise<void> => {
|
|
89
93
|
if (!text || text.trim().length === 0) {
|
|
90
94
|
return;
|
|
91
95
|
}
|
|
@@ -97,6 +101,8 @@ export const createSlowLaneClient = (
|
|
|
97
101
|
session_id: sessionId,
|
|
98
102
|
};
|
|
99
103
|
|
|
104
|
+
startExp(EXPERIENCE_NAME.SLOW_LANE_FETCH, requestId, { textLength: text.length });
|
|
105
|
+
|
|
100
106
|
if (isAutocompleteDebugEnabled()) {
|
|
101
107
|
// eslint-disable-next-line no-console
|
|
102
108
|
console.groupCollapsed(
|
|
@@ -122,6 +128,10 @@ export const createSlowLaneClient = (
|
|
|
122
128
|
if (!res.ok) {
|
|
123
129
|
storedContextVector = null;
|
|
124
130
|
storedLmLogits = null;
|
|
131
|
+
failExp(EXPERIENCE_NAME.SLOW_LANE_FETCH, requestId, {
|
|
132
|
+
status: res.status,
|
|
133
|
+
errorType: 'http_error',
|
|
134
|
+
});
|
|
125
135
|
if (isAutocompleteDebugEnabled()) {
|
|
126
136
|
// eslint-disable-next-line no-console
|
|
127
137
|
console.log(
|
|
@@ -170,6 +180,12 @@ export const createSlowLaneClient = (
|
|
|
170
180
|
console.groupEnd();
|
|
171
181
|
}
|
|
172
182
|
|
|
183
|
+
succeedExp(EXPERIENCE_NAME.SLOW_LANE_FETCH, requestId, {
|
|
184
|
+
textLength: text.length,
|
|
185
|
+
hasVector: storedContextVector !== null,
|
|
186
|
+
hasLmLogits: storedLmLogits !== null,
|
|
187
|
+
});
|
|
188
|
+
|
|
173
189
|
onUpdate?.({
|
|
174
190
|
textLength: text.length,
|
|
175
191
|
hasVector: storedContextVector !== null,
|
|
@@ -179,6 +195,7 @@ export const createSlowLaneClient = (
|
|
|
179
195
|
} catch (e) {
|
|
180
196
|
storedContextVector = null;
|
|
181
197
|
storedLmLogits = null;
|
|
198
|
+
failExp(EXPERIENCE_NAME.SLOW_LANE_FETCH, requestId, { errorType: 'network' });
|
|
182
199
|
if (isAutocompleteDebugEnabled()) {
|
|
183
200
|
// eslint-disable-next-line no-console
|
|
184
201
|
console.log(
|
|
@@ -187,6 +204,10 @@ export const createSlowLaneClient = (
|
|
|
187
204
|
'color: #f44336;',
|
|
188
205
|
);
|
|
189
206
|
}
|
|
207
|
+
} finally {
|
|
208
|
+
if (inflightRequestId === requestId) {
|
|
209
|
+
inflightRequestId = null;
|
|
210
|
+
}
|
|
190
211
|
}
|
|
191
212
|
};
|
|
192
213
|
|
|
@@ -197,7 +218,12 @@ export const createSlowLaneClient = (
|
|
|
197
218
|
lastRequestedText = text;
|
|
198
219
|
debounceTimer = setTimeout(() => {
|
|
199
220
|
debounceTimer = null;
|
|
200
|
-
|
|
221
|
+
if (inflightRequestId !== null) {
|
|
222
|
+
abortExp(EXPERIENCE_NAME.SLOW_LANE_FETCH, inflightRequestId, 'superseded');
|
|
223
|
+
}
|
|
224
|
+
const requestId = String(++requestSeq);
|
|
225
|
+
inflightRequestId = requestId;
|
|
226
|
+
doUpdateContext(lastRequestedText, requestId);
|
|
201
227
|
}, debounceMs);
|
|
202
228
|
};
|
|
203
229
|
|
|
@@ -15,6 +15,8 @@
|
|
|
15
15
|
* via incrementSessionFreq(), called on word boundaries from the plugin.
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
|
+
import { EXPERIENCE_NAME, failExp, startExp, succeedExp } from '../analytics/ufo';
|
|
19
|
+
|
|
18
20
|
// import bigramsData from './data/bigrams.json';
|
|
19
21
|
import l3VocabularyData from './data/l3_vocabulary.json';
|
|
20
22
|
import vocabularyData from './data/vocabulary_10k.json';
|
|
@@ -723,12 +725,14 @@ export const loadVectorsAsync = async (options?: {
|
|
|
723
725
|
return;
|
|
724
726
|
}
|
|
725
727
|
vectorsLoadStarted = true;
|
|
728
|
+
startExp(EXPERIENCE_NAME.LOAD_VECTORS, 'singleton');
|
|
726
729
|
|
|
727
730
|
let url: string;
|
|
728
731
|
try {
|
|
729
732
|
url = await options.getBinaryUrl();
|
|
730
733
|
} catch (e) {
|
|
731
734
|
vectorsLoadStarted = false;
|
|
735
|
+
failExp(EXPERIENCE_NAME.LOAD_VECTORS, 'singleton', { errorType: 'resolve_url' });
|
|
732
736
|
// eslint-disable-next-line no-console
|
|
733
737
|
console.warn('[text-predictor] Failed to resolve vectors URL:', e);
|
|
734
738
|
return;
|
|
@@ -738,6 +742,10 @@ export const loadVectorsAsync = async (options?: {
|
|
|
738
742
|
const res = await fetch(url);
|
|
739
743
|
if (!res.ok) {
|
|
740
744
|
vectorsLoadStarted = false;
|
|
745
|
+
failExp(EXPERIENCE_NAME.LOAD_VECTORS, 'singleton', {
|
|
746
|
+
status: res.status,
|
|
747
|
+
errorType: 'http_error',
|
|
748
|
+
});
|
|
741
749
|
// eslint-disable-next-line no-console
|
|
742
750
|
console.warn(`[text-predictor] Failed to load vectors: ${res.status}`);
|
|
743
751
|
return;
|
|
@@ -749,6 +757,11 @@ export const loadVectorsAsync = async (options?: {
|
|
|
749
757
|
const dim = float32.length / nWords;
|
|
750
758
|
|
|
751
759
|
vectorStore = { float32, wordIndex, dim };
|
|
760
|
+
succeedExp(EXPERIENCE_NAME.LOAD_VECTORS, 'singleton', {
|
|
761
|
+
wordCount: nWords,
|
|
762
|
+
dim,
|
|
763
|
+
sizeBytes: float32.byteLength,
|
|
764
|
+
});
|
|
752
765
|
if (isAutocompleteDebugEnabled()) {
|
|
753
766
|
// eslint-disable-next-line no-console
|
|
754
767
|
console.log('[text-predictor] Vectors loaded:', {
|
|
@@ -759,6 +772,7 @@ export const loadVectorsAsync = async (options?: {
|
|
|
759
772
|
}
|
|
760
773
|
} catch (e) {
|
|
761
774
|
vectorsLoadStarted = false;
|
|
775
|
+
failExp(EXPERIENCE_NAME.LOAD_VECTORS, 'singleton', { errorType: 'network' });
|
|
762
776
|
// eslint-disable-next-line no-console
|
|
763
777
|
console.warn('[text-predictor] Failed to load vectors:', e);
|
|
764
778
|
}
|
|
@@ -769,17 +783,33 @@ export const initVectors = (store: VectorStore): void => {
|
|
|
769
783
|
};
|
|
770
784
|
|
|
771
785
|
export const loadDefaultVocabulary = (): void => {
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
docFreq: stats.doc_freq,
|
|
778
|
-
authorFreq: stats.author_freq,
|
|
779
|
-
}));
|
|
780
|
-
initVocabulary({ terms });
|
|
786
|
+
if (isInitialized) {
|
|
787
|
+
return;
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
startExp(EXPERIENCE_NAME.LOAD_VOCABULARY, 'singleton');
|
|
781
791
|
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
792
|
+
try {
|
|
793
|
+
// 1. Load the Atlassian Domain (L2)
|
|
794
|
+
const data = vocabularyData as VocabularyJson;
|
|
795
|
+
const terms = Object.entries(data.words).map(([word, stats]) => ({
|
|
796
|
+
word,
|
|
797
|
+
freq: stats.freq,
|
|
798
|
+
docFreq: stats.doc_freq,
|
|
799
|
+
authorFreq: stats.author_freq,
|
|
800
|
+
}));
|
|
801
|
+
initVocabulary({ terms });
|
|
802
|
+
|
|
803
|
+
// 2. Load General English (L3)
|
|
804
|
+
const l3Words = l3VocabularyData as string[];
|
|
805
|
+
initL3Vocabulary(l3Words);
|
|
806
|
+
|
|
807
|
+
succeedExp(EXPERIENCE_NAME.LOAD_VOCABULARY, 'singleton', {
|
|
808
|
+
l2WordCount: terms.length,
|
|
809
|
+
l3WordCount: l3Words.length,
|
|
810
|
+
});
|
|
811
|
+
} catch (e) {
|
|
812
|
+
failExp(EXPERIENCE_NAME.LOAD_VOCABULARY, 'singleton', { errorType: 'parse_error' });
|
|
813
|
+
throw e;
|
|
814
|
+
}
|
|
785
815
|
};
|