@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
|
@@ -194,16 +194,15 @@ function applyGrammarFilter(
|
|
|
194
194
|
}
|
|
195
195
|
}
|
|
196
196
|
|
|
197
|
-
|
|
198
|
-
|
|
197
|
+
// Grammar is authoritative.
|
|
199
198
|
return {
|
|
200
|
-
filtered:
|
|
199
|
+
filtered: filtered,
|
|
201
200
|
grammarMeta: {
|
|
202
201
|
prevWord: lowerPrev,
|
|
203
202
|
prevTags,
|
|
204
203
|
before: candidates.length,
|
|
205
|
-
after:
|
|
206
|
-
dropped:
|
|
204
|
+
after: filtered.length,
|
|
205
|
+
dropped: dropped,
|
|
207
206
|
},
|
|
208
207
|
};
|
|
209
208
|
}
|
|
@@ -17,11 +17,9 @@
|
|
|
17
17
|
|
|
18
18
|
import { EXPERIENCE_NAME, failExp, startExp, succeedExp } from '../analytics/ufo';
|
|
19
19
|
|
|
20
|
-
//
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
import wordIndexData from './data/word_index_10k.json';
|
|
24
|
-
// import { rankCandidates, isGrammarAllowed } from './scoring-pipeline';
|
|
20
|
+
// The vocabulary, L3 and word-index JSON payloads are dynamically imported in
|
|
21
|
+
// loadDefaultVocabulary / loadVectorsAsync so their (large) contents stay out of
|
|
22
|
+
// the editor's main chunk and only load when autocomplete is initialised.
|
|
25
23
|
import { isAutocompleteDebugEnabled } from './debug-mode';
|
|
26
24
|
import { rankCandidates, STAGE1_WEIGHT, STAGE2_WEIGHT, MIN_STAGE1_SCORE } from './scoring-pipeline';
|
|
27
25
|
import type { ScoringCandidate } from './scoring-pipeline';
|
|
@@ -35,7 +33,7 @@ const PUNCTUATION_BOUNDARY_REGEX = /^[.,;:!?()\[\]{}"'`]+|[.,;:!?()\[\]{}"'`]+$/
|
|
|
35
33
|
const MIN_PREFIX_LENGTH = 3;
|
|
36
34
|
const MAX_CANDIDATES = 200;
|
|
37
35
|
const CONTEXT_WORDS = 10;
|
|
38
|
-
const MIN_SCORE_THRESHOLD = 0.
|
|
36
|
+
const MIN_SCORE_THRESHOLD = 0.35;
|
|
39
37
|
const L3_BASELINE_FREQ = 0.001;
|
|
40
38
|
|
|
41
39
|
// ─── Types ───────────────────────────────────────────────────────────────────
|
|
@@ -175,7 +173,6 @@ const wordTrie = new WeightedWordTrie();
|
|
|
175
173
|
// L3 Trie (General English Fallback)
|
|
176
174
|
const l3Trie = new WeightedWordTrie();
|
|
177
175
|
|
|
178
|
-
// --- Initialization Function ---
|
|
179
176
|
/**
|
|
180
177
|
* Loads the General English vocabulary.
|
|
181
178
|
* expects a simple array of strings: ["about", "above", "actually", ...]
|
|
@@ -290,14 +287,11 @@ const tokenize = (text: string): string[] => {
|
|
|
290
287
|
};
|
|
291
288
|
|
|
292
289
|
const extractPreviousWord = (text: string): string => {
|
|
293
|
-
//
|
|
290
|
+
// Only consider the current sentence/line the user is typing in.
|
|
294
291
|
// eslint-disable-next-line require-unicode-regexp
|
|
295
292
|
const sentences = text.split(/[\n.?!]+/);
|
|
296
|
-
|
|
297
|
-
// 2. Only look at the current sentence/line the user is typing in
|
|
298
293
|
const currentSentence = sentences[sentences.length - 1];
|
|
299
294
|
|
|
300
|
-
// 3. Extract the previous word as normal
|
|
301
295
|
// eslint-disable-next-line require-unicode-regexp
|
|
302
296
|
const words = currentSentence.trimEnd().split(/\s+/);
|
|
303
297
|
return words.length >= 2 ? words[words.length - 2] : '';
|
|
@@ -370,7 +364,6 @@ export const incrementSessionFreq = (word: string): void => {
|
|
|
370
364
|
* Pass `undefined` (or omit the argument) to skip priming — useful when the
|
|
371
365
|
* calling context does not yet have a page value available.
|
|
372
366
|
*/
|
|
373
|
-
// NOTE: We ingest full page context here
|
|
374
367
|
export const ingestDocumentPage = (pageContent: string | undefined): void => {
|
|
375
368
|
if (!pageContent) {
|
|
376
369
|
return;
|
|
@@ -402,7 +395,11 @@ export const ingestDocumentPage = (pageContent: string | undefined): void => {
|
|
|
402
395
|
|
|
403
396
|
export const predict = (textBefore: string): string | null => {
|
|
404
397
|
if (!isInitialized) {
|
|
405
|
-
|
|
398
|
+
// Vocabulary JSON is code-split and loads asynchronously. Kick off the load
|
|
399
|
+
// and skip this keystroke; the plugin also primes it on focus, so the tries
|
|
400
|
+
// are usually ready before the user types.
|
|
401
|
+
void loadDefaultVocabulary().catch(() => {});
|
|
402
|
+
return null;
|
|
406
403
|
}
|
|
407
404
|
|
|
408
405
|
const t0 = performance.now();
|
|
@@ -458,19 +455,17 @@ export const predict = (textBefore: string): string | null => {
|
|
|
458
455
|
return null;
|
|
459
456
|
}
|
|
460
457
|
|
|
461
|
-
// 1. Primary Query: Ask the L2 Domain Trie
|
|
462
458
|
const candidates = wordTrie.getCandidates(currentWord, MAX_CANDIDATES);
|
|
463
459
|
|
|
464
|
-
//
|
|
460
|
+
// Gap-fill from the L3 general-English trie, requesting a full buffer so
|
|
461
|
+
// enough survive de-duplication against the L2 results.
|
|
465
462
|
if (candidates.length < MAX_CANDIDATES) {
|
|
466
|
-
// Ask L3 for MAX_CANDIDATES to guarantee we have enough buffer
|
|
467
|
-
// to survive the deduplication process.
|
|
468
463
|
const l3Candidates = l3Trie.getCandidates(currentWord, MAX_CANDIDATES);
|
|
469
464
|
|
|
470
465
|
const existingWords = new Set(candidates.map((c) => c.word));
|
|
471
466
|
|
|
472
467
|
for (const l3c of l3Candidates) {
|
|
473
|
-
if (candidates.length >= MAX_CANDIDATES) break;
|
|
468
|
+
if (candidates.length >= MAX_CANDIDATES) break;
|
|
474
469
|
|
|
475
470
|
if (!existingWords.has(l3c.word)) {
|
|
476
471
|
candidates.push(l3c);
|
|
@@ -711,6 +706,45 @@ interface VocabularyJson {
|
|
|
711
706
|
>;
|
|
712
707
|
}
|
|
713
708
|
|
|
709
|
+
/**
|
|
710
|
+
* Unwrap a dynamically imported JSON module to its parsed value, handling both
|
|
711
|
+
* interop modes AFM's bundler chain emits: a `.default`-wrapped namespace
|
|
712
|
+
* (classic webpack) and a named-exports namespace (webpack 5 / atlaspack JSON
|
|
713
|
+
* modules, where `default` can be a misleading scalar). Named exports are
|
|
714
|
+
* preferred when present. The caller declares the JSON `shape` because a dense
|
|
715
|
+
* array and a sparse numeric-keyed object are emitted identically as named
|
|
716
|
+
* exports. Kept in lock-step with the matching helper in local-slow-lane-client.ts.
|
|
717
|
+
*/
|
|
718
|
+
const unwrapJsonModule = <T>(mod: unknown, shape: 'object' | 'array'): T | null => {
|
|
719
|
+
if (mod == null || typeof mod !== 'object') {
|
|
720
|
+
return null;
|
|
721
|
+
}
|
|
722
|
+
const namespace = mod as Record<string, unknown> & { default?: unknown };
|
|
723
|
+
const ownKeys = Object.keys(namespace).filter((k) => k !== 'default' && k !== '__esModule');
|
|
724
|
+
|
|
725
|
+
if (ownKeys.length > 0) {
|
|
726
|
+
if (shape === 'array') {
|
|
727
|
+
const len = ownKeys.length;
|
|
728
|
+
const arr = new Array(len);
|
|
729
|
+
for (let i = 0; i < len; i++) {
|
|
730
|
+
arr[i] = namespace[String(i)];
|
|
731
|
+
}
|
|
732
|
+
return arr as T;
|
|
733
|
+
}
|
|
734
|
+
const obj: Record<string, unknown> = {};
|
|
735
|
+
for (const k of ownKeys) {
|
|
736
|
+
obj[k] = namespace[k];
|
|
737
|
+
}
|
|
738
|
+
return obj as T;
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
if ('default' in namespace && namespace.default != null) {
|
|
742
|
+
return namespace.default as T;
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
return null;
|
|
746
|
+
};
|
|
747
|
+
|
|
714
748
|
export const loadVectorsAsync = async (options?: {
|
|
715
749
|
getBinaryUrl?: () => Promise<string>;
|
|
716
750
|
}): Promise<void> => {
|
|
@@ -752,8 +786,25 @@ export const loadVectorsAsync = async (options?: {
|
|
|
752
786
|
}
|
|
753
787
|
const buffer = await res.arrayBuffer();
|
|
754
788
|
const float32 = new Float32Array(buffer);
|
|
755
|
-
|
|
789
|
+
|
|
790
|
+
// word_index_10k.json is wrapped as `{ "index": {…} }` so no real entry
|
|
791
|
+
// (e.g. the word "default") can shadow the synthetic ESM `default` export
|
|
792
|
+
// the bundler creates for dynamically-imported JSON.
|
|
793
|
+
const wordIndexModule = await import(
|
|
794
|
+
/* webpackChunkName: "@atlaskit-internal_editor-plugin-autocomplete-word-index-10k" */ './data/word_index_10k.json'
|
|
795
|
+
);
|
|
796
|
+
const wordIndexOuter = unwrapJsonModule<{ index?: Record<string, number> }>(
|
|
797
|
+
wordIndexModule,
|
|
798
|
+
'object',
|
|
799
|
+
);
|
|
800
|
+
const wordIndex = wordIndexOuter?.index ?? {};
|
|
756
801
|
const nWords = Object.keys(wordIndex).length;
|
|
802
|
+
if (nWords === 0) {
|
|
803
|
+
// eslint-disable-next-line no-console
|
|
804
|
+
console.warn(
|
|
805
|
+
'[text-predictor] word_index_10k.json missing its `index` wrapper — wordIndex is empty, semantic scoring will be a no-op.',
|
|
806
|
+
);
|
|
807
|
+
}
|
|
757
808
|
const dim = float32.length / nWords;
|
|
758
809
|
|
|
759
810
|
vectorStore = { float32, wordIndex, dim };
|
|
@@ -782,34 +833,62 @@ export const initVectors = (store: VectorStore): void => {
|
|
|
782
833
|
vectorStore = store;
|
|
783
834
|
};
|
|
784
835
|
|
|
785
|
-
|
|
836
|
+
let vocabularyLoadPromise: Promise<void> | undefined;
|
|
837
|
+
|
|
838
|
+
export const loadDefaultVocabulary = (): Promise<void> => {
|
|
786
839
|
if (isInitialized) {
|
|
787
|
-
return;
|
|
840
|
+
return Promise.resolve();
|
|
841
|
+
}
|
|
842
|
+
if (vocabularyLoadPromise) {
|
|
843
|
+
return vocabularyLoadPromise;
|
|
788
844
|
}
|
|
789
845
|
|
|
790
|
-
|
|
846
|
+
vocabularyLoadPromise = (async () => {
|
|
847
|
+
startExp(EXPERIENCE_NAME.LOAD_VOCABULARY, 'singleton');
|
|
791
848
|
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
849
|
+
try {
|
|
850
|
+
// The L2 vocabulary and L3 word list are code-split into their own async
|
|
851
|
+
// chunks so they stay out of the editor's main bundle.
|
|
852
|
+
const [vocabularyModule, l3VocabularyModule] = await Promise.all([
|
|
853
|
+
import(
|
|
854
|
+
/* webpackChunkName: "@atlaskit-internal_editor-plugin-autocomplete-vocabulary-10k" */ './data/vocabulary_10k.json'
|
|
855
|
+
),
|
|
856
|
+
import(
|
|
857
|
+
/* webpackChunkName: "@atlaskit-internal_editor-plugin-autocomplete-l3-vocabulary" */ './data/l3_vocabulary.json'
|
|
858
|
+
),
|
|
859
|
+
]);
|
|
860
|
+
|
|
861
|
+
const vocabularyData = unwrapJsonModule<VocabularyJson>(vocabularyModule, 'object');
|
|
862
|
+
const l3VocabularyData = unwrapJsonModule<string[]>(l3VocabularyModule, 'array');
|
|
863
|
+
|
|
864
|
+
if (vocabularyData?.words == null || !Array.isArray(l3VocabularyData)) {
|
|
865
|
+
throw new Error('[text-predictor] vocabulary JSON modules could not be unwrapped');
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
const terms = Object.entries(vocabularyData.words).map(([word, stats]) => ({
|
|
869
|
+
word,
|
|
870
|
+
freq: stats.freq,
|
|
871
|
+
docFreq: stats.doc_freq,
|
|
872
|
+
authorFreq: stats.author_freq,
|
|
873
|
+
}));
|
|
874
|
+
|
|
875
|
+
// Load L3 before L2: initVocabulary flips `isInitialized = true`, so it
|
|
876
|
+
// must run last — otherwise a throw in initL3Vocabulary would strand
|
|
877
|
+
// `isInitialized` true and the retry path could never reload L3.
|
|
878
|
+
initL3Vocabulary(l3VocabularyData);
|
|
879
|
+
initVocabulary({ terms });
|
|
880
|
+
|
|
881
|
+
succeedExp(EXPERIENCE_NAME.LOAD_VOCABULARY, 'singleton', {
|
|
882
|
+
l2WordCount: terms.length,
|
|
883
|
+
l3WordCount: l3VocabularyData.length,
|
|
884
|
+
});
|
|
885
|
+
} catch (e) {
|
|
886
|
+
failExp(EXPERIENCE_NAME.LOAD_VOCABULARY, 'singleton', { errorType: 'parse_error' });
|
|
887
|
+
// Allow a later call to retry the load rather than caching the failure.
|
|
888
|
+
vocabularyLoadPromise = undefined;
|
|
889
|
+
throw e;
|
|
890
|
+
}
|
|
891
|
+
})();
|
|
892
|
+
|
|
893
|
+
return vocabularyLoadPromise;
|
|
815
894
|
};
|