@atlaskit/editor-plugin-autocomplete 9.1.0 → 9.2.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 +115 -0
- package/dist/cjs/analytics/ufo.js +6 -5
- package/dist/cjs/pm-plugins/autocomplete-plugin.js +496 -50
- package/dist/cjs/pm-plugins/debug-mode.js +14 -3
- package/dist/cjs/pm-plugins/inline-code-harvester.js +576 -0
- package/dist/cjs/pm-plugins/text-predictor.js +298 -151
- package/dist/es2019/analytics/ufo.js +2 -1
- package/dist/es2019/pm-plugins/autocomplete-plugin.js +457 -36
- package/dist/es2019/pm-plugins/debug-mode.js +13 -2
- package/dist/es2019/pm-plugins/inline-code-harvester.js +455 -0
- package/dist/es2019/pm-plugins/text-predictor.js +136 -4
- package/dist/esm/analytics/ufo.js +2 -1
- package/dist/esm/pm-plugins/autocomplete-plugin.js +494 -50
- package/dist/esm/pm-plugins/debug-mode.js +13 -2
- package/dist/esm/pm-plugins/inline-code-harvester.js +572 -0
- package/dist/esm/pm-plugins/text-predictor.js +297 -150
- package/dist/types/analytics/ufo.d.ts +1 -1
- package/dist/types/pm-plugins/autocomplete-plugin.d.ts +37 -0
- package/dist/types/pm-plugins/debug-mode.d.ts +19 -4
- package/dist/types/pm-plugins/inline-code-harvester.d.ts +146 -0
- package/dist/types/pm-plugins/text-predictor.d.ts +70 -3
- package/package.json +2 -2
- package/src/analytics/ufo.ts +3 -6
- package/src/pm-plugins/autocomplete-plugin.ts +526 -31
- package/src/pm-plugins/debug-mode.ts +26 -5
- package/src/pm-plugins/inline-code-harvester.ts +561 -0
- package/src/pm-plugins/text-predictor.ts +162 -7
|
@@ -175,6 +175,18 @@ const DEBUG_TEXT_TAIL_CHARS = 120;
|
|
|
175
175
|
* is vocabulary coverage.
|
|
176
176
|
*/
|
|
177
177
|
|
|
178
|
+
/**
|
|
179
|
+
* What the scored path concluded, recorded on every evaluation whether or not
|
|
180
|
+
* debug is on.
|
|
181
|
+
*
|
|
182
|
+
* This exists for the inline-code harvester, which may only offer a harvested
|
|
183
|
+
* surface once the scored path has finished and come away empty. The reason is
|
|
184
|
+
* the load-bearing part: `no-candidate` means no vocabulary reaches this prefix
|
|
185
|
+
* at all, while `winner-margin` means two known words the model cannot yet
|
|
186
|
+
* separate — the first is a gap worth filling and the second is a prefix
|
|
187
|
+
* ambiguous enough that filling it would be a guess.
|
|
188
|
+
*/
|
|
189
|
+
|
|
178
190
|
/**
|
|
179
191
|
* A candidate paired with the length of the already-typed prefix it completes.
|
|
180
192
|
* For a single word this is the current partial token length; for a phrase it
|
|
@@ -203,6 +215,17 @@ class TrieNode {
|
|
|
203
215
|
class WeightedWordTrie {
|
|
204
216
|
constructor() {
|
|
205
217
|
_defineProperty(this, "root", new TrieNode());
|
|
218
|
+
/**
|
|
219
|
+
* Every node a session boost has been written to.
|
|
220
|
+
*
|
|
221
|
+
* Kept because dropping the boosts is no longer a rare event — it happens
|
|
222
|
+
* each time the reader changes page or conversation — and walking a vocabulary
|
|
223
|
+
* of tens of thousands of words to find the few hundred that were touched
|
|
224
|
+
* costs about 10ms of main thread every time. The two writers below are the
|
|
225
|
+
* only way a `sessionFreq` moves, so keeping this in step costs one Set
|
|
226
|
+
* insertion on a path that is already descending the trie.
|
|
227
|
+
*/
|
|
228
|
+
_defineProperty(this, "boostedNodes", new Set());
|
|
206
229
|
/** Highest tenantFreq seen — used to normalize freq scores at query time */
|
|
207
230
|
_defineProperty(this, "maxTenantFreq", 1);
|
|
208
231
|
}
|
|
@@ -276,6 +299,11 @@ class WeightedWordTrie {
|
|
|
276
299
|
return node.word !== null ? node : null;
|
|
277
300
|
}
|
|
278
301
|
|
|
302
|
+
/** Whether this exact surface is stored as a terminal word. */
|
|
303
|
+
hasWord(word) {
|
|
304
|
+
return this.findNode(word) !== null;
|
|
305
|
+
}
|
|
306
|
+
|
|
279
307
|
/**
|
|
280
308
|
* Set the session frequency for a word.
|
|
281
309
|
* Returns true if the word exists in the trie.
|
|
@@ -286,6 +314,7 @@ class WeightedWordTrie {
|
|
|
286
314
|
return false;
|
|
287
315
|
}
|
|
288
316
|
node.sessionFreq = count;
|
|
317
|
+
this.boostedNodes.add(node);
|
|
289
318
|
return true;
|
|
290
319
|
}
|
|
291
320
|
|
|
@@ -299,17 +328,29 @@ class WeightedWordTrie {
|
|
|
299
328
|
return false;
|
|
300
329
|
}
|
|
301
330
|
node.sessionFreq += 1;
|
|
331
|
+
this.boostedNodes.add(node);
|
|
302
332
|
return true;
|
|
303
333
|
}
|
|
304
334
|
|
|
335
|
+
/**
|
|
336
|
+
* Zero every session boost, in the number of words boosted rather than the
|
|
337
|
+
* number of words known.
|
|
338
|
+
*/
|
|
339
|
+
clearSessionBoosts() {
|
|
340
|
+
for (const node of this.boostedNodes) {
|
|
341
|
+
node.sessionFreq = 0;
|
|
342
|
+
}
|
|
343
|
+
this.boostedNodes.clear();
|
|
344
|
+
}
|
|
345
|
+
|
|
305
346
|
/**
|
|
306
347
|
* Every word carrying a session boost, optionally limited to one prefix's
|
|
307
348
|
* subtree. Unlike `getCandidates` a word equal to the prefix is included,
|
|
308
349
|
* since the question here is what the session holds rather than what could
|
|
309
350
|
* still be typed.
|
|
310
351
|
*
|
|
311
|
-
* Walks the trie
|
|
312
|
-
*
|
|
352
|
+
* Walks the trie rather than reading `boostedNodes`, since a prefix answer is
|
|
353
|
+
* a subtree question and this only runs when a human asks it.
|
|
313
354
|
*/
|
|
314
355
|
collectSessionBoosted(prefix = '') {
|
|
315
356
|
let node = this.root;
|
|
@@ -390,6 +431,13 @@ let phraseTermCount = 0;
|
|
|
390
431
|
let maxBigramFreq = 1;
|
|
391
432
|
let maxPhraseFreq = 1;
|
|
392
433
|
let lastPredictionDebug = null;
|
|
434
|
+
let lastPredictionOutcome = null;
|
|
435
|
+
const recordPredictionOutcome = outcome => {
|
|
436
|
+
lastPredictionOutcome = outcome;
|
|
437
|
+
};
|
|
438
|
+
|
|
439
|
+
/** The verdict from the most recent `predict` call. Always populated. */
|
|
440
|
+
export const getLastPredictionOutcome = () => lastPredictionOutcome;
|
|
393
441
|
|
|
394
442
|
// ── Stabilization (QI-2): post-accept cooldown + whole-surface repetition ────
|
|
395
443
|
// Two guards that stop the accept→echo (`end to end` → `end to end to end`) and
|
|
@@ -421,6 +469,22 @@ export const noteSuggestionAccepted = surface => {
|
|
|
421
469
|
}
|
|
422
470
|
};
|
|
423
471
|
|
|
472
|
+
/**
|
|
473
|
+
* Whether `surface` is the one the user just accepted and is still inside its
|
|
474
|
+
* cooldown window.
|
|
475
|
+
*
|
|
476
|
+
* Read-only, unlike the advance inside `predict`: the cooldown is measured in
|
|
477
|
+
* predictions, and a caller asking whether it is active must not consume one of
|
|
478
|
+
* them. Exported for the harvest path, which displays without going through
|
|
479
|
+
* arbitration and so would otherwise re-offer what was just accepted.
|
|
480
|
+
*/
|
|
481
|
+
export const isSurfaceInAcceptCooldown = surface => {
|
|
482
|
+
if (!acceptCooldown || acceptCooldown.surface !== surface.trim().toLowerCase()) {
|
|
483
|
+
return false;
|
|
484
|
+
}
|
|
485
|
+
return acceptCooldown.predictionsSince <= COOLDOWN_KEYSTROKES || performance.now() - acceptCooldown.ts < COOLDOWN_MS;
|
|
486
|
+
};
|
|
487
|
+
|
|
424
488
|
/** Get vector for a word from the store. */
|
|
425
489
|
const getWordVector = word => {
|
|
426
490
|
if (!vectorStore) {
|
|
@@ -690,6 +754,36 @@ export const incrementSessionFreq = word => {
|
|
|
690
754
|
wordTrie.incrementSessionFreq(word);
|
|
691
755
|
};
|
|
692
756
|
|
|
757
|
+
/**
|
|
758
|
+
* Drop every L1 boost this session has accumulated.
|
|
759
|
+
*
|
|
760
|
+
* The vocabulary itself is left alone: only `sessionFreq` is cleared, so the
|
|
761
|
+
* tenant and generic frequencies a boost was sitting on top of survive. Called
|
|
762
|
+
* when the plugin decides the session it was learning for has ended — a new
|
|
763
|
+
* conversation, or a different page — since a boost is a claim about what is
|
|
764
|
+
* being discussed and that claim does not carry over.
|
|
765
|
+
*/
|
|
766
|
+
export const resetSessionBoosts = () => {
|
|
767
|
+
wordTrie.clearSessionBoosts();
|
|
768
|
+
// Anything memoized against the old boosts is now describing a session that
|
|
769
|
+
// no longer exists.
|
|
770
|
+
recallGeneration++;
|
|
771
|
+
};
|
|
772
|
+
|
|
773
|
+
/**
|
|
774
|
+
* Which vocabulary already holds this surface, if any.
|
|
775
|
+
*
|
|
776
|
+
* Used by the inline-code harvester to drop terms the scored path can already
|
|
777
|
+
* serve, so that harvesting stays limited to words with no route to a
|
|
778
|
+
* suggestion today.
|
|
779
|
+
*/
|
|
780
|
+
export const lookupVocabularySource = word => {
|
|
781
|
+
if (wordTrie.hasWord(word)) {
|
|
782
|
+
return 'l2';
|
|
783
|
+
}
|
|
784
|
+
return l3Trie.hasWord(word) ? 'l3' : null;
|
|
785
|
+
};
|
|
786
|
+
|
|
693
787
|
/**
|
|
694
788
|
* Prime session frequencies from a document page string.
|
|
695
789
|
*
|
|
@@ -728,7 +822,7 @@ export const ingestDocumentPage = pageContent => {
|
|
|
728
822
|
* strongest boosts are the ones that change an ordering, and `boosted` still
|
|
729
823
|
* reports the full size, so the cap loses nothing but volume.
|
|
730
824
|
*/
|
|
731
|
-
const MAX_LISTED_SESSION_WORDS =
|
|
825
|
+
const MAX_LISTED_SESSION_WORDS = 100;
|
|
732
826
|
/**
|
|
733
827
|
* Read the session's L1 boosts, optionally narrowed to a prefix.
|
|
734
828
|
*
|
|
@@ -739,7 +833,8 @@ const MAX_LISTED_SESSION_WORDS = 50;
|
|
|
739
833
|
* Only words the vocabulary already holds can carry a boost, because both writers
|
|
740
834
|
* go through `incrementSessionFreq` and it only finds existing nodes. An ingested
|
|
741
835
|
* word absent from the vocabulary is therefore missing from here and always will
|
|
742
|
-
* be
|
|
836
|
+
* be — that gap is what the inline-code harvester covers, and those surfaces show
|
|
837
|
+
* up under `__atlCtcDebug__.harvest()` instead.
|
|
743
838
|
*/
|
|
744
839
|
export const inspectSessionBoosts = prefix => {
|
|
745
840
|
const boosted = wordTrie.collectSessionBoosted(prefix !== null && prefix !== void 0 ? prefix : '');
|
|
@@ -871,6 +966,14 @@ export const predict = textBefore => {
|
|
|
871
966
|
void loadDefaultVocabulary({
|
|
872
967
|
source: 'predict'
|
|
873
968
|
}).catch(() => {});
|
|
969
|
+
// Awaiting, not empty-handed: with no vocabulary loaded the harvester's own
|
|
970
|
+
// intake filter has not been applied to anything either.
|
|
971
|
+
recordPredictionOutcome({
|
|
972
|
+
abstainReason: 'not-initialized',
|
|
973
|
+
awaitingAsyncEvidence: true,
|
|
974
|
+
scoredCandidateCount: 0,
|
|
975
|
+
textBefore
|
|
976
|
+
});
|
|
874
977
|
return null;
|
|
875
978
|
}
|
|
876
979
|
const t0 = performance.now();
|
|
@@ -900,11 +1003,23 @@ export const predict = textBefore => {
|
|
|
900
1003
|
priority: 0
|
|
901
1004
|
});
|
|
902
1005
|
}
|
|
1006
|
+
recordPredictionOutcome({
|
|
1007
|
+
abstainReason: 'prefetch',
|
|
1008
|
+
awaitingAsyncEvidence: true,
|
|
1009
|
+
scoredCandidateCount: 0,
|
|
1010
|
+
textBefore
|
|
1011
|
+
});
|
|
903
1012
|
return null;
|
|
904
1013
|
}
|
|
905
1014
|
const trimmed = textBefore.trimEnd();
|
|
906
1015
|
const trailingSurfaceToken = (_trimmed$match$ = (_trimmed$match = trimmed.match(TRAILING_SURFACE_TOKEN_REGEX)) === null || _trimmed$match === void 0 ? void 0 : _trimmed$match[0]) !== null && _trimmed$match$ !== void 0 ? _trimmed$match$ : '';
|
|
907
1016
|
if (trailingSurfaceToken.length === 0) {
|
|
1017
|
+
recordPredictionOutcome({
|
|
1018
|
+
abstainReason: 'no-surface-token',
|
|
1019
|
+
awaitingAsyncEvidence: false,
|
|
1020
|
+
scoredCandidateCount: 0,
|
|
1021
|
+
textBefore
|
|
1022
|
+
});
|
|
908
1023
|
return null;
|
|
909
1024
|
}
|
|
910
1025
|
const currentWord = trailingSurfaceToken;
|
|
@@ -916,6 +1031,14 @@ export const predict = textBefore => {
|
|
|
916
1031
|
|
|
917
1032
|
// If every trie was empty for this prefix
|
|
918
1033
|
if (canonicalMatched.length === 0) {
|
|
1034
|
+
// Terminal, and the only verdict that says the vocabulary has no claim on
|
|
1035
|
+
// this prefix at all — which is what makes it the harvester's cue.
|
|
1036
|
+
recordPredictionOutcome({
|
|
1037
|
+
abstainReason: 'no-candidate',
|
|
1038
|
+
awaitingAsyncEvidence: false,
|
|
1039
|
+
scoredCandidateCount: 0,
|
|
1040
|
+
textBefore
|
|
1041
|
+
});
|
|
919
1042
|
if (isAutocompleteDebugEnabled()) {
|
|
920
1043
|
// eslint-disable-next-line no-console
|
|
921
1044
|
console.log(`%c[CTC]%c — abstain: no matches for "${currentWord}"`, CTC_STYLES.brand, CTC_STYLES.body);
|
|
@@ -1599,6 +1722,12 @@ export const predict = textBefore => {
|
|
|
1599
1722
|
return ranked.some(hasJudgeableEvidence) ? 'below-posterior-gate' : 'no-evidence';
|
|
1600
1723
|
};
|
|
1601
1724
|
const abstainReason = resolveAbstainReason();
|
|
1725
|
+
recordPredictionOutcome({
|
|
1726
|
+
abstainReason,
|
|
1727
|
+
awaitingAsyncEvidence: hasUnresolvedPotential && !clearsWinnerMargin,
|
|
1728
|
+
scoredCandidateCount: ranked.length,
|
|
1729
|
+
textBefore
|
|
1730
|
+
});
|
|
1602
1731
|
|
|
1603
1732
|
// The leader is the best-supported candidate, not the selected one: an
|
|
1604
1733
|
// evaluation that showed nothing is exactly the one whose posterior needs
|
|
@@ -1649,6 +1778,9 @@ export const predict = textBefore => {
|
|
|
1649
1778
|
'missing-artifact': 'canonical artifact coverage missing',
|
|
1650
1779
|
'no-candidate': 'nothing cleared scoring',
|
|
1651
1780
|
'no-evidence': 'full-surface evidence absent',
|
|
1781
|
+
// Recorded at the two exits above this block, so they never print here.
|
|
1782
|
+
'no-surface-token': 'no trailing surface token',
|
|
1783
|
+
'not-initialized': 'vocabulary not loaded',
|
|
1652
1784
|
prefetch: `prefetch: ${currentWord.length}/${DISPLAY_MIN_PREFIX_LENGTH} chars`,
|
|
1653
1785
|
'short-completion': `completion shorter than ${MIN_SUGGESTION_LENGTH} chars`,
|
|
1654
1786
|
'unresolved-rival': 'expanding plausible token-prefix groups',
|
|
@@ -14,7 +14,8 @@ function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t =
|
|
|
14
14
|
* https://hello.atlassian.net/wiki/spaces/AA6/pages/3961393753
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
-
import { ConcurrentExperience
|
|
17
|
+
import { ConcurrentExperience } from '@atlaskit/ufo/concurrent-experience';
|
|
18
|
+
import { ExperiencePerformanceTypes, ExperienceTypes } from '@atlaskit/ufo/experience-types';
|
|
18
19
|
|
|
19
20
|
/**
|
|
20
21
|
* Experience name strings are surfaced downstream by the UFO pipeline as
|