@atlaskit/editor-plugin-autocomplete 0.1.0 → 0.2.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 (52) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/afm-cc/tsconfig.json +5 -1
  3. package/afm-jira/tsconfig.json +5 -1
  4. package/afm-products/tsconfig.json +5 -1
  5. package/build/tsconfig.json +24 -0
  6. package/dist/cjs/autocompletePlugin.js +18 -5
  7. package/dist/cjs/autocompletePluginType.js +5 -1
  8. package/dist/cjs/pm-plugins/autocomplete-plugin.js +361 -0
  9. package/dist/cjs/pm-plugins/ghost-text-decoration.js +39 -0
  10. package/dist/cjs/pm-plugins/scoring-pipeline.js +258 -0
  11. package/dist/cjs/pm-plugins/slow-lane-client.js +197 -0
  12. package/dist/cjs/pm-plugins/text-predictor.js +786 -0
  13. package/dist/es2019/autocompletePlugin.js +20 -6
  14. package/dist/es2019/autocompletePluginType.js +1 -0
  15. package/dist/es2019/pm-plugins/autocomplete-plugin.js +360 -0
  16. package/dist/es2019/pm-plugins/ghost-text-decoration.js +33 -0
  17. package/dist/es2019/pm-plugins/scoring-pipeline.js +224 -0
  18. package/dist/es2019/pm-plugins/slow-lane-client.js +154 -0
  19. package/dist/es2019/pm-plugins/text-predictor.js +624 -0
  20. package/dist/esm/autocompletePlugin.js +18 -5
  21. package/dist/esm/autocompletePluginType.js +1 -0
  22. package/dist/esm/pm-plugins/autocomplete-plugin.js +354 -0
  23. package/dist/esm/pm-plugins/ghost-text-decoration.js +33 -0
  24. package/dist/esm/pm-plugins/scoring-pipeline.js +255 -0
  25. package/dist/esm/pm-plugins/slow-lane-client.js +190 -0
  26. package/dist/esm/pm-plugins/text-predictor.js +783 -0
  27. package/dist/types/autocompletePluginType.d.ts +6 -3
  28. package/dist/types/pm-plugins/autocomplete-plugin.d.ts +36 -0
  29. package/dist/types/pm-plugins/ghost-text-decoration.d.ts +7 -0
  30. package/dist/types/pm-plugins/scoring-pipeline.d.ts +33 -0
  31. package/dist/types/pm-plugins/slow-lane-client.d.ts +46 -0
  32. package/dist/types/pm-plugins/text-predictor.d.ts +90 -0
  33. package/dist/types-ts4.5/autocompletePluginType.d.ts +6 -3
  34. package/dist/types-ts4.5/pm-plugins/autocomplete-plugin.d.ts +36 -0
  35. package/dist/types-ts4.5/pm-plugins/ghost-text-decoration.d.ts +7 -0
  36. package/dist/types-ts4.5/pm-plugins/scoring-pipeline.d.ts +33 -0
  37. package/dist/types-ts4.5/pm-plugins/slow-lane-client.d.ts +46 -0
  38. package/dist/types-ts4.5/pm-plugins/text-predictor.d.ts +90 -0
  39. package/package.json +2 -2
  40. package/src/autocompletePlugin.tsx +25 -5
  41. package/src/autocompletePluginType.ts +14 -3
  42. package/src/pm-plugins/autocomplete-plugin/package.json +15 -0
  43. package/src/pm-plugins/autocomplete-plugin.ts +429 -0
  44. package/src/pm-plugins/ghost-text-decoration.ts +44 -0
  45. package/src/pm-plugins/scoring-pipeline.ts +297 -0
  46. package/src/pm-plugins/slow-lane-client/package.json +15 -0
  47. package/src/pm-plugins/slow-lane-client.ts +220 -0
  48. package/src/pm-plugins/text-predictor/package.json +15 -0
  49. package/src/pm-plugins/text-predictor.ts +771 -0
  50. package/src/pm-plugins/typings.d.ts +12 -0
  51. package/tsconfig.app.json +10 -2
  52. package/tsconfig.json +3 -1
@@ -0,0 +1,624 @@
1
+ import _defineProperty from "@babel/runtime/helpers/defineProperty";
2
+ /**
3
+ * Fast Lane Predictor: Local autocomplete using weighted trie + frequency + semantic scoring.
4
+ *
5
+ * Two prediction modes:
6
+ * 1. Word boundary → bigram-based next-word suggestion (grammar-filtered)
7
+ * 2. Mid-word (≥3 chars) → trie prefix search → scoring pipeline → top result
8
+ *
9
+ * Scoring is delegated to scoring-pipeline.ts which handles:
10
+ * Stage 1 (semantic + frequency), grammar filter, Stage 2 (optional LM re-ranking).
11
+ *
12
+ * Context vector: average of word vectors from text before cursor (last N words).
13
+ * Falls back to cold mode (freq-only) when vectors not yet loaded.
14
+ *
15
+ * Session personalization (L1): words the user types are incrementally boosted
16
+ * via incrementSessionFreq(), called on word boundaries from the plugin.
17
+ */
18
+
19
+ // import bigramsData from './data/bigrams.json';
20
+ import l3VocabularyData from './data/l3_vocabulary.json';
21
+ import vocabularyData from './data/vocabulary_10k.json';
22
+ import wordIndexData from './data/word_index_10k.json';
23
+ // import { rankCandidates, isGrammarAllowed } from './scoring-pipeline';
24
+ import { rankCandidates } from './scoring-pipeline';
25
+ import { getStoredContextVector, getStoredLmLogits } from './slow-lane-client';
26
+
27
+ // ─── Constants ───────────────────────────────────────────────────────────────
28
+
29
+ const PUNCTUATION_BOUNDARY_REGEX = /^[.,;:!?()\[\]{}"'`]+|[.,;:!?()\[\]{}"'`]+$/gu;
30
+ const MIN_PREFIX_LENGTH = 3;
31
+ const MAX_CANDIDATES = 200;
32
+ const CONTEXT_WORDS = 10;
33
+ const MIN_SCORE_THRESHOLD = 0.2;
34
+ const L3_BASELINE_FREQ = 0.001;
35
+
36
+ // ─── Types ───────────────────────────────────────────────────────────────────
37
+
38
+ class TrieNode {
39
+ constructor() {
40
+ _defineProperty(this, "children", new Map());
41
+ _defineProperty(this, "word", null);
42
+ _defineProperty(this, "tenantFreq", 0);
43
+ _defineProperty(this, "docFreq", 0);
44
+ _defineProperty(this, "authorFreq", 0);
45
+ _defineProperty(this, "sessionFreq", 0);
46
+ }
47
+ }
48
+ class WeightedWordTrie {
49
+ constructor() {
50
+ _defineProperty(this, "root", new TrieNode());
51
+ /** Highest tenantFreq seen — used to normalize freq scores at query time */
52
+ _defineProperty(this, "maxTenantFreq", 1);
53
+ }
54
+ insert(word, tenantFreq, docFreq, authorFreq) {
55
+ let node = this.root;
56
+ for (const char of word.toLowerCase()) {
57
+ let next = node.children.get(char);
58
+ if (!next) {
59
+ next = new TrieNode();
60
+ node.children.set(char, next);
61
+ }
62
+ node = next;
63
+ }
64
+ node.word = word;
65
+ node.tenantFreq = tenantFreq;
66
+ node.docFreq = docFreq;
67
+ node.authorFreq = authorFreq;
68
+ if (tenantFreq > this.maxTenantFreq) {
69
+ this.maxTenantFreq = tenantFreq;
70
+ }
71
+ }
72
+
73
+ /**
74
+ * Return all words matching this prefix, up to maxResults.
75
+ * O(prefix_length + results) — traverses to the prefix node then collects subtree.
76
+ */
77
+ getCandidates(prefix, maxResults = MAX_CANDIDATES) {
78
+ let node = this.root;
79
+ for (const char of prefix.toLowerCase()) {
80
+ const next = node.children.get(char);
81
+ if (!next) {
82
+ return [];
83
+ }
84
+ node = next;
85
+ }
86
+ const candidates = [];
87
+ const stack = [node];
88
+ while (stack.length > 0 && candidates.length < maxResults) {
89
+ const current = stack.pop();
90
+ if (!current) {
91
+ continue;
92
+ }
93
+
94
+ // Only add candidates that are longer than the prefix
95
+ if (current.word && current.word.length > prefix.length) {
96
+ candidates.push({
97
+ word: current.word,
98
+ node: current
99
+ });
100
+ }
101
+ for (const child of current.children.values()) {
102
+ stack.push(child);
103
+ }
104
+ }
105
+ return candidates;
106
+ }
107
+ findNode(word) {
108
+ let node = this.root;
109
+ for (const char of word.toLowerCase()) {
110
+ const next = node.children.get(char);
111
+ if (!next) {
112
+ return null;
113
+ }
114
+ node = next;
115
+ }
116
+ return node.word !== null ? node : null;
117
+ }
118
+
119
+ /**
120
+ * Set the session frequency for a word.
121
+ * Returns true if the word exists in the trie.
122
+ */
123
+ updateSessionFreq(word, count) {
124
+ const node = this.findNode(word);
125
+ if (!node) {
126
+ return false;
127
+ }
128
+ node.sessionFreq = count;
129
+ return true;
130
+ }
131
+
132
+ /**
133
+ * Increment the session frequency for a word by 1.
134
+ * Returns true if the word exists in the trie.
135
+ */
136
+ incrementSessionFreq(word) {
137
+ const node = this.findNode(word);
138
+ if (!node) {
139
+ return false;
140
+ }
141
+ node.sessionFreq += 1;
142
+ return true;
143
+ }
144
+ }
145
+
146
+ // L1/L2 Trie (Session + Atlassian Domain)
147
+ const wordTrie = new WeightedWordTrie();
148
+
149
+ // L3 Trie (General English Fallback)
150
+ const l3Trie = new WeightedWordTrie();
151
+
152
+ // --- Initialization Function ---
153
+ /**
154
+ * Loads the General English vocabulary.
155
+ * expects a simple array of strings: ["about", "above", "actually", ...]
156
+ */
157
+ export const initL3Vocabulary = l3Words => {
158
+ for (const word of l3Words) {
159
+ // Insert with a tiny baseline frequency so it mathematically
160
+ // loses to any domain word in Stage 1, but still scores above 0.
161
+ l3Trie.insert(word, L3_BASELINE_FREQ, 0, 0);
162
+ }
163
+ if (debugMode) {
164
+ // eslint-disable-next-line no-console
165
+ console.log(`[text-predictor] L3 General English loaded: ${l3Words.length} words`);
166
+ }
167
+ };
168
+
169
+ // const bigramMap: Map<string, Record<string, number>> = new Map(
170
+ // Object.entries(bigramsData as Record<string, Record<string, number>>),
171
+ // );
172
+
173
+ let isInitialized = false;
174
+ let vectorStore = null;
175
+ let vectorsLoadStarted = false;
176
+ let debugMode = true;
177
+ let lastPredictionDebug = null;
178
+ let hasLoggedSemanticActive = false;
179
+
180
+ /** Get vector for a word from the store. */
181
+ const getWordVector = word => {
182
+ if (!vectorStore) {
183
+ return null;
184
+ }
185
+ const idx = vectorStore.wordIndex[word.toLowerCase()];
186
+ if (idx === undefined) {
187
+ return null;
188
+ }
189
+ const start = idx * vectorStore.dim;
190
+ return vectorStore.float32.subarray(start, start + vectorStore.dim);
191
+ };
192
+
193
+ /**
194
+ * Compute context vector by averaging vectors of last N words in text.
195
+ * Falls back to null (cold mode) if no words have vectors.
196
+ */
197
+ const computeContextVectorLocal = textBefore => {
198
+ if (!vectorStore) {
199
+ return null;
200
+ }
201
+ const tokens = tokenize(textBefore);
202
+ const words = tokens.slice(-CONTEXT_WORDS);
203
+ const vectors = [];
204
+ for (const word of words) {
205
+ const v = getWordVector(word);
206
+ if (v) {
207
+ vectors.push(v);
208
+ }
209
+ }
210
+ if (vectors.length === 0) {
211
+ return null;
212
+ }
213
+ const dim = vectorStore.dim;
214
+ const avg = new Float32Array(dim);
215
+ for (const v of vectors) {
216
+ for (let i = 0; i < dim; i++) {
217
+ avg[i] += v[i];
218
+ }
219
+ }
220
+ for (let i = 0; i < dim; i++) {
221
+ avg[i] /= vectors.length;
222
+ }
223
+ return avg;
224
+ };
225
+
226
+ /**
227
+ * Get context vector for scoring. Prefers Slow Lane (BE) context when available
228
+ * and dimension matches; otherwise falls back to local averaging.
229
+ */
230
+ const getContextVectorForScoring = textBefore => {
231
+ const slowLaneVector = getStoredContextVector();
232
+ if (slowLaneVector && vectorStore && slowLaneVector.length === vectorStore.dim) {
233
+ return slowLaneVector;
234
+ }
235
+ return computeContextVectorLocal(textBefore);
236
+ };
237
+ const tokenize = text => {
238
+ const tokens = [];
239
+ // eslint-disable-next-line @atlassian/perf-linting/no-expensive-split-replace
240
+ for (const raw of text.toLowerCase().split(/\s+/u)) {
241
+ // eslint-disable-next-line @atlassian/perf-linting/no-expensive-split-replace
242
+ const clean = raw.replace(PUNCTUATION_BOUNDARY_REGEX, '');
243
+ if (clean.length >= 2) {
244
+ tokens.push(clean);
245
+ }
246
+ }
247
+ return tokens;
248
+ };
249
+ const extractPreviousWord = text => {
250
+ // 1. Split the text by newlines or punctuation (. ? !)
251
+ const sentences = text.split(/[\n.?!]+/u);
252
+
253
+ // 2. Only look at the current sentence/line the user is typing in
254
+ const currentSentence = sentences[sentences.length - 1];
255
+
256
+ // 3. Extract the previous word as normal
257
+ const words = currentSentence.trimEnd().split(/\s+/u);
258
+ return words.length >= 2 ? words[words.length - 2] : '';
259
+ };
260
+
261
+ // ─── Debug Helpers ───────────────────────────────────────────────────────────
262
+
263
+ /** Enable or disable debug logging. Also checks localStorage key `autocomplete-debug`. */
264
+ export const setDebugMode = enabled => {
265
+ debugMode = enabled;
266
+ };
267
+
268
+ /** Check localStorage for autocomplete-debug on first access. */
269
+ const ensureDebugModeFromStorage = () => {
270
+ if (typeof localStorage !== 'undefined' && localStorage.getItem('autocomplete-debug') === '1') {
271
+ debugMode = true;
272
+ }
273
+ };
274
+
275
+ /**
276
+ * Get predictor status for debugging.
277
+ * vectorsLoaded: true when semantic scoring is active
278
+ * wordCount: number of words in vector store (0 if not loaded)
279
+ */
280
+ export const getPredictorStatus = () => {
281
+ ensureDebugModeFromStorage();
282
+ return {
283
+ vectorsLoaded: vectorStore !== null,
284
+ wordCount: vectorStore ? Object.keys(vectorStore.wordIndex).length : 0,
285
+ vectorsLoadStarted,
286
+ isInitialized
287
+ };
288
+ };
289
+
290
+ /**
291
+ * Get details of the last prediction (for debugging).
292
+ * Returns null if no prediction has run yet or debug was off.
293
+ */
294
+ export const getLastPredictionDebug = () => {
295
+ ensureDebugModeFromStorage();
296
+ return lastPredictionDebug;
297
+ };
298
+ export const initVocabulary = vocabulary => {
299
+ for (const term of vocabulary.terms) {
300
+ wordTrie.insert(term.word, term.freq, term.docFreq, term.authorFreq);
301
+ }
302
+ isInitialized = true;
303
+ };
304
+
305
+ /**
306
+ * Increment L1 session frequency for a single word.
307
+ * Called from the plugin on word boundaries for efficient incremental boosting.
308
+ */
309
+ export const incrementSessionFreq = word => {
310
+ wordTrie.incrementSessionFreq(word);
311
+ };
312
+
313
+ /**
314
+ * Prime session frequencies from a document page string.
315
+ *
316
+ * Iterates through every token in `pageContent` and increments its session
317
+ * frequency so that words already present on the page receive an L1 boost
318
+ * before the user starts typing.
319
+ *
320
+ * Pass `undefined` (or omit the argument) to skip priming — useful when the
321
+ * calling context does not yet have a page value available.
322
+ */
323
+ // NOTE: We ingest full page context here
324
+ export const ingestDocumentPage = pageContent => {
325
+ if (!pageContent) {
326
+ return;
327
+ }
328
+ const words = tokenize(pageContent);
329
+ const validBoostedWords = new Set();
330
+ for (const word of words) {
331
+ const didBoost = wordTrie.incrementSessionFreq(word);
332
+ if (didBoost) {
333
+ validBoostedWords.add(word);
334
+ }
335
+ }
336
+ if (debugMode && validBoostedWords.size > 0) {
337
+ // eslint-disable-next-line no-console
338
+ console.groupCollapsed(`%c[L1 Session] %cPrimed ${validBoostedWords.size} valid dictionary words from page`, 'color: #00b8d9; font-weight: bold;', 'color: inherit; font-style: italic;');
339
+ // eslint-disable-next-line no-console
340
+ console.dir(Array.from(validBoostedWords).sort());
341
+ // eslint-disable-next-line no-console
342
+ console.groupEnd();
343
+ }
344
+ };
345
+ export const predict = textBefore => {
346
+ ensureDebugModeFromStorage();
347
+ if (!isInitialized) {
348
+ loadDefaultVocabulary();
349
+ }
350
+ const t0 = performance.now();
351
+
352
+ // ── Step 1: Bigram-based next-word suggestion at word boundary ───────────
353
+ // if (textBefore.length > 0 && /\s$/u.test(textBefore)) {
354
+ // const words = textBefore.toLowerCase().trimEnd().split(/\s+/u);
355
+ // const prevWord = words[words.length - 1];
356
+ // const nextWords = bigramMap.get(prevWord);
357
+ // if (nextWords) {
358
+ // const sorted = Object.entries(nextWords).sort((a, b) => b[1] - a[1]);
359
+ // let bestWord = '';
360
+ // for (const [word] of sorted) {
361
+ // if (!isGrammarAllowed(prevWord, word)) {
362
+ // continue;
363
+ // }
364
+ // bestWord = word;
365
+ // break;
366
+ // }
367
+ // if (bestWord) {
368
+ // if (debugMode) {
369
+ // const latencyMs = performance.now() - t0;
370
+ // console.log(
371
+ // '%c[autocomplete] BIGRAM',
372
+ // 'color:cyan',
373
+ // '| "' + prevWord + '" -> "' + bestWord + '" | ' + latencyMs.toFixed(1) + 'ms',
374
+ // );
375
+ // }
376
+ // return bestWord;
377
+ // }
378
+ // }
379
+ // if (debugMode) {
380
+ // console.log(
381
+ // '%c[autocomplete] BIGRAM-MISS',
382
+ // 'color:gray',
383
+ // '| no bigram for "' + words[words.length - 1] + '", skipping prefix completion',
384
+ // );
385
+ // }
386
+ // return null;
387
+ // }
388
+
389
+ // ── Step 2: Prefix completion (≥3 chars typed) ──────────────────────────
390
+ if (textBefore.length > 0 && /\s$/u.test(textBefore)) {
391
+ return null;
392
+ }
393
+ const trimmed = textBefore.trimEnd();
394
+ const lastSpaceIdx = trimmed.lastIndexOf(' ');
395
+ const currentWord = lastSpaceIdx === -1 ? trimmed : trimmed.slice(lastSpaceIdx + 1);
396
+ if (currentWord.length < MIN_PREFIX_LENGTH) {
397
+ return null;
398
+ }
399
+
400
+ // 1. Primary Query: Ask the L2 Domain Trie
401
+ const candidates = wordTrie.getCandidates(currentWord, MAX_CANDIDATES);
402
+
403
+ // 2. Fallback Query: Gap-fill with the L3 General English Trie
404
+ if (candidates.length < MAX_CANDIDATES) {
405
+ // Ask L3 for MAX_CANDIDATES to guarantee we have enough buffer
406
+ // to survive the deduplication process.
407
+ const l3Candidates = l3Trie.getCandidates(currentWord, MAX_CANDIDATES);
408
+ const existingWords = new Set(candidates.map(c => c.word));
409
+ for (const l3c of l3Candidates) {
410
+ if (candidates.length >= MAX_CANDIDATES) break; // Stop exactly at the limit
411
+
412
+ if (!existingWords.has(l3c.word)) {
413
+ candidates.push(l3c);
414
+ }
415
+ }
416
+ }
417
+
418
+ // If both Tries are completely empty for this prefix
419
+ if (candidates.length === 0) {
420
+ return null;
421
+ }
422
+ const previousWord = extractPreviousWord(trimmed);
423
+ const contextVector = getContextVectorForScoring(trimmed);
424
+ const lmLogits = getStoredLmLogits();
425
+
426
+ // Raw LM Output Logger
427
+ if (debugMode && lmLogits && Object.keys(lmLogits).length > 0) {
428
+ const rawLmTop = Object.entries(lmLogits).sort((a, b) => b[1] - a[1]).slice(0, 5).map(([word, score]) => ({
429
+ Word: word,
430
+ Prob: Number(score.toFixed(5))
431
+ }));
432
+ // eslint-disable-next-line no-console
433
+ console.log('%c[Raw LM Prediction]🧠', 'color: #e83e8c; font-weight: bold;', rawLmTop);
434
+ }
435
+ const mode = contextVector ? 'warm' : 'cold';
436
+ if (debugMode && contextVector && !hasLoggedSemanticActive) {
437
+ hasLoggedSemanticActive = true;
438
+ }
439
+
440
+ // Build ScoringCandidate array from TrieNodes
441
+ const scoringCandidates = candidates.map(({
442
+ word,
443
+ node
444
+ }) => ({
445
+ word,
446
+ tenantFreq: node.tenantFreq,
447
+ docFreq: node.docFreq,
448
+ authorFreq: node.authorFreq,
449
+ sessionFreq: node.sessionFreq
450
+ }));
451
+ const {
452
+ candidates: ranked,
453
+ grammarMeta
454
+ } = rankCandidates(scoringCandidates, contextVector, w => getWordVector(w), lmLogits, wordTrie.maxTenantFreq, previousWord);
455
+ const best = ranked[0];
456
+ const suggestion = best && best.finalScore >= MIN_SCORE_THRESHOLD ? best.word.slice(currentWord.length) : null;
457
+ if (debugMode) {
458
+ const latencyMs = performance.now() - t0;
459
+ const tokens = tokenize(trimmed);
460
+ const contextWords = tokens.slice(-CONTEXT_WORDS);
461
+ const belowThreshold = best && best.finalScore < MIN_SCORE_THRESHOLD;
462
+ const suggestionLabel = belowThreshold ? '🚫 (below threshold)' : suggestion && suggestion.length > 0 ? `✨ "${suggestion}"` : '🚫 (no match)';
463
+ lastPredictionDebug = {
464
+ textBefore: trimmed,
465
+ currentWord,
466
+ mode,
467
+ contextWords,
468
+ topCandidates: ranked.slice(0, 5).map(r => ({
469
+ word: r.word,
470
+ finalScore: r.finalScore,
471
+ semanticScore: r.semanticScore,
472
+ freqScore: r.freqScore,
473
+ lmScore: r.lmScore
474
+ })),
475
+ suggestion: suggestion && suggestion.length > 0 ? suggestion : null
476
+ };
477
+
478
+ // ── Mode label: COLD / WARM(local) / WARM(BE) ───────────────────────
479
+ const slowLaneVec = getStoredContextVector();
480
+ const isUsingSlowLaneVector = slowLaneVec !== null && vectorStore !== null && slowLaneVec.length === vectorStore.dim;
481
+ const modeLabel = !contextVector ? 'COLD' : isUsingSlowLaneVector ? 'WARM(BE)' : 'WARM(local)';
482
+ const modeColor = modeLabel === 'WARM(BE)' ? 'color: #ff9800; font-weight: bold;' : modeLabel === 'WARM(local)' ? 'color: #4caf50; font-weight: bold;' : 'color: #9e9e9e; font-weight: bold;';
483
+
484
+ // 1. Collapsible group header
485
+ // eslint-disable-next-line no-console
486
+ console.groupCollapsed(`%c[Autocomplete] %c${modeLabel} %c| "${currentWord}" ➔ ${suggestionLabel} | ⏱ ${latencyMs.toFixed(1)}ms`, 'color: #00b8d9; font-weight: bold;', modeColor, 'color: inherit; font-weight: normal;');
487
+
488
+ // 2. Context window (what local vector averaging sees)
489
+ // eslint-disable-next-line no-console
490
+ console.log('%cContext Window:', 'color: #888; font-style: italic;', contextWords.length ? contextWords.join(' ') : '(none)');
491
+
492
+ // 3. Slow-lane status
493
+ const vectorStatus = isUsingSlowLaneVector ? `✅ BE semantic vector (dim=${slowLaneVec === null || slowLaneVec === void 0 ? void 0 : slowLaneVec.length})` : vectorStore ? '⚠️ Local vector average (slow-lane not yet returned)' : '❌ No vectors (cold)';
494
+ const logitsStatus = lmLogits && Object.keys(lmLogits).length > 0 ? `✅ LM logits active (${Object.keys(lmLogits).length} tokens)` : '⏳ No LM logits (slow-lane pending or failed)';
495
+ // eslint-disable-next-line no-console
496
+ console.log('%cSlow Lane:', 'color: #888; font-style: italic;', vectorStatus, '|', logitsStatus);
497
+
498
+ // 4. Scoring formula active this prediction
499
+ const formulaLabel = lmLogits && Object.keys(lmLogits).length > 0 ? 'Stage1(×0.6) + LM(×0.4)' : 'Stage1 only (no LM logits)';
500
+ // eslint-disable-next-line no-console
501
+ console.log('%cFormula:', 'color: #888; font-style: italic;', formulaLabel);
502
+
503
+ // 5. Grammar filter result (collected inside rankCandidates, logged here)
504
+ if (grammarMeta) {
505
+ // eslint-disable-next-line no-console
506
+ console.log(`%c[Grammar] "${grammarMeta.prevWord}" [${grammarMeta.prevTags.join('|')}] → ${grammarMeta.before} candidates → ${grammarMeta.after} after filter`, 'color: #4caf50; font-weight: bold;');
507
+ if (grammarMeta.dropped.length > 0) {
508
+ // eslint-disable-next-line no-console
509
+ console.log(`%c🚫 Dropped: ${grammarMeta.dropped.join(', ')}`, 'color: #f44336; font-style: italic;');
510
+ }
511
+ }
512
+
513
+ // 6. Candidate table
514
+ if (ranked.length > 0) {
515
+ const lmCoverage = ranked.slice(0, 10).filter(r => r.lmScore > 0.05).length;
516
+ // eslint-disable-next-line no-console
517
+ console.log(`%cLM coverage: ${lmCoverage}/${Math.min(ranked.length, 10)} candidates had real logit scores`, 'color: #888; font-style: italic;');
518
+ const tableData = ranked.slice(0, 10).map(r => {
519
+ let rawLogit = 'Not in Payload';
520
+ if (lmLogits) {
521
+ const val = lmLogits[r.word.toLowerCase()];
522
+ if (val !== undefined) {
523
+ rawLogit = Number(val.toFixed(5));
524
+ }
525
+ }
526
+ const original = scoringCandidates.find(sc => sc.word === r.word);
527
+ let source = 'Unknown';
528
+ if (original) {
529
+ if (original.docFreq === 0 && original.tenantFreq === L3_BASELINE_FREQ) {
530
+ source = '🌍 L3 (Generic)';
531
+ } else if (original.sessionFreq > 0 && original.tenantFreq === 0) {
532
+ source = '👤 L1 (Session Only)';
533
+ } else {
534
+ source = '🏢 L2 (Domain)';
535
+ }
536
+ }
537
+ return {
538
+ Candidate: r.word,
539
+ Source: source,
540
+ 'Final Score': Number(r.finalScore.toFixed(4)),
541
+ Semantics: Number(r.semanticScore.toFixed(4)),
542
+ Freq: Number(r.freqScore.toFixed(4)),
543
+ 'LM Score': Number(r.lmScore.toFixed(4)),
544
+ 'Raw Logit': rawLogit,
545
+ 'Session Freq': (original === null || original === void 0 ? void 0 : original.sessionFreq) || 0
546
+ };
547
+ });
548
+
549
+ // eslint-disable-next-line no-console
550
+ console.table(tableData);
551
+ } else {
552
+ // eslint-disable-next-line no-console
553
+ console.log('No candidates found.');
554
+ }
555
+
556
+ // eslint-disable-next-line no-console
557
+ console.groupEnd();
558
+ }
559
+ return suggestion && suggestion.length > 0 ? suggestion : null;
560
+ };
561
+
562
+ // ─── Data Loading ────────────────────────────────────────────────────────────
563
+
564
+ const DEFAULT_VECTORS_URL = '/data/word-vectors_10k.bin';
565
+ export const loadVectorsAsync = async options => {
566
+ var _options$vectorsUrl;
567
+ if (vectorStore || vectorsLoadStarted) {
568
+ return;
569
+ }
570
+ vectorsLoadStarted = true;
571
+ const url = (_options$vectorsUrl = options === null || options === void 0 ? void 0 : options.vectorsUrl) !== null && _options$vectorsUrl !== void 0 ? _options$vectorsUrl : DEFAULT_VECTORS_URL;
572
+ try {
573
+ const res = await fetch(url);
574
+ if (!res.ok) {
575
+ vectorsLoadStarted = false;
576
+ // eslint-disable-next-line no-console
577
+ console.warn(`[text-predictor] Failed to load vectors: ${res.status}`);
578
+ return;
579
+ }
580
+ const buffer = await res.arrayBuffer();
581
+ const float32 = new Float32Array(buffer);
582
+ const wordIndex = wordIndexData;
583
+ const nWords = Object.keys(wordIndex).length;
584
+ const dim = float32.length / nWords;
585
+ vectorStore = {
586
+ float32,
587
+ wordIndex,
588
+ dim
589
+ };
590
+ ensureDebugModeFromStorage();
591
+ if (debugMode) {
592
+ // eslint-disable-next-line no-console
593
+ console.log('[text-predictor] Vectors loaded:', {
594
+ wordCount: nWords,
595
+ dim,
596
+ sizeBytes: float32.byteLength
597
+ });
598
+ }
599
+ } catch (e) {
600
+ vectorsLoadStarted = false;
601
+ // eslint-disable-next-line no-console
602
+ console.warn('[text-predictor] Failed to load vectors:', e);
603
+ }
604
+ };
605
+ export const initVectors = store => {
606
+ vectorStore = store;
607
+ };
608
+ export const loadDefaultVocabulary = () => {
609
+ // 1. Load the Atlassian Domain (L2)
610
+ const data = vocabularyData;
611
+ const terms = Object.entries(data.words).map(([word, stats]) => ({
612
+ word,
613
+ freq: stats.freq,
614
+ docFreq: stats.doc_freq,
615
+ authorFreq: stats.author_freq
616
+ }));
617
+ initVocabulary({
618
+ terms
619
+ });
620
+
621
+ // 2. Load General English (L3)
622
+ const l3Words = l3VocabularyData;
623
+ initL3Vocabulary(l3Words);
624
+ };
@@ -1,8 +1,21 @@
1
- // This file will contain the autocomplete plugin implementation.
2
- // The full implementation will be added in a follow-up PR.
3
-
4
- export var autocompletePlugin = function autocompletePlugin() {
1
+ import { autocompletePluginKey, createAutocompletePlugin } from './pm-plugins/autocomplete-plugin';
2
+ export var autocompletePlugin = function autocompletePlugin(_ref) {
3
+ var options = _ref.config;
5
4
  return {
6
- name: 'autocomplete'
5
+ name: 'autocomplete',
6
+ getSharedState: function getSharedState(editorState) {
7
+ if (!editorState) {
8
+ return undefined;
9
+ }
10
+ return autocompletePluginKey.getState(editorState);
11
+ },
12
+ pmPlugins: function pmPlugins() {
13
+ return [{
14
+ name: 'autocomplete',
15
+ plugin: function plugin() {
16
+ return createAutocompletePlugin(options);
17
+ }
18
+ }];
19
+ }
7
20
  };
8
21
  };
@@ -0,0 +1 @@
1
+ export {};