@atlaskit/editor-plugin-autocomplete 3.0.0 → 3.1.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 +8 -0
- package/dist/cjs/pm-plugins/autocomplete-plugin.js +6 -22
- package/dist/cjs/pm-plugins/scoring-pipeline.js +5 -4
- package/dist/cjs/pm-plugins/text-predictor.js +152 -60
- package/dist/es2019/pm-plugins/autocomplete-plugin.js +6 -22
- package/dist/es2019/pm-plugins/scoring-pipeline.js +5 -4
- package/dist/es2019/pm-plugins/text-predictor.js +105 -50
- package/dist/esm/pm-plugins/autocomplete-plugin.js +6 -22
- package/dist/esm/pm-plugins/scoring-pipeline.js +5 -4
- package/dist/esm/pm-plugins/text-predictor.js +144 -60
- package/dist/types/pm-plugins/text-predictor.d.ts +1 -1
- package/dist/types-ts4.5/pm-plugins/text-predictor.d.ts +1 -1
- package/package.json +2 -2
- package/src/pm-plugins/autocomplete-plugin.ts +6 -16
- package/src/pm-plugins/data/word_index_10k.json +7761 -7759
- package/src/pm-plugins/scoring-pipeline.ts +4 -5
- package/src/pm-plugins/text-predictor.ts +124 -45
|
@@ -18,11 +18,9 @@ import _defineProperty from "@babel/runtime/helpers/defineProperty";
|
|
|
18
18
|
|
|
19
19
|
import { EXPERIENCE_NAME, failExp, startExp, succeedExp } from '../analytics/ufo';
|
|
20
20
|
|
|
21
|
-
//
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
import wordIndexData from './data/word_index_10k.json';
|
|
25
|
-
// import { rankCandidates, isGrammarAllowed } from './scoring-pipeline';
|
|
21
|
+
// The vocabulary, L3 and word-index JSON payloads are dynamically imported in
|
|
22
|
+
// loadDefaultVocabulary / loadVectorsAsync so their (large) contents stay out of
|
|
23
|
+
// the editor's main chunk and only load when autocomplete is initialised.
|
|
26
24
|
import { isAutocompleteDebugEnabled } from './debug-mode';
|
|
27
25
|
import { rankCandidates, STAGE1_WEIGHT, STAGE2_WEIGHT, MIN_STAGE1_SCORE } from './scoring-pipeline';
|
|
28
26
|
import { getStoredContextVector, getStoredLmLogits } from './slow-lane-client';
|
|
@@ -34,7 +32,7 @@ const PUNCTUATION_BOUNDARY_REGEX = /^[.,;:!?()\[\]{}"'`]+|[.,;:!?()\[\]{}"'`]+$/
|
|
|
34
32
|
const MIN_PREFIX_LENGTH = 3;
|
|
35
33
|
const MAX_CANDIDATES = 200;
|
|
36
34
|
const CONTEXT_WORDS = 10;
|
|
37
|
-
const MIN_SCORE_THRESHOLD = 0.
|
|
35
|
+
const MIN_SCORE_THRESHOLD = 0.35;
|
|
38
36
|
const L3_BASELINE_FREQ = 0.001;
|
|
39
37
|
|
|
40
38
|
// ─── Types ───────────────────────────────────────────────────────────────────
|
|
@@ -153,7 +151,6 @@ const wordTrie = new WeightedWordTrie();
|
|
|
153
151
|
// L3 Trie (General English Fallback)
|
|
154
152
|
const l3Trie = new WeightedWordTrie();
|
|
155
153
|
|
|
156
|
-
// --- Initialization Function ---
|
|
157
154
|
/**
|
|
158
155
|
* Loads the General English vocabulary.
|
|
159
156
|
* expects a simple array of strings: ["about", "above", "actually", ...]
|
|
@@ -249,14 +246,11 @@ const tokenize = text => {
|
|
|
249
246
|
return tokens;
|
|
250
247
|
};
|
|
251
248
|
const extractPreviousWord = text => {
|
|
252
|
-
//
|
|
249
|
+
// Only consider the current sentence/line the user is typing in.
|
|
253
250
|
// eslint-disable-next-line require-unicode-regexp
|
|
254
251
|
const sentences = text.split(/[\n.?!]+/);
|
|
255
|
-
|
|
256
|
-
// 2. Only look at the current sentence/line the user is typing in
|
|
257
252
|
const currentSentence = sentences[sentences.length - 1];
|
|
258
253
|
|
|
259
|
-
// 3. Extract the previous word as normal
|
|
260
254
|
// eslint-disable-next-line require-unicode-regexp
|
|
261
255
|
const words = currentSentence.trimEnd().split(/\s+/);
|
|
262
256
|
return words.length >= 2 ? words[words.length - 2] : '';
|
|
@@ -310,7 +304,6 @@ export const incrementSessionFreq = word => {
|
|
|
310
304
|
* Pass `undefined` (or omit the argument) to skip priming — useful when the
|
|
311
305
|
* calling context does not yet have a page value available.
|
|
312
306
|
*/
|
|
313
|
-
// NOTE: We ingest full page context here
|
|
314
307
|
export const ingestDocumentPage = pageContent => {
|
|
315
308
|
if (!pageContent) {
|
|
316
309
|
return;
|
|
@@ -334,7 +327,11 @@ export const ingestDocumentPage = pageContent => {
|
|
|
334
327
|
};
|
|
335
328
|
export const predict = textBefore => {
|
|
336
329
|
if (!isInitialized) {
|
|
337
|
-
|
|
330
|
+
// Vocabulary JSON is code-split and loads asynchronously. Kick off the load
|
|
331
|
+
// and skip this keystroke; the plugin also primes it on focus, so the tries
|
|
332
|
+
// are usually ready before the user types.
|
|
333
|
+
void loadDefaultVocabulary().catch(() => {});
|
|
334
|
+
return null;
|
|
338
335
|
}
|
|
339
336
|
const t0 = performance.now();
|
|
340
337
|
|
|
@@ -386,19 +383,15 @@ export const predict = textBefore => {
|
|
|
386
383
|
if (currentWord.length < MIN_PREFIX_LENGTH) {
|
|
387
384
|
return null;
|
|
388
385
|
}
|
|
389
|
-
|
|
390
|
-
// 1. Primary Query: Ask the L2 Domain Trie
|
|
391
386
|
const candidates = wordTrie.getCandidates(currentWord, MAX_CANDIDATES);
|
|
392
387
|
|
|
393
|
-
//
|
|
388
|
+
// Gap-fill from the L3 general-English trie, requesting a full buffer so
|
|
389
|
+
// enough survive de-duplication against the L2 results.
|
|
394
390
|
if (candidates.length < MAX_CANDIDATES) {
|
|
395
|
-
// Ask L3 for MAX_CANDIDATES to guarantee we have enough buffer
|
|
396
|
-
// to survive the deduplication process.
|
|
397
391
|
const l3Candidates = l3Trie.getCandidates(currentWord, MAX_CANDIDATES);
|
|
398
392
|
const existingWords = new Set(candidates.map(c => c.word));
|
|
399
393
|
for (const l3c of l3Candidates) {
|
|
400
|
-
if (candidates.length >= MAX_CANDIDATES) break;
|
|
401
|
-
|
|
394
|
+
if (candidates.length >= MAX_CANDIDATES) break;
|
|
402
395
|
if (!existingWords.has(l3c.word)) {
|
|
403
396
|
candidates.push(l3c);
|
|
404
397
|
}
|
|
@@ -558,6 +551,41 @@ export const predict = textBefore => {
|
|
|
558
551
|
|
|
559
552
|
// ─── Data Loading ────────────────────────────────────────────────────────────
|
|
560
553
|
|
|
554
|
+
/**
|
|
555
|
+
* Unwrap a dynamically imported JSON module to its parsed value, handling both
|
|
556
|
+
* interop modes AFM's bundler chain emits: a `.default`-wrapped namespace
|
|
557
|
+
* (classic webpack) and a named-exports namespace (webpack 5 / atlaspack JSON
|
|
558
|
+
* modules, where `default` can be a misleading scalar). Named exports are
|
|
559
|
+
* preferred when present. The caller declares the JSON `shape` because a dense
|
|
560
|
+
* array and a sparse numeric-keyed object are emitted identically as named
|
|
561
|
+
* exports. Kept in lock-step with the matching helper in local-slow-lane-client.ts.
|
|
562
|
+
*/
|
|
563
|
+
const unwrapJsonModule = (mod, shape) => {
|
|
564
|
+
if (mod == null || typeof mod !== 'object') {
|
|
565
|
+
return null;
|
|
566
|
+
}
|
|
567
|
+
const namespace = mod;
|
|
568
|
+
const ownKeys = Object.keys(namespace).filter(k => k !== 'default' && k !== '__esModule');
|
|
569
|
+
if (ownKeys.length > 0) {
|
|
570
|
+
if (shape === 'array') {
|
|
571
|
+
const len = ownKeys.length;
|
|
572
|
+
const arr = new Array(len);
|
|
573
|
+
for (let i = 0; i < len; i++) {
|
|
574
|
+
arr[i] = namespace[String(i)];
|
|
575
|
+
}
|
|
576
|
+
return arr;
|
|
577
|
+
}
|
|
578
|
+
const obj = {};
|
|
579
|
+
for (const k of ownKeys) {
|
|
580
|
+
obj[k] = namespace[k];
|
|
581
|
+
}
|
|
582
|
+
return obj;
|
|
583
|
+
}
|
|
584
|
+
if ('default' in namespace && namespace.default != null) {
|
|
585
|
+
return namespace.default;
|
|
586
|
+
}
|
|
587
|
+
return null;
|
|
588
|
+
};
|
|
561
589
|
export const loadVectorsAsync = async options => {
|
|
562
590
|
if (vectorStore || vectorsLoadStarted) {
|
|
563
591
|
return;
|
|
@@ -582,6 +610,7 @@ export const loadVectorsAsync = async options => {
|
|
|
582
610
|
return;
|
|
583
611
|
}
|
|
584
612
|
try {
|
|
613
|
+
var _wordIndexOuter$index;
|
|
585
614
|
const res = await fetch(url);
|
|
586
615
|
if (!res.ok) {
|
|
587
616
|
vectorsLoadStarted = false;
|
|
@@ -595,8 +624,18 @@ export const loadVectorsAsync = async options => {
|
|
|
595
624
|
}
|
|
596
625
|
const buffer = await res.arrayBuffer();
|
|
597
626
|
const float32 = new Float32Array(buffer);
|
|
598
|
-
|
|
627
|
+
|
|
628
|
+
// word_index_10k.json is wrapped as `{ "index": {…} }` so no real entry
|
|
629
|
+
// (e.g. the word "default") can shadow the synthetic ESM `default` export
|
|
630
|
+
// the bundler creates for dynamically-imported JSON.
|
|
631
|
+
const wordIndexModule = await import( /* webpackChunkName: "@atlaskit-internal_editor-plugin-autocomplete-word-index-10k" */'./data/word_index_10k.json');
|
|
632
|
+
const wordIndexOuter = unwrapJsonModule(wordIndexModule, 'object');
|
|
633
|
+
const wordIndex = (_wordIndexOuter$index = wordIndexOuter === null || wordIndexOuter === void 0 ? void 0 : wordIndexOuter.index) !== null && _wordIndexOuter$index !== void 0 ? _wordIndexOuter$index : {};
|
|
599
634
|
const nWords = Object.keys(wordIndex).length;
|
|
635
|
+
if (nWords === 0) {
|
|
636
|
+
// eslint-disable-next-line no-console
|
|
637
|
+
console.warn('[text-predictor] word_index_10k.json missing its `index` wrapper — wordIndex is empty, semantic scoring will be a no-op.');
|
|
638
|
+
}
|
|
600
639
|
const dim = float32.length / nWords;
|
|
601
640
|
vectorStore = {
|
|
602
641
|
float32,
|
|
@@ -628,35 +667,51 @@ export const loadVectorsAsync = async options => {
|
|
|
628
667
|
export const initVectors = store => {
|
|
629
668
|
vectorStore = store;
|
|
630
669
|
};
|
|
670
|
+
let vocabularyLoadPromise;
|
|
631
671
|
export const loadDefaultVocabulary = () => {
|
|
632
672
|
if (isInitialized) {
|
|
633
|
-
return;
|
|
634
|
-
}
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
673
|
+
return Promise.resolve();
|
|
674
|
+
}
|
|
675
|
+
if (vocabularyLoadPromise) {
|
|
676
|
+
return vocabularyLoadPromise;
|
|
677
|
+
}
|
|
678
|
+
vocabularyLoadPromise = (async () => {
|
|
679
|
+
startExp(EXPERIENCE_NAME.LOAD_VOCABULARY, 'singleton');
|
|
680
|
+
try {
|
|
681
|
+
// The L2 vocabulary and L3 word list are code-split into their own async
|
|
682
|
+
// chunks so they stay out of the editor's main bundle.
|
|
683
|
+
const [vocabularyModule, l3VocabularyModule] = await Promise.all([import( /* webpackChunkName: "@atlaskit-internal_editor-plugin-autocomplete-vocabulary-10k" */'./data/vocabulary_10k.json'), import( /* webpackChunkName: "@atlaskit-internal_editor-plugin-autocomplete-l3-vocabulary" */'./data/l3_vocabulary.json')]);
|
|
684
|
+
const vocabularyData = unwrapJsonModule(vocabularyModule, 'object');
|
|
685
|
+
const l3VocabularyData = unwrapJsonModule(l3VocabularyModule, 'array');
|
|
686
|
+
if ((vocabularyData === null || vocabularyData === void 0 ? void 0 : vocabularyData.words) == null || !Array.isArray(l3VocabularyData)) {
|
|
687
|
+
throw new Error('[text-predictor] vocabulary JSON modules could not be unwrapped');
|
|
688
|
+
}
|
|
689
|
+
const terms = Object.entries(vocabularyData.words).map(([word, stats]) => ({
|
|
690
|
+
word,
|
|
691
|
+
freq: stats.freq,
|
|
692
|
+
docFreq: stats.doc_freq,
|
|
693
|
+
authorFreq: stats.author_freq
|
|
694
|
+
}));
|
|
695
|
+
|
|
696
|
+
// Load L3 before L2: initVocabulary flips `isInitialized = true`, so it
|
|
697
|
+
// must run last — otherwise a throw in initL3Vocabulary would strand
|
|
698
|
+
// `isInitialized` true and the retry path could never reload L3.
|
|
699
|
+
initL3Vocabulary(l3VocabularyData);
|
|
700
|
+
initVocabulary({
|
|
701
|
+
terms
|
|
702
|
+
});
|
|
703
|
+
succeedExp(EXPERIENCE_NAME.LOAD_VOCABULARY, 'singleton', {
|
|
704
|
+
l2WordCount: terms.length,
|
|
705
|
+
l3WordCount: l3VocabularyData.length
|
|
706
|
+
});
|
|
707
|
+
} catch (e) {
|
|
708
|
+
failExp(EXPERIENCE_NAME.LOAD_VOCABULARY, 'singleton', {
|
|
709
|
+
errorType: 'parse_error'
|
|
710
|
+
});
|
|
711
|
+
// Allow a later call to retry the load rather than caching the failure.
|
|
712
|
+
vocabularyLoadPromise = undefined;
|
|
713
|
+
throw e;
|
|
714
|
+
}
|
|
715
|
+
})();
|
|
716
|
+
return vocabularyLoadPromise;
|
|
662
717
|
};
|
|
@@ -31,8 +31,6 @@ var createInitialState = function createInitialState() {
|
|
|
31
31
|
var getTextBeforeCursor = function getTextBeforeCursor(state) {
|
|
32
32
|
var $from = state.selection.$from;
|
|
33
33
|
var maxChars = 200;
|
|
34
|
-
|
|
35
|
-
// 1. Get the perfectly flattened text of the current block up to the cursor
|
|
36
34
|
var blockNode = $from.parent;
|
|
37
35
|
var offsetInBlock = $from.parentOffset;
|
|
38
36
|
var blockText = blockNode.textContent.slice(0, offsetInBlock);
|
|
@@ -41,7 +39,7 @@ var getTextBeforeCursor = function getTextBeforeCursor(state) {
|
|
|
41
39
|
}
|
|
42
40
|
var fullText = blockText;
|
|
43
41
|
|
|
44
|
-
//
|
|
42
|
+
// Walk backwards through previous blocks until we have enough context.
|
|
45
43
|
var depth = $from.depth - 1;
|
|
46
44
|
while (fullText.length < maxChars && depth >= 0) {
|
|
47
45
|
var parentNode = $from.node(depth);
|
|
@@ -242,8 +240,6 @@ export var createAutocompletePlugin = function createAutocompletePlugin(options,
|
|
|
242
240
|
try {
|
|
243
241
|
var state = view.state;
|
|
244
242
|
var selection = state.selection;
|
|
245
|
-
|
|
246
|
-
// Only predict for cursor selections (not range selections)
|
|
247
243
|
if (!selection.empty) {
|
|
248
244
|
return;
|
|
249
245
|
}
|
|
@@ -255,13 +251,9 @@ export var createAutocompletePlugin = function createAutocompletePlugin(options,
|
|
|
255
251
|
return;
|
|
256
252
|
}
|
|
257
253
|
dismissedContext = null;
|
|
258
|
-
|
|
259
|
-
// Don't predict if there's not enough context
|
|
260
254
|
if (textBefore.trim().length < 3) {
|
|
261
255
|
return;
|
|
262
256
|
}
|
|
263
|
-
|
|
264
|
-
// Tier 1 prediction is synchronous -- no async needed
|
|
265
257
|
var prediction = predict(textBefore);
|
|
266
258
|
if (prediction && prediction.length > 0) {
|
|
267
259
|
var typedLength = getTypedLengthForPrediction(textBefore);
|
|
@@ -323,8 +315,7 @@ export var createAutocompletePlugin = function createAutocompletePlugin(options,
|
|
|
323
315
|
return _objectSpread(_objectSpread({}, pluginState), meta);
|
|
324
316
|
}
|
|
325
317
|
|
|
326
|
-
//
|
|
327
|
-
// (new prediction will be scheduled from view.update)
|
|
318
|
+
// A new prediction is scheduled from view.update.
|
|
328
319
|
if (tr.docChanged) {
|
|
329
320
|
return _objectSpread(_objectSpread({}, pluginState), {}, {
|
|
330
321
|
ghostText: '',
|
|
@@ -332,8 +323,6 @@ export var createAutocompletePlugin = function createAutocompletePlugin(options,
|
|
|
332
323
|
decorationSet: DecorationSet.empty
|
|
333
324
|
});
|
|
334
325
|
}
|
|
335
|
-
|
|
336
|
-
// If selection changed without doc change, clear ghost text
|
|
337
326
|
if (tr.selectionSet && pluginState.ghostText) {
|
|
338
327
|
return _objectSpread(_objectSpread({}, pluginState), {}, {
|
|
339
328
|
ghostText: '',
|
|
@@ -394,13 +383,11 @@ export var createAutocompletePlugin = function createAutocompletePlugin(options,
|
|
|
394
383
|
return false;
|
|
395
384
|
},
|
|
396
385
|
focus: function focus() {
|
|
397
|
-
|
|
398
|
-
loadDefaultVocabulary();
|
|
399
|
-
} catch (error) {
|
|
386
|
+
loadDefaultVocabulary().catch(function (error) {
|
|
400
387
|
logException(error, {
|
|
401
388
|
location: 'editor-plugin-autocomplete/loadDefaultVocabulary'
|
|
402
389
|
});
|
|
403
|
-
}
|
|
390
|
+
});
|
|
404
391
|
loadVectorsAsync({
|
|
405
392
|
getBinaryUrl: options === null || options === void 0 ? void 0 : options.getVectorsBinaryUrl
|
|
406
393
|
}).catch(function (error) {
|
|
@@ -442,12 +429,9 @@ export var createAutocompletePlugin = function createAutocompletePlugin(options,
|
|
|
442
429
|
if (justAccepted) {
|
|
443
430
|
justAccepted = false;
|
|
444
431
|
|
|
445
|
-
//
|
|
446
|
-
//
|
|
447
|
-
// block and abort until the user actually types a new character!
|
|
432
|
+
// Snapshot the post-acceptance text so follow-up transactions hit
|
|
433
|
+
// the dismissedContext guard and abort until the user types again.
|
|
448
434
|
dismissedContext = getTextBeforeCursor(view.state);
|
|
449
|
-
|
|
450
|
-
// Also clear any pending debounce timers from before the acceptance
|
|
451
435
|
if (debounceTimer) {
|
|
452
436
|
clearTimeout(debounceTimer);
|
|
453
437
|
}
|
|
@@ -161,20 +161,21 @@ function applyGrammarFilter(candidates, previousWord) {
|
|
|
161
161
|
dropped.push(entry.candidate.word);
|
|
162
162
|
}
|
|
163
163
|
}
|
|
164
|
+
|
|
165
|
+
// Grammar is authoritative.
|
|
164
166
|
} catch (err) {
|
|
165
167
|
_iterator2.e(err);
|
|
166
168
|
} finally {
|
|
167
169
|
_iterator2.f();
|
|
168
170
|
}
|
|
169
|
-
var finalFiltered = filtered.length > 0 ? filtered : candidates;
|
|
170
171
|
return {
|
|
171
|
-
filtered:
|
|
172
|
+
filtered: filtered,
|
|
172
173
|
grammarMeta: {
|
|
173
174
|
prevWord: lowerPrev,
|
|
174
175
|
prevTags: prevTags,
|
|
175
176
|
before: candidates.length,
|
|
176
|
-
after:
|
|
177
|
-
dropped:
|
|
177
|
+
after: filtered.length,
|
|
178
|
+
dropped: dropped
|
|
178
179
|
}
|
|
179
180
|
};
|
|
180
181
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import _asyncToGenerator from "@babel/runtime/helpers/asyncToGenerator";
|
|
2
|
+
import _typeof from "@babel/runtime/helpers/typeof";
|
|
2
3
|
import _slicedToArray from "@babel/runtime/helpers/slicedToArray";
|
|
3
4
|
import _createClass from "@babel/runtime/helpers/createClass";
|
|
4
5
|
import _classCallCheck from "@babel/runtime/helpers/classCallCheck";
|
|
@@ -26,11 +27,9 @@ function _arrayLikeToArray(r, a) { (null == a || a > r.length) && (a = r.length)
|
|
|
26
27
|
|
|
27
28
|
import { EXPERIENCE_NAME, failExp, startExp, succeedExp } from '../analytics/ufo';
|
|
28
29
|
|
|
29
|
-
//
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
import wordIndexData from './data/word_index_10k.json';
|
|
33
|
-
// import { rankCandidates, isGrammarAllowed } from './scoring-pipeline';
|
|
30
|
+
// The vocabulary, L3 and word-index JSON payloads are dynamically imported in
|
|
31
|
+
// loadDefaultVocabulary / loadVectorsAsync so their (large) contents stay out of
|
|
32
|
+
// the editor's main chunk and only load when autocomplete is initialised.
|
|
34
33
|
import { isAutocompleteDebugEnabled } from './debug-mode';
|
|
35
34
|
import { rankCandidates, STAGE1_WEIGHT, STAGE2_WEIGHT, MIN_STAGE1_SCORE } from './scoring-pipeline';
|
|
36
35
|
import { getStoredContextVector, getStoredLmLogits } from './slow-lane-client';
|
|
@@ -42,7 +41,7 @@ var PUNCTUATION_BOUNDARY_REGEX = /^[.,;:!?()\[\]{}"'`]+|[.,;:!?()\[\]{}"'`]+$/g;
|
|
|
42
41
|
var MIN_PREFIX_LENGTH = 3;
|
|
43
42
|
var MAX_CANDIDATES = 200;
|
|
44
43
|
var CONTEXT_WORDS = 10;
|
|
45
|
-
var MIN_SCORE_THRESHOLD = 0.
|
|
44
|
+
var MIN_SCORE_THRESHOLD = 0.35;
|
|
46
45
|
var L3_BASELINE_FREQ = 0.001;
|
|
47
46
|
|
|
48
47
|
// ─── Types ───────────────────────────────────────────────────────────────────
|
|
@@ -206,7 +205,6 @@ var wordTrie = new WeightedWordTrie();
|
|
|
206
205
|
// L3 Trie (General English Fallback)
|
|
207
206
|
var l3Trie = new WeightedWordTrie();
|
|
208
207
|
|
|
209
|
-
// --- Initialization Function ---
|
|
210
208
|
/**
|
|
211
209
|
* Loads the General English vocabulary.
|
|
212
210
|
* expects a simple array of strings: ["about", "above", "actually", ...]
|
|
@@ -330,14 +328,11 @@ var tokenize = function tokenize(text) {
|
|
|
330
328
|
return tokens;
|
|
331
329
|
};
|
|
332
330
|
var extractPreviousWord = function extractPreviousWord(text) {
|
|
333
|
-
//
|
|
331
|
+
// Only consider the current sentence/line the user is typing in.
|
|
334
332
|
// eslint-disable-next-line require-unicode-regexp
|
|
335
333
|
var sentences = text.split(/[\n.?!]+/);
|
|
336
|
-
|
|
337
|
-
// 2. Only look at the current sentence/line the user is typing in
|
|
338
334
|
var currentSentence = sentences[sentences.length - 1];
|
|
339
335
|
|
|
340
|
-
// 3. Extract the previous word as normal
|
|
341
336
|
// eslint-disable-next-line require-unicode-regexp
|
|
342
337
|
var words = currentSentence.trimEnd().split(/\s+/);
|
|
343
338
|
return words.length >= 2 ? words[words.length - 2] : '';
|
|
@@ -400,7 +395,6 @@ export var incrementSessionFreq = function incrementSessionFreq(word) {
|
|
|
400
395
|
* Pass `undefined` (or omit the argument) to skip priming — useful when the
|
|
401
396
|
* calling context does not yet have a page value available.
|
|
402
397
|
*/
|
|
403
|
-
// NOTE: We ingest full page context here
|
|
404
398
|
export var ingestDocumentPage = function ingestDocumentPage(pageContent) {
|
|
405
399
|
if (!pageContent) {
|
|
406
400
|
return;
|
|
@@ -433,7 +427,11 @@ export var ingestDocumentPage = function ingestDocumentPage(pageContent) {
|
|
|
433
427
|
};
|
|
434
428
|
export var predict = function predict(textBefore) {
|
|
435
429
|
if (!isInitialized) {
|
|
436
|
-
|
|
430
|
+
// Vocabulary JSON is code-split and loads asynchronously. Kick off the load
|
|
431
|
+
// and skip this keystroke; the plugin also primes it on focus, so the tries
|
|
432
|
+
// are usually ready before the user types.
|
|
433
|
+
void loadDefaultVocabulary().catch(function () {});
|
|
434
|
+
return null;
|
|
437
435
|
}
|
|
438
436
|
var t0 = performance.now();
|
|
439
437
|
|
|
@@ -485,14 +483,11 @@ export var predict = function predict(textBefore) {
|
|
|
485
483
|
if (currentWord.length < MIN_PREFIX_LENGTH) {
|
|
486
484
|
return null;
|
|
487
485
|
}
|
|
488
|
-
|
|
489
|
-
// 1. Primary Query: Ask the L2 Domain Trie
|
|
490
486
|
var candidates = wordTrie.getCandidates(currentWord, MAX_CANDIDATES);
|
|
491
487
|
|
|
492
|
-
//
|
|
488
|
+
// Gap-fill from the L3 general-English trie, requesting a full buffer so
|
|
489
|
+
// enough survive de-duplication against the L2 results.
|
|
493
490
|
if (candidates.length < MAX_CANDIDATES) {
|
|
494
|
-
// Ask L3 for MAX_CANDIDATES to guarantee we have enough buffer
|
|
495
|
-
// to survive the deduplication process.
|
|
496
491
|
var l3Candidates = l3Trie.getCandidates(currentWord, MAX_CANDIDATES);
|
|
497
492
|
var existingWords = new Set(candidates.map(function (c) {
|
|
498
493
|
return c.word;
|
|
@@ -502,8 +497,7 @@ export var predict = function predict(textBefore) {
|
|
|
502
497
|
try {
|
|
503
498
|
for (_iterator0.s(); !(_step0 = _iterator0.n()).done;) {
|
|
504
499
|
var l3c = _step0.value;
|
|
505
|
-
if (candidates.length >= MAX_CANDIDATES) break;
|
|
506
|
-
|
|
500
|
+
if (candidates.length >= MAX_CANDIDATES) break;
|
|
507
501
|
if (!existingWords.has(l3c.word)) {
|
|
508
502
|
candidates.push(l3c);
|
|
509
503
|
}
|
|
@@ -687,9 +681,55 @@ export var predict = function predict(textBefore) {
|
|
|
687
681
|
|
|
688
682
|
// ─── Data Loading ────────────────────────────────────────────────────────────
|
|
689
683
|
|
|
684
|
+
/**
|
|
685
|
+
* Unwrap a dynamically imported JSON module to its parsed value, handling both
|
|
686
|
+
* interop modes AFM's bundler chain emits: a `.default`-wrapped namespace
|
|
687
|
+
* (classic webpack) and a named-exports namespace (webpack 5 / atlaspack JSON
|
|
688
|
+
* modules, where `default` can be a misleading scalar). Named exports are
|
|
689
|
+
* preferred when present. The caller declares the JSON `shape` because a dense
|
|
690
|
+
* array and a sparse numeric-keyed object are emitted identically as named
|
|
691
|
+
* exports. Kept in lock-step with the matching helper in local-slow-lane-client.ts.
|
|
692
|
+
*/
|
|
693
|
+
var unwrapJsonModule = function unwrapJsonModule(mod, shape) {
|
|
694
|
+
if (mod == null || _typeof(mod) !== 'object') {
|
|
695
|
+
return null;
|
|
696
|
+
}
|
|
697
|
+
var namespace = mod;
|
|
698
|
+
var ownKeys = Object.keys(namespace).filter(function (k) {
|
|
699
|
+
return k !== 'default' && k !== '__esModule';
|
|
700
|
+
});
|
|
701
|
+
if (ownKeys.length > 0) {
|
|
702
|
+
if (shape === 'array') {
|
|
703
|
+
var len = ownKeys.length;
|
|
704
|
+
var arr = new Array(len);
|
|
705
|
+
for (var i = 0; i < len; i++) {
|
|
706
|
+
arr[i] = namespace[String(i)];
|
|
707
|
+
}
|
|
708
|
+
return arr;
|
|
709
|
+
}
|
|
710
|
+
var obj = {};
|
|
711
|
+
var _iterator1 = _createForOfIteratorHelper(ownKeys),
|
|
712
|
+
_step1;
|
|
713
|
+
try {
|
|
714
|
+
for (_iterator1.s(); !(_step1 = _iterator1.n()).done;) {
|
|
715
|
+
var k = _step1.value;
|
|
716
|
+
obj[k] = namespace[k];
|
|
717
|
+
}
|
|
718
|
+
} catch (err) {
|
|
719
|
+
_iterator1.e(err);
|
|
720
|
+
} finally {
|
|
721
|
+
_iterator1.f();
|
|
722
|
+
}
|
|
723
|
+
return obj;
|
|
724
|
+
}
|
|
725
|
+
if ('default' in namespace && namespace.default != null) {
|
|
726
|
+
return namespace.default;
|
|
727
|
+
}
|
|
728
|
+
return null;
|
|
729
|
+
};
|
|
690
730
|
export var loadVectorsAsync = /*#__PURE__*/function () {
|
|
691
731
|
var _ref6 = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime.mark(function _callee(options) {
|
|
692
|
-
var url, res, buffer, float32, wordIndex, nWords, dim, _t, _t2;
|
|
732
|
+
var url, _wordIndexOuter$index, res, buffer, float32, wordIndexModule, wordIndexOuter, wordIndex, nWords, dim, _t, _t2;
|
|
693
733
|
return _regeneratorRuntime.wrap(function (_context) {
|
|
694
734
|
while (1) switch (_context.prev = _context.next) {
|
|
695
735
|
case 0:
|
|
@@ -749,9 +789,20 @@ export var loadVectorsAsync = /*#__PURE__*/function () {
|
|
|
749
789
|
return res.arrayBuffer();
|
|
750
790
|
case 9:
|
|
751
791
|
buffer = _context.sent;
|
|
752
|
-
float32 = new Float32Array(buffer);
|
|
753
|
-
|
|
792
|
+
float32 = new Float32Array(buffer); // word_index_10k.json is wrapped as `{ "index": {…} }` so no real entry
|
|
793
|
+
// (e.g. the word "default") can shadow the synthetic ESM `default` export
|
|
794
|
+
// the bundler creates for dynamically-imported JSON.
|
|
795
|
+
_context.next = 10;
|
|
796
|
+
return import( /* webpackChunkName: "@atlaskit-internal_editor-plugin-autocomplete-word-index-10k" */'./data/word_index_10k.json');
|
|
797
|
+
case 10:
|
|
798
|
+
wordIndexModule = _context.sent;
|
|
799
|
+
wordIndexOuter = unwrapJsonModule(wordIndexModule, 'object');
|
|
800
|
+
wordIndex = (_wordIndexOuter$index = wordIndexOuter === null || wordIndexOuter === void 0 ? void 0 : wordIndexOuter.index) !== null && _wordIndexOuter$index !== void 0 ? _wordIndexOuter$index : {};
|
|
754
801
|
nWords = Object.keys(wordIndex).length;
|
|
802
|
+
if (nWords === 0) {
|
|
803
|
+
// eslint-disable-next-line no-console
|
|
804
|
+
console.warn('[text-predictor] word_index_10k.json missing its `index` wrapper — wordIndex is empty, semantic scoring will be a no-op.');
|
|
805
|
+
}
|
|
755
806
|
dim = float32.length / nWords;
|
|
756
807
|
vectorStore = {
|
|
757
808
|
float32: float32,
|
|
@@ -771,10 +822,10 @@ export var loadVectorsAsync = /*#__PURE__*/function () {
|
|
|
771
822
|
sizeBytes: float32.byteLength
|
|
772
823
|
});
|
|
773
824
|
}
|
|
774
|
-
_context.next =
|
|
825
|
+
_context.next = 12;
|
|
775
826
|
break;
|
|
776
|
-
case
|
|
777
|
-
_context.prev =
|
|
827
|
+
case 11:
|
|
828
|
+
_context.prev = 11;
|
|
778
829
|
_t2 = _context["catch"](6);
|
|
779
830
|
vectorsLoadStarted = false;
|
|
780
831
|
failExp(EXPERIENCE_NAME.LOAD_VECTORS, 'singleton', {
|
|
@@ -782,11 +833,11 @@ export var loadVectorsAsync = /*#__PURE__*/function () {
|
|
|
782
833
|
});
|
|
783
834
|
// eslint-disable-next-line no-console
|
|
784
835
|
console.warn('[text-predictor] Failed to load vectors:', _t2);
|
|
785
|
-
case
|
|
836
|
+
case 12:
|
|
786
837
|
case "end":
|
|
787
838
|
return _context.stop();
|
|
788
839
|
}
|
|
789
|
-
}, _callee, null, [[3, 5], [6,
|
|
840
|
+
}, _callee, null, [[3, 5], [6, 11]]);
|
|
790
841
|
}));
|
|
791
842
|
return function loadVectorsAsync(_x) {
|
|
792
843
|
return _ref6.apply(this, arguments);
|
|
@@ -795,40 +846,73 @@ export var loadVectorsAsync = /*#__PURE__*/function () {
|
|
|
795
846
|
export var initVectors = function initVectors(store) {
|
|
796
847
|
vectorStore = store;
|
|
797
848
|
};
|
|
849
|
+
var vocabularyLoadPromise;
|
|
798
850
|
export var loadDefaultVocabulary = function loadDefaultVocabulary() {
|
|
799
851
|
if (isInitialized) {
|
|
800
|
-
return;
|
|
852
|
+
return Promise.resolve();
|
|
801
853
|
}
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
// 1. Load the Atlassian Domain (L2)
|
|
805
|
-
var data = vocabularyData;
|
|
806
|
-
var terms = Object.entries(data.words).map(function (_ref7) {
|
|
807
|
-
var _ref8 = _slicedToArray(_ref7, 2),
|
|
808
|
-
word = _ref8[0],
|
|
809
|
-
stats = _ref8[1];
|
|
810
|
-
return {
|
|
811
|
-
word: word,
|
|
812
|
-
freq: stats.freq,
|
|
813
|
-
docFreq: stats.doc_freq,
|
|
814
|
-
authorFreq: stats.author_freq
|
|
815
|
-
};
|
|
816
|
-
});
|
|
817
|
-
initVocabulary({
|
|
818
|
-
terms: terms
|
|
819
|
-
});
|
|
820
|
-
|
|
821
|
-
// 2. Load General English (L3)
|
|
822
|
-
var l3Words = l3VocabularyData;
|
|
823
|
-
initL3Vocabulary(l3Words);
|
|
824
|
-
succeedExp(EXPERIENCE_NAME.LOAD_VOCABULARY, 'singleton', {
|
|
825
|
-
l2WordCount: terms.length,
|
|
826
|
-
l3WordCount: l3Words.length
|
|
827
|
-
});
|
|
828
|
-
} catch (e) {
|
|
829
|
-
failExp(EXPERIENCE_NAME.LOAD_VOCABULARY, 'singleton', {
|
|
830
|
-
errorType: 'parse_error'
|
|
831
|
-
});
|
|
832
|
-
throw e;
|
|
854
|
+
if (vocabularyLoadPromise) {
|
|
855
|
+
return vocabularyLoadPromise;
|
|
833
856
|
}
|
|
857
|
+
vocabularyLoadPromise = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime.mark(function _callee2() {
|
|
858
|
+
var _yield$Promise$all, _yield$Promise$all2, vocabularyModule, l3VocabularyModule, vocabularyData, l3VocabularyData, terms, _t3;
|
|
859
|
+
return _regeneratorRuntime.wrap(function (_context2) {
|
|
860
|
+
while (1) switch (_context2.prev = _context2.next) {
|
|
861
|
+
case 0:
|
|
862
|
+
startExp(EXPERIENCE_NAME.LOAD_VOCABULARY, 'singleton');
|
|
863
|
+
_context2.prev = 1;
|
|
864
|
+
_context2.next = 2;
|
|
865
|
+
return Promise.all([import( /* webpackChunkName: "@atlaskit-internal_editor-plugin-autocomplete-vocabulary-10k" */'./data/vocabulary_10k.json'), import( /* webpackChunkName: "@atlaskit-internal_editor-plugin-autocomplete-l3-vocabulary" */'./data/l3_vocabulary.json')]);
|
|
866
|
+
case 2:
|
|
867
|
+
_yield$Promise$all = _context2.sent;
|
|
868
|
+
_yield$Promise$all2 = _slicedToArray(_yield$Promise$all, 2);
|
|
869
|
+
vocabularyModule = _yield$Promise$all2[0];
|
|
870
|
+
l3VocabularyModule = _yield$Promise$all2[1];
|
|
871
|
+
vocabularyData = unwrapJsonModule(vocabularyModule, 'object');
|
|
872
|
+
l3VocabularyData = unwrapJsonModule(l3VocabularyModule, 'array');
|
|
873
|
+
if (!((vocabularyData === null || vocabularyData === void 0 ? void 0 : vocabularyData.words) == null || !Array.isArray(l3VocabularyData))) {
|
|
874
|
+
_context2.next = 3;
|
|
875
|
+
break;
|
|
876
|
+
}
|
|
877
|
+
throw new Error('[text-predictor] vocabulary JSON modules could not be unwrapped');
|
|
878
|
+
case 3:
|
|
879
|
+
terms = Object.entries(vocabularyData.words).map(function (_ref8) {
|
|
880
|
+
var _ref9 = _slicedToArray(_ref8, 2),
|
|
881
|
+
word = _ref9[0],
|
|
882
|
+
stats = _ref9[1];
|
|
883
|
+
return {
|
|
884
|
+
word: word,
|
|
885
|
+
freq: stats.freq,
|
|
886
|
+
docFreq: stats.doc_freq,
|
|
887
|
+
authorFreq: stats.author_freq
|
|
888
|
+
};
|
|
889
|
+
}); // Load L3 before L2: initVocabulary flips `isInitialized = true`, so it
|
|
890
|
+
// must run last — otherwise a throw in initL3Vocabulary would strand
|
|
891
|
+
// `isInitialized` true and the retry path could never reload L3.
|
|
892
|
+
initL3Vocabulary(l3VocabularyData);
|
|
893
|
+
initVocabulary({
|
|
894
|
+
terms: terms
|
|
895
|
+
});
|
|
896
|
+
succeedExp(EXPERIENCE_NAME.LOAD_VOCABULARY, 'singleton', {
|
|
897
|
+
l2WordCount: terms.length,
|
|
898
|
+
l3WordCount: l3VocabularyData.length
|
|
899
|
+
});
|
|
900
|
+
_context2.next = 5;
|
|
901
|
+
break;
|
|
902
|
+
case 4:
|
|
903
|
+
_context2.prev = 4;
|
|
904
|
+
_t3 = _context2["catch"](1);
|
|
905
|
+
failExp(EXPERIENCE_NAME.LOAD_VOCABULARY, 'singleton', {
|
|
906
|
+
errorType: 'parse_error'
|
|
907
|
+
});
|
|
908
|
+
// Allow a later call to retry the load rather than caching the failure.
|
|
909
|
+
vocabularyLoadPromise = undefined;
|
|
910
|
+
throw _t3;
|
|
911
|
+
case 5:
|
|
912
|
+
case "end":
|
|
913
|
+
return _context2.stop();
|
|
914
|
+
}
|
|
915
|
+
}, _callee2, null, [[1, 4]]);
|
|
916
|
+
}))();
|
|
917
|
+
return vocabularyLoadPromise;
|
|
834
918
|
};
|