@atlaskit/editor-plugin-autocomplete 9.0.0 → 9.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 +28 -0
- package/dist/cjs/pm-plugins/autocomplete-plugin.js +82 -31
- package/dist/cjs/pm-plugins/debug-mode.js +17 -2
- package/dist/cjs/pm-plugins/text-predictor.js +290 -178
- package/dist/es2019/pm-plugins/autocomplete-plugin.js +64 -22
- package/dist/es2019/pm-plugins/debug-mode.js +16 -1
- package/dist/es2019/pm-plugins/text-predictor.js +91 -3
- package/dist/esm/pm-plugins/autocomplete-plugin.js +82 -31
- package/dist/esm/pm-plugins/debug-mode.js +16 -1
- package/dist/esm/pm-plugins/text-predictor.js +290 -178
- package/dist/types/pm-plugins/debug-mode.d.ts +19 -0
- package/dist/types/pm-plugins/text-predictor.d.ts +36 -1
- package/package.json +2 -2
- package/src/pm-plugins/autocomplete-plugin.ts +67 -21
- package/src/pm-plugins/debug-mode.ts +26 -1
- package/src/pm-plugins/text-predictor.ts +116 -2
|
@@ -28,6 +28,16 @@ declare global {
|
|
|
28
28
|
isEnabled: () => boolean;
|
|
29
29
|
/** Whether verbose logging (candidate tables + extra detail) is on. */
|
|
30
30
|
isVerbose: () => boolean;
|
|
31
|
+
/**
|
|
32
|
+
* Snapshot of the L1 session boosts — the vocabulary words this session
|
|
33
|
+
* has seen and how often — or one family of them:
|
|
34
|
+
* `__atlCtcDebug__.session('poll')`.
|
|
35
|
+
*
|
|
36
|
+
* Installed by the predictor rather than declared with the rest of the
|
|
37
|
+
* API, and typed as `unknown` so the return shape can live with the module
|
|
38
|
+
* that owns it instead of creating a cycle back to this one.
|
|
39
|
+
*/
|
|
40
|
+
session?: (prefix?: string) => unknown;
|
|
31
41
|
};
|
|
32
42
|
}
|
|
33
43
|
}
|
|
@@ -60,3 +70,12 @@ export declare const ctcSection: (label: string, body: string) => void;
|
|
|
60
70
|
export declare const ctcTag: (tag: string, body: string, tagStyle?: string) => void;
|
|
61
71
|
export declare const isAutocompleteDebugEnabled: () => boolean;
|
|
62
72
|
export declare const isAutocompleteDebugVerbose: () => boolean;
|
|
73
|
+
/**
|
|
74
|
+
* Hang the L1 session-boost snapshot off the console API.
|
|
75
|
+
*
|
|
76
|
+
* Unlike the log helpers this is available whether or not debug is enabled:
|
|
77
|
+
* inspecting state on demand is not logging, and asking someone to turn on
|
|
78
|
+
* logging and retype to find out what the session already holds defeats the
|
|
79
|
+
* point of being able to ask.
|
|
80
|
+
*/
|
|
81
|
+
export declare const registerCtcSessionInspector: (inspect: (prefix?: string) => unknown) => void;
|
|
@@ -12,7 +12,10 @@
|
|
|
12
12
|
* Falls back to cold mode (freq-only) when vectors not yet loaded.
|
|
13
13
|
*
|
|
14
14
|
* Session personalization (L1): words the user types are incrementally boosted
|
|
15
|
-
* via incrementSessionFreq(), called on word boundaries from the plugin
|
|
15
|
+
* via incrementSessionFreq(), called on word boundaries from the plugin, and
|
|
16
|
+
* words in ingested context text via ingestDocumentPage(). What the session has
|
|
17
|
+
* boosted is visible at any time from the console: `__atlCtcDebug__.session()`,
|
|
18
|
+
* or `__atlCtcDebug__.session('poll')` for one family — see inspectSessionBoosts.
|
|
16
19
|
*/
|
|
17
20
|
import type { TermType } from './scoring-pipeline';
|
|
18
21
|
export interface WeightedTerm {
|
|
@@ -111,6 +114,38 @@ export declare const incrementSessionFreq: (word: string) => void;
|
|
|
111
114
|
* calling context does not yet have a page value available.
|
|
112
115
|
*/
|
|
113
116
|
export declare const ingestDocumentPage: (pageContent: string | undefined) => void;
|
|
117
|
+
interface SessionWordSnapshot {
|
|
118
|
+
/** Times this session has seen it: words typed plus words in ingested text. */
|
|
119
|
+
sessionFreq: number;
|
|
120
|
+
/** No corpus frequency behind it, so L1 is the whole of its standing. */
|
|
121
|
+
sessionOnly: boolean;
|
|
122
|
+
surface: string;
|
|
123
|
+
/** Corpus frequency shipped with the vocabulary, for scale against the boost. */
|
|
124
|
+
tenantFreq: number;
|
|
125
|
+
}
|
|
126
|
+
export interface SessionSnapshot {
|
|
127
|
+
/** How many words hold a boost, whether or not they are listed below. */
|
|
128
|
+
boosted: number;
|
|
129
|
+
/** Ceiling on `words`; beyond it the weakest boosts are left out of the listing. */
|
|
130
|
+
limit: number;
|
|
131
|
+
/** The prefix asked about, when one was passed. */
|
|
132
|
+
prefix?: string;
|
|
133
|
+
/** Strongest boost first, then alphabetically. */
|
|
134
|
+
words: SessionWordSnapshot[];
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Read the session's L1 boosts, optionally narrowed to a prefix.
|
|
138
|
+
*
|
|
139
|
+
* Installed as `__atlCtcDebug__.session()`, with `__atlCtcDebug__.session('poll')`
|
|
140
|
+
* to ask about one family. Returned rather than logged, so the console renders it
|
|
141
|
+
* as an inspectable object and a caller can assert on it.
|
|
142
|
+
*
|
|
143
|
+
* Only words the vocabulary already holds can carry a boost, because both writers
|
|
144
|
+
* go through `incrementSessionFreq` and it only finds existing nodes. An ingested
|
|
145
|
+
* word absent from the vocabulary is therefore missing from here and always will
|
|
146
|
+
* be.
|
|
147
|
+
*/
|
|
148
|
+
export declare const inspectSessionBoosts: (prefix?: string) => SessionSnapshot;
|
|
114
149
|
/**
|
|
115
150
|
* Result of a prediction: the ghost tail to insert plus an immutable record of
|
|
116
151
|
* the evidence that authorized the UI commitment.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@atlaskit/editor-plugin-autocomplete",
|
|
3
|
-
"version": "9.
|
|
3
|
+
"version": "9.1.0",
|
|
4
4
|
"description": "Client-side text autocomplete plugin for @atlaskit/editor-core",
|
|
5
5
|
"author": "Atlassian Pty Ltd",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
"wink-nlp": "^2.4.0"
|
|
28
28
|
},
|
|
29
29
|
"peerDependencies": {
|
|
30
|
-
"@atlaskit/editor-common": "^119.
|
|
30
|
+
"@atlaskit/editor-common": "^119.9.0",
|
|
31
31
|
"@atlaskit/editor-plugin-analytics": "^15.0.0",
|
|
32
32
|
"react": "^18.2.0 || ^19.2.0"
|
|
33
33
|
},
|
|
@@ -669,7 +669,67 @@ export const createAutocompletePlugin = (
|
|
|
669
669
|
// Set when the plugin is torn down so in-flight getContext() resolutions don't
|
|
670
670
|
// mutate the global text-predictor state after destruction.
|
|
671
671
|
let destroyed = false;
|
|
672
|
-
|
|
672
|
+
// L1 ingestion cannot run before the vocabulary has loaded: `incrementSessionFreq`
|
|
673
|
+
// only touches trie nodes that already exist, so an empty trie silently rejects
|
|
674
|
+
// every word. Text that arrives first — which a page usually does, since the
|
|
675
|
+
// chat resolves it on focus in the same tick as the load starts — is held here
|
|
676
|
+
// and applied once the load has settled.
|
|
677
|
+
const pendingSessionContextTexts = new Set<string>();
|
|
678
|
+
const sessionIngestedContextTexts = new Set<string>();
|
|
679
|
+
let isVocabularyReady = false;
|
|
680
|
+
let vocabularyLoadPromise: Promise<void> | undefined;
|
|
681
|
+
|
|
682
|
+
const addBoundedContextText = (texts: Set<string>, text: string): void => {
|
|
683
|
+
texts.add(text);
|
|
684
|
+
// Evict oldest entries (Set preserves insertion order) to bound memory.
|
|
685
|
+
while (texts.size > MAX_INGESTED_CONTEXT_TEXTS) {
|
|
686
|
+
const oldest = texts.values().next().value;
|
|
687
|
+
if (oldest === undefined) {
|
|
688
|
+
break;
|
|
689
|
+
}
|
|
690
|
+
texts.delete(oldest);
|
|
691
|
+
}
|
|
692
|
+
};
|
|
693
|
+
|
|
694
|
+
const flushPendingSessionContext = (): void => {
|
|
695
|
+
if (!isVocabularyReady || destroyed) {
|
|
696
|
+
return;
|
|
697
|
+
}
|
|
698
|
+
for (const text of pendingSessionContextTexts) {
|
|
699
|
+
if (!sessionIngestedContextTexts.has(text)) {
|
|
700
|
+
ingestDocumentPage(text);
|
|
701
|
+
addBoundedContextText(sessionIngestedContextTexts, text);
|
|
702
|
+
}
|
|
703
|
+
pendingSessionContextTexts.delete(text);
|
|
704
|
+
}
|
|
705
|
+
};
|
|
706
|
+
|
|
707
|
+
const ensureVocabularyReady = (): Promise<void> => {
|
|
708
|
+
if (isVocabularyReady) {
|
|
709
|
+
flushPendingSessionContext();
|
|
710
|
+
return Promise.resolve();
|
|
711
|
+
}
|
|
712
|
+
vocabularyLoadPromise ??= loadDefaultVocabulary({
|
|
713
|
+
isLocalLLM: options?.useLocalModel ?? false,
|
|
714
|
+
surface,
|
|
715
|
+
})
|
|
716
|
+
.then(() => {
|
|
717
|
+
if (destroyed) {
|
|
718
|
+
return;
|
|
719
|
+
}
|
|
720
|
+
isVocabularyReady = true;
|
|
721
|
+
flushPendingSessionContext();
|
|
722
|
+
})
|
|
723
|
+
.catch((error) => {
|
|
724
|
+
// Do not consume the pending text on failure. A later focus retries the
|
|
725
|
+
// load and can still apply the original context exactly once.
|
|
726
|
+
vocabularyLoadPromise = undefined;
|
|
727
|
+
logException(error as Error, {
|
|
728
|
+
location: 'editor-plugin-autocomplete/loadDefaultVocabulary',
|
|
729
|
+
});
|
|
730
|
+
});
|
|
731
|
+
return vocabularyLoadPromise;
|
|
732
|
+
};
|
|
673
733
|
|
|
674
734
|
const logContextResolved = (source: string, context?: AutocompleteContext): void => {
|
|
675
735
|
if (!isAutocompleteDebugEnabled()) {
|
|
@@ -698,20 +758,11 @@ export const createAutocompletePlugin = (
|
|
|
698
758
|
resolvedContext = { ...resolvedContext, ...definedContext };
|
|
699
759
|
|
|
700
760
|
const ingestContextText = (text?: string): void => {
|
|
701
|
-
if (!text ||
|
|
761
|
+
if (!text || sessionIngestedContextTexts.has(text)) {
|
|
702
762
|
return;
|
|
703
763
|
}
|
|
704
764
|
|
|
705
|
-
|
|
706
|
-
// Evict oldest entries (Set preserves insertion order) to bound memory.
|
|
707
|
-
while (ingestedContextTexts.size > MAX_INGESTED_CONTEXT_TEXTS) {
|
|
708
|
-
const oldest = ingestedContextTexts.values().next().value;
|
|
709
|
-
if (oldest === undefined) {
|
|
710
|
-
break;
|
|
711
|
-
}
|
|
712
|
-
ingestedContextTexts.delete(oldest);
|
|
713
|
-
}
|
|
714
|
-
ingestDocumentPage(text);
|
|
765
|
+
addBoundedContextText(pendingSessionContextTexts, text);
|
|
715
766
|
};
|
|
716
767
|
|
|
717
768
|
ingestContextText(context.fullPageContent);
|
|
@@ -719,6 +770,7 @@ export const createAutocompletePlugin = (
|
|
|
719
770
|
for (const siblingCommentContent of context.siblingCommentsContents ?? []) {
|
|
720
771
|
ingestContextText(siblingCommentContent);
|
|
721
772
|
}
|
|
773
|
+
flushPendingSessionContext();
|
|
722
774
|
|
|
723
775
|
// Context arrived after word boundaries may already have fired. Re-send
|
|
724
776
|
// slow-lane context immediately so the next inference includes the thread.
|
|
@@ -1198,14 +1250,7 @@ export const createAutocompletePlugin = (
|
|
|
1198
1250
|
return false;
|
|
1199
1251
|
}
|
|
1200
1252
|
|
|
1201
|
-
|
|
1202
|
-
isLocalLLM: options?.useLocalModel ?? false,
|
|
1203
|
-
surface,
|
|
1204
|
-
}).catch((error) => {
|
|
1205
|
-
logException(error as Error, {
|
|
1206
|
-
location: 'editor-plugin-autocomplete/loadDefaultVocabulary',
|
|
1207
|
-
});
|
|
1208
|
-
});
|
|
1253
|
+
void ensureVocabularyReady();
|
|
1209
1254
|
loadVectorsAsync({
|
|
1210
1255
|
isLocalLLM: options?.useLocalModel ?? false,
|
|
1211
1256
|
surface,
|
|
@@ -1313,7 +1358,8 @@ export const createAutocompletePlugin = (
|
|
|
1313
1358
|
if (hasDestroy(slowLaneClient)) {
|
|
1314
1359
|
slowLaneClient.destroy();
|
|
1315
1360
|
}
|
|
1316
|
-
|
|
1361
|
+
pendingSessionContextTexts.clear();
|
|
1362
|
+
sessionIngestedContextTexts.clear();
|
|
1317
1363
|
},
|
|
1318
1364
|
};
|
|
1319
1365
|
},
|
|
@@ -29,6 +29,16 @@ declare global {
|
|
|
29
29
|
isEnabled: () => boolean;
|
|
30
30
|
/** Whether verbose logging (candidate tables + extra detail) is on. */
|
|
31
31
|
isVerbose: () => boolean;
|
|
32
|
+
/**
|
|
33
|
+
* Snapshot of the L1 session boosts — the vocabulary words this session
|
|
34
|
+
* has seen and how often — or one family of them:
|
|
35
|
+
* `__atlCtcDebug__.session('poll')`.
|
|
36
|
+
*
|
|
37
|
+
* Installed by the predictor rather than declared with the rest of the
|
|
38
|
+
* API, and typed as `unknown` so the return shape can live with the module
|
|
39
|
+
* that owns it instead of creating a cycle back to this one.
|
|
40
|
+
*/
|
|
41
|
+
session?: (prefix?: string) => unknown;
|
|
32
42
|
};
|
|
33
43
|
}
|
|
34
44
|
}
|
|
@@ -120,7 +130,7 @@ const printLegend = (verbose: boolean): void => {
|
|
|
120
130
|
);
|
|
121
131
|
// eslint-disable-next-line no-console
|
|
122
132
|
console.log(
|
|
123
|
-
'%cInspect%c __atlCtcDebug__.enable("verbose") · .disable()',
|
|
133
|
+
'%cInspect%c __atlCtcDebug__.enable("verbose") · .disable() · .session() (L1 boosts, narrow to a family with .session("poll"))',
|
|
124
134
|
CTC_STYLES.section,
|
|
125
135
|
CTC_STYLES.body,
|
|
126
136
|
);
|
|
@@ -163,5 +173,20 @@ export const isAutocompleteDebugEnabled = (): boolean => getDebugApi()?.isEnable
|
|
|
163
173
|
|
|
164
174
|
export const isAutocompleteDebugVerbose = (): boolean => getDebugApi()?.isVerbose() ?? false;
|
|
165
175
|
|
|
176
|
+
/**
|
|
177
|
+
* Hang the L1 session-boost snapshot off the console API.
|
|
178
|
+
*
|
|
179
|
+
* Unlike the log helpers this is available whether or not debug is enabled:
|
|
180
|
+
* inspecting state on demand is not logging, and asking someone to turn on
|
|
181
|
+
* logging and retype to find out what the session already holds defeats the
|
|
182
|
+
* point of being able to ask.
|
|
183
|
+
*/
|
|
184
|
+
export const registerCtcSessionInspector = (inspect: (prefix?: string) => unknown): void => {
|
|
185
|
+
const api = getDebugApi();
|
|
186
|
+
if (api) {
|
|
187
|
+
api.session = inspect;
|
|
188
|
+
}
|
|
189
|
+
};
|
|
190
|
+
|
|
166
191
|
// Eagerly install so the console API is available on load, regardless of call order.
|
|
167
192
|
getDebugApi();
|
|
@@ -12,7 +12,10 @@
|
|
|
12
12
|
* Falls back to cold mode (freq-only) when vectors not yet loaded.
|
|
13
13
|
*
|
|
14
14
|
* Session personalization (L1): words the user types are incrementally boosted
|
|
15
|
-
* via incrementSessionFreq(), called on word boundaries from the plugin
|
|
15
|
+
* via incrementSessionFreq(), called on word boundaries from the plugin, and
|
|
16
|
+
* words in ingested context text via ingestDocumentPage(). What the session has
|
|
17
|
+
* boosted is visible at any time from the console: `__atlCtcDebug__.session()`,
|
|
18
|
+
* or `__atlCtcDebug__.session('poll')` for one family — see inspectSessionBoosts.
|
|
16
19
|
*/
|
|
17
20
|
|
|
18
21
|
import { EXPERIENCE_NAME, failExp, startExp, succeedExp } from '../analytics/ufo';
|
|
@@ -35,6 +38,7 @@ import {
|
|
|
35
38
|
ctcTag,
|
|
36
39
|
isAutocompleteDebugEnabled,
|
|
37
40
|
isAutocompleteDebugVerbose,
|
|
41
|
+
registerCtcSessionInspector,
|
|
38
42
|
} from './debug-mode';
|
|
39
43
|
import {
|
|
40
44
|
loadGrammarDataAsync,
|
|
@@ -388,6 +392,44 @@ class WeightedWordTrie {
|
|
|
388
392
|
node.sessionFreq += 1;
|
|
389
393
|
return true;
|
|
390
394
|
}
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* Every word carrying a session boost, optionally limited to one prefix's
|
|
398
|
+
* subtree. Unlike `getCandidates` a word equal to the prefix is included,
|
|
399
|
+
* since the question here is what the session holds rather than what could
|
|
400
|
+
* still be typed.
|
|
401
|
+
*
|
|
402
|
+
* Walks the trie instead of reading an index, so nothing has to be kept in
|
|
403
|
+
* step on the write path for the sake of being able to ask.
|
|
404
|
+
*/
|
|
405
|
+
collectSessionBoosted(prefix: string = ''): Candidate[] {
|
|
406
|
+
let node = this.root;
|
|
407
|
+
for (const char of prefix.toLowerCase()) {
|
|
408
|
+
const next = node.children.get(char);
|
|
409
|
+
if (!next) {
|
|
410
|
+
return [];
|
|
411
|
+
}
|
|
412
|
+
node = next;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
const boosted: Candidate[] = [];
|
|
416
|
+
const stack: TrieNode[] = [node];
|
|
417
|
+
|
|
418
|
+
while (stack.length > 0) {
|
|
419
|
+
const current = stack.pop();
|
|
420
|
+
if (!current) {
|
|
421
|
+
continue;
|
|
422
|
+
}
|
|
423
|
+
if (current.word !== null && current.sessionFreq > 0) {
|
|
424
|
+
boosted.push({ word: current.word, node: current });
|
|
425
|
+
}
|
|
426
|
+
for (const child of current.children.values()) {
|
|
427
|
+
stack.push(child);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
return boosted;
|
|
432
|
+
}
|
|
391
433
|
}
|
|
392
434
|
|
|
393
435
|
// L1/L2 Trie (Session + Atlassian Domain)
|
|
@@ -823,7 +865,11 @@ export const ingestDocumentPage = (pageContent: string | undefined): void => {
|
|
|
823
865
|
}
|
|
824
866
|
|
|
825
867
|
if (isAutocompleteDebugEnabled() && validBoostedWords.size > 0) {
|
|
826
|
-
ctcTag(
|
|
868
|
+
ctcTag(
|
|
869
|
+
'init',
|
|
870
|
+
`L1 session primed ${validBoostedWords.size} words from page · __atlCtcDebug__.session() to inspect`,
|
|
871
|
+
CTC_STYLES.brand,
|
|
872
|
+
);
|
|
827
873
|
if (isAutocompleteDebugVerbose()) {
|
|
828
874
|
// eslint-disable-next-line no-console
|
|
829
875
|
console.dir(Array.from(validBoostedWords).sort());
|
|
@@ -831,6 +877,74 @@ export const ingestDocumentPage = (pageContent: string | undefined): void => {
|
|
|
831
877
|
}
|
|
832
878
|
};
|
|
833
879
|
|
|
880
|
+
/**
|
|
881
|
+
* How many boosted words `inspectSessionBoosts` lists.
|
|
882
|
+
*
|
|
883
|
+
* A page ingest can boost thousands, and a list that long is not read. The
|
|
884
|
+
* strongest boosts are the ones that change an ordering, and `boosted` still
|
|
885
|
+
* reports the full size, so the cap loses nothing but volume.
|
|
886
|
+
*/
|
|
887
|
+
const MAX_LISTED_SESSION_WORDS = 50;
|
|
888
|
+
|
|
889
|
+
interface SessionWordSnapshot {
|
|
890
|
+
/** Times this session has seen it: words typed plus words in ingested text. */
|
|
891
|
+
sessionFreq: number;
|
|
892
|
+
/** No corpus frequency behind it, so L1 is the whole of its standing. */
|
|
893
|
+
sessionOnly: boolean;
|
|
894
|
+
surface: string;
|
|
895
|
+
/** Corpus frequency shipped with the vocabulary, for scale against the boost. */
|
|
896
|
+
tenantFreq: number;
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
export interface SessionSnapshot {
|
|
900
|
+
/** How many words hold a boost, whether or not they are listed below. */
|
|
901
|
+
boosted: number;
|
|
902
|
+
/** Ceiling on `words`; beyond it the weakest boosts are left out of the listing. */
|
|
903
|
+
limit: number;
|
|
904
|
+
/** The prefix asked about, when one was passed. */
|
|
905
|
+
prefix?: string;
|
|
906
|
+
/** Strongest boost first, then alphabetically. */
|
|
907
|
+
words: SessionWordSnapshot[];
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
/**
|
|
911
|
+
* Read the session's L1 boosts, optionally narrowed to a prefix.
|
|
912
|
+
*
|
|
913
|
+
* Installed as `__atlCtcDebug__.session()`, with `__atlCtcDebug__.session('poll')`
|
|
914
|
+
* to ask about one family. Returned rather than logged, so the console renders it
|
|
915
|
+
* as an inspectable object and a caller can assert on it.
|
|
916
|
+
*
|
|
917
|
+
* Only words the vocabulary already holds can carry a boost, because both writers
|
|
918
|
+
* go through `incrementSessionFreq` and it only finds existing nodes. An ingested
|
|
919
|
+
* word absent from the vocabulary is therefore missing from here and always will
|
|
920
|
+
* be.
|
|
921
|
+
*/
|
|
922
|
+
export const inspectSessionBoosts = (prefix?: string): SessionSnapshot => {
|
|
923
|
+
const boosted = wordTrie.collectSessionBoosted(prefix ?? '');
|
|
924
|
+
return {
|
|
925
|
+
boosted: boosted.length,
|
|
926
|
+
limit: MAX_LISTED_SESSION_WORDS,
|
|
927
|
+
...(prefix === undefined ? {} : { prefix }),
|
|
928
|
+
words: boosted
|
|
929
|
+
.sort(
|
|
930
|
+
(a, b) =>
|
|
931
|
+
b.node.sessionFreq - a.node.sessionFreq ||
|
|
932
|
+
a.word.localeCompare(b.word, 'en', { numeric: true, sensitivity: 'base' }),
|
|
933
|
+
)
|
|
934
|
+
.slice(0, MAX_LISTED_SESSION_WORDS)
|
|
935
|
+
.map(({ node, word }) => ({
|
|
936
|
+
sessionFreq: node.sessionFreq,
|
|
937
|
+
sessionOnly: node.tenantFreq === 0,
|
|
938
|
+
surface: word,
|
|
939
|
+
tenantFreq: node.tenantFreq,
|
|
940
|
+
})),
|
|
941
|
+
};
|
|
942
|
+
};
|
|
943
|
+
|
|
944
|
+
// At module scope so the console answers before the first keystroke, which is
|
|
945
|
+
// when someone reaching for it usually asks.
|
|
946
|
+
registerCtcSessionInspector(inspectSessionBoosts);
|
|
947
|
+
|
|
834
948
|
/**
|
|
835
949
|
* Result of a prediction: the ghost tail to insert plus an immutable record of
|
|
836
950
|
* the evidence that authorized the UI commitment.
|