@atlaskit/editor-plugin-autocomplete 0.1.0 → 0.3.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 (58) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/afm-cc/tsconfig.json +2 -1
  3. package/afm-jira/tsconfig.json +2 -1
  4. package/afm-products/tsconfig.json +2 -1
  5. package/build/tsconfig.json +20 -0
  6. package/build/url-module.d.ts +8 -0
  7. package/dist/cjs/autocompletePlugin.js +18 -5
  8. package/dist/cjs/autocompletePluginType.js +5 -1
  9. package/dist/cjs/pm-plugins/autocomplete-plugin.js +368 -0
  10. package/dist/cjs/pm-plugins/ghost-text-decoration.js +39 -0
  11. package/dist/cjs/pm-plugins/scoring-pipeline.js +256 -0
  12. package/dist/cjs/pm-plugins/slow-lane-client.js +199 -0
  13. package/dist/cjs/pm-plugins/text-predictor.js +796 -0
  14. package/dist/es2019/autocompletePlugin.js +20 -6
  15. package/dist/es2019/autocompletePluginType.js +1 -0
  16. package/dist/es2019/pm-plugins/autocomplete-plugin.js +368 -0
  17. package/dist/es2019/pm-plugins/ghost-text-decoration.js +33 -0
  18. package/dist/es2019/pm-plugins/scoring-pipeline.js +221 -0
  19. package/dist/es2019/pm-plugins/slow-lane-client.js +157 -0
  20. package/dist/es2019/pm-plugins/text-predictor.js +631 -0
  21. package/dist/esm/autocompletePlugin.js +18 -5
  22. package/dist/esm/autocompletePluginType.js +1 -0
  23. package/dist/esm/pm-plugins/autocomplete-plugin.js +362 -0
  24. package/dist/esm/pm-plugins/ghost-text-decoration.js +33 -0
  25. package/dist/esm/pm-plugins/scoring-pipeline.js +252 -0
  26. package/dist/esm/pm-plugins/slow-lane-client.js +192 -0
  27. package/dist/esm/pm-plugins/text-predictor.js +793 -0
  28. package/dist/types/autocompletePluginType.d.ts +6 -3
  29. package/dist/types/pm-plugins/autocomplete-plugin.d.ts +36 -0
  30. package/dist/types/pm-plugins/ghost-text-decoration.d.ts +7 -0
  31. package/dist/types/pm-plugins/scoring-pipeline.d.ts +33 -0
  32. package/dist/types/pm-plugins/slow-lane-client.d.ts +46 -0
  33. package/dist/types/pm-plugins/text-predictor.d.ts +90 -0
  34. package/dist/types-ts4.5/autocompletePluginType.d.ts +6 -3
  35. package/dist/types-ts4.5/pm-plugins/autocomplete-plugin.d.ts +36 -0
  36. package/dist/types-ts4.5/pm-plugins/ghost-text-decoration.d.ts +7 -0
  37. package/dist/types-ts4.5/pm-plugins/scoring-pipeline.d.ts +33 -0
  38. package/dist/types-ts4.5/pm-plugins/slow-lane-client.d.ts +46 -0
  39. package/dist/types-ts4.5/pm-plugins/text-predictor.d.ts +90 -0
  40. package/package.json +2 -2
  41. package/src/autocompletePlugin.tsx +25 -5
  42. package/src/autocompletePluginType.ts +14 -3
  43. package/src/pm-plugins/autocomplete-plugin/package.json +15 -0
  44. package/src/pm-plugins/autocomplete-plugin.ts +443 -0
  45. package/src/pm-plugins/data/combined_l2_l3_pos_tags.json +73571 -3
  46. package/src/pm-plugins/data/ghost_pos_tags.json +43 -3
  47. package/src/pm-plugins/data/grammar_transitions_10k.json +46 -3
  48. package/src/pm-plugins/data/l3_vocabulary.json +20002 -3
  49. package/src/pm-plugins/data/vocabulary_10k.json +38794 -3
  50. package/src/pm-plugins/data/word_index_10k.json +7760 -3
  51. package/src/pm-plugins/ghost-text-decoration.ts +44 -0
  52. package/src/pm-plugins/scoring-pipeline.ts +294 -0
  53. package/src/pm-plugins/slow-lane-client/package.json +15 -0
  54. package/src/pm-plugins/slow-lane-client.ts +222 -0
  55. package/src/pm-plugins/text-predictor/package.json +15 -0
  56. package/src/pm-plugins/text-predictor.ts +780 -0
  57. package/tsconfig.app.json +12 -3
  58. package/tsconfig.json +4 -1
@@ -0,0 +1,44 @@
1
+ import type { EditorState } from '@atlaskit/editor-prosemirror/state';
2
+ import { Decoration, DecorationSet } from '@atlaskit/editor-prosemirror/view';
3
+
4
+ const GHOST_TEXT_CLASS = 'autocomplete-ghost-text';
5
+
6
+ /**
7
+ * Creates a DecorationSet containing a ghost text widget at the given position.
8
+ * The ghost text is rendered as a styled <span> that appears after the cursor.
9
+ */
10
+ export const createGhostTextDecorationSet = (
11
+ state: EditorState,
12
+ position: number,
13
+ text: string,
14
+ ): DecorationSet => {
15
+ if (!text) {
16
+ return DecorationSet.empty;
17
+ }
18
+
19
+ const decoration = Decoration.widget(
20
+ position,
21
+ () => {
22
+ const container = document.createElement('span');
23
+ container.className = GHOST_TEXT_CLASS;
24
+ container.setAttribute('data-autocomplete-ghost', 'true');
25
+ container.style.color = '#999';
26
+ container.style.opacity = '0.6';
27
+ container.style.pointerEvents = 'none';
28
+ container.style.userSelect = 'none';
29
+ container.style.fontStyle = 'italic';
30
+ // U+200B (Zero Width Space) gives the browser a line-break opportunity
31
+ // immediately before the ghost text. This ensures the typed text before
32
+ // the span is never pushed to the next line by the ghost text's width —
33
+ // only the ghost text itself will wrap if it doesn't fit.
34
+ container.textContent = '\u200b' + text;
35
+ return container;
36
+ },
37
+ {
38
+ side: 1, // Render after content at this position
39
+ key: 'autocomplete-ghost-text',
40
+ },
41
+ );
42
+
43
+ return DecorationSet.create(state.doc, [decoration]);
44
+ };
@@ -0,0 +1,294 @@
1
+ /**
2
+ * Scoring Pipeline: Stage 1 (Semantic + Frequency), Grammar Filter, Stage 2 (LM Re-ranking).
3
+ *
4
+ * Operates synchronously on pre-loaded data. Each stage gracefully degrades
5
+ * when its required data isn't available (cold → warm → full warm).
6
+ */
7
+
8
+ import posTagsData from './data/combined_l2_l3_pos_tags.json';
9
+ import ghostPosTagsData from './data/ghost_pos_tags.json';
10
+ import grammarTransitionsData from './data/grammar_transitions_10k.json';
11
+
12
+ // ─── Types ──────────────────────────────────────────────────
13
+
14
+ export interface ScoringCandidate {
15
+ authorFreq: number;
16
+ docFreq: number;
17
+ sessionFreq: number;
18
+ tenantFreq: number;
19
+ word: string;
20
+ }
21
+
22
+ export interface ScoredCandidate {
23
+ finalScore: number;
24
+ freqScore: number;
25
+ lmScore: number;
26
+ semanticScore: number;
27
+ word: string;
28
+ }
29
+
30
+ /** Metadata returned by the grammar filter for debug logging in the caller. */
31
+ export interface GrammarFilterMeta {
32
+ after: number;
33
+ before: number;
34
+ dropped: string[];
35
+ prevTags: string[];
36
+ prevWord: string;
37
+ }
38
+
39
+ interface PosTransitionRule {
40
+ allowed: string[];
41
+ }
42
+
43
+ interface GrammarTransitions {
44
+ transitions: Record<string, PosTransitionRule>;
45
+ }
46
+
47
+ // ─── Scoring Constants ──────────────────────────────────────
48
+
49
+ const ALPHA = 0.5;
50
+ const BETA = 0.5;
51
+ const NEUTRAL_SCORE = 0.5;
52
+ const STAGE1_WEIGHT = 0.6;
53
+ const STAGE2_WEIGHT = 0.4;
54
+ const MIN_STAGE1_SCORE = 0.35;
55
+ const L1_SESSION_CAP = 1.2;
56
+
57
+ // ─── Grammar Data (loaded once on import) ───────────────────
58
+
59
+ const posTags: Map<string, string[]> = new Map(
60
+ Object.entries(posTagsData as Record<string, string[]>),
61
+ );
62
+
63
+ const grammarTransitions = grammarTransitionsData as GrammarTransitions;
64
+
65
+ /**
66
+ * Precomputed map from each POS tag to the set of allowed next POS tags.
67
+ * Built once at module load from grammarTransitions so applyGrammarFilter
68
+ * never re-iterates the transition rules per call.
69
+ */
70
+ const precomputedAllowedByPos: Map<string, Set<string>> = new Map(
71
+ Object.entries(grammarTransitions.transitions).map(([pos, rule]) => [
72
+ pos,
73
+ new Set(rule.allowed),
74
+ ]),
75
+ );
76
+
77
+ // ─── Math ───────────────────────────────────────────────────
78
+
79
+ function cosineSimilarity(a: Float32Array, b: Float32Array): number {
80
+ let dot = 0;
81
+ let normA = 0;
82
+ let normB = 0;
83
+ for (let i = 0; i < a.length; i++) {
84
+ dot += a[i] * b[i];
85
+ normA += a[i] * a[i];
86
+ normB += b[i] * b[i];
87
+ }
88
+ const dNormA = Math.sqrt(normA);
89
+ const dNormB = Math.sqrt(normB);
90
+ if (dNormA === 0 || dNormB === 0) {
91
+ return NEUTRAL_SCORE;
92
+ }
93
+ return (1 + dot / (dNormA * dNormB)) / 2;
94
+ }
95
+
96
+ // ─── Stage 1: Semantic + Frequency ──────────────────────────
97
+
98
+ function scoreStage1(
99
+ candidate: ScoringCandidate,
100
+ contextVector: Float32Array | null,
101
+ getWordVector: (word: string) => Float32Array | null,
102
+ maxTenantFreq: number,
103
+ ): { freqScore: number; semanticScore: number; stage1Score: number } {
104
+
105
+ // 1. Calculate Base Global Score (Normalized Log)
106
+ const maxPossibleLog = Math.log10(maxTenantFreq + 1);
107
+
108
+ // Diversity Adjustment
109
+ const diversityRaw = (
110
+ Math.log10(candidate.tenantFreq + 1) * 0.50 +
111
+ Math.log10(candidate.docFreq + 1) * 0.25 +
112
+ Math.log10(candidate.authorFreq + 1) * 0.25
113
+ ) / maxPossibleLog;
114
+
115
+ const sessionMultiplier = candidate.sessionFreq > 0
116
+ ? 1 + (Math.log10(candidate.sessionFreq + 1) * 2.5)
117
+ : 1;
118
+
119
+ // Apply multiplier; capped at L1_SESSION_CAP (default 1.2) to prevent excessive over-indexing
120
+ const freqScore = Math.min(diversityRaw * sessionMultiplier, L1_SESSION_CAP);
121
+
122
+ // 3. Semantic Scoring
123
+ let semanticScore = NEUTRAL_SCORE;
124
+ if (contextVector) {
125
+ const wordVec = getWordVector(candidate.word);
126
+ semanticScore = wordVec ? cosineSimilarity(contextVector, wordVec) : NEUTRAL_SCORE;
127
+ }
128
+
129
+ return {
130
+ semanticScore,
131
+ freqScore,
132
+ stage1Score: (ALPHA * semanticScore) + (BETA * freqScore),
133
+ };
134
+ }
135
+
136
+ // ─── Grammar Filter ─────────────────────────────────────────
137
+
138
+ /**
139
+ * GHOST POS DICTIONARY
140
+ * A hardcoded mapping of common structural English words that were stripped
141
+ * from the main domain vocabulary. This allows the grammar filter to understand
142
+ * context without suggesting these words to the user.
143
+ */
144
+ const ghostPosTags: Record<string, string[]> = ghostPosTagsData as Record<string, string[]>;
145
+
146
+ type FilterEntry = { candidate: ScoringCandidate; freqScore: number; semanticScore: number; stage1Score: number };
147
+
148
+ function applyGrammarFilter(
149
+ candidates: FilterEntry[],
150
+ previousWord: string,
151
+ ): { filtered: FilterEntry[]; grammarMeta: GrammarFilterMeta | null } {
152
+ if (!previousWord) return { filtered: candidates, grammarMeta: null };
153
+
154
+ const lowerPrev = previousWord.toLowerCase();
155
+ const prevTags = ghostPosTags[lowerPrev] || posTags.get(lowerPrev);
156
+
157
+ if (!prevTags || prevTags.length === 0) {
158
+ return { filtered: candidates, grammarMeta: null };
159
+ }
160
+
161
+ let allowedNextTags: Set<string>;
162
+ if (prevTags.length === 1) {
163
+ // Common case: single POS tag — reuse the precomputed Set directly (no allocation)
164
+ allowedNextTags = precomputedAllowedByPos.get(prevTags[0]) ?? new Set();
165
+ } else {
166
+ allowedNextTags = new Set<string>();
167
+ for (const pt of prevTags) {
168
+ const allowed = precomputedAllowedByPos.get(pt);
169
+ if (allowed) allowed.forEach(tag => allowedNextTags.add(tag));
170
+ }
171
+ }
172
+
173
+ const filtered: FilterEntry[] = [];
174
+ const dropped: string[] = [];
175
+
176
+ for (const entry of candidates) {
177
+ const candidateTags = posTags.get(entry.candidate.word.toLowerCase());
178
+
179
+ // If candidate has no tags (unknown word), let it pass to be safe
180
+ if (!candidateTags || candidateTags.length === 0) {
181
+ filtered.push(entry);
182
+ continue;
183
+ }
184
+
185
+ if (candidateTags.some((ct) => allowedNextTags.has(ct))) {
186
+ filtered.push(entry);
187
+ } else {
188
+ dropped.push(entry.candidate.word);
189
+ }
190
+ }
191
+
192
+ const finalFiltered = filtered.length > 0 ? filtered : candidates;
193
+
194
+ return {
195
+ filtered: finalFiltered,
196
+ grammarMeta: {
197
+ prevWord: lowerPrev,
198
+ prevTags,
199
+ before: candidates.length,
200
+ after: finalFiltered.length,
201
+ dropped: filtered.length > 0 ? dropped : [],
202
+ },
203
+ };
204
+ }
205
+
206
+ // ─── Stage 2: LM Re-ranking ────────────────────────────────
207
+ function getLmScore(
208
+ word: string,
209
+ lmLogits: Record<string, number> | null
210
+ ): number {
211
+ if (!lmLogits) return 0;
212
+
213
+ // Look up the word directly! No more tokens.
214
+ const val = lmLogits[word.toLowerCase()];
215
+ if (typeof val === 'number') {
216
+ return val;
217
+ }
218
+ return 0;
219
+ }
220
+
221
+ // ─── Public API ─────────────────────────────────────────────
222
+
223
+ export interface RankCandidatesResult {
224
+ candidates: ScoredCandidate[];
225
+ grammarMeta: GrammarFilterMeta | null;
226
+ }
227
+
228
+ export function rankCandidates(
229
+ candidates: ScoringCandidate[],
230
+ contextVector: Float32Array | null,
231
+ getWordVector: (word: string) => Float32Array | null,
232
+ lmLogits: Record<string, number> | null,
233
+ maxTenantFreq: number,
234
+ previousWord: string,
235
+ ): RankCandidatesResult {
236
+ // Stage 1
237
+ const stage1Results = candidates.map((candidate) => {
238
+ const { semanticScore, freqScore, stage1Score } = scoreStage1(
239
+ candidate,
240
+ contextVector,
241
+ getWordVector,
242
+ maxTenantFreq,
243
+ );
244
+ return { candidate, semanticScore, freqScore, stage1Score };
245
+ });
246
+
247
+ const stage1Survivors = stage1Results.filter(entry => entry.stage1Score >= MIN_STAGE1_SCORE);
248
+
249
+ // Grammar Filter
250
+ const { filtered, grammarMeta } = applyGrammarFilter(stage1Survivors, previousWord);
251
+
252
+ // Stage 2 + final assembly
253
+ let lmMax = 0;
254
+ if (lmLogits && Object.keys(lmLogits).length > 0) {
255
+ const values = Object.values(lmLogits);
256
+ lmMax = Math.max(...values);
257
+ }
258
+
259
+ const scored: ScoredCandidate[] = filtered.map((entry) => {
260
+ let lmScore = 0;
261
+ let finalScore = entry.stage1Score;
262
+
263
+ if (lmLogits && Object.keys(lmLogits).length > 0) {
264
+ const rawLm = getLmScore(entry.candidate.word, lmLogits);
265
+
266
+ if (rawLm !== 0) {
267
+ // The word was in the top_k! Score it normally.
268
+ const logitDiff = Math.log(rawLm) - Math.log(lmMax);
269
+ lmScore = Math.exp(logitDiff);
270
+ } else {
271
+ lmScore = 0.05;
272
+ }
273
+
274
+ finalScore = STAGE1_WEIGHT * entry.stage1Score + STAGE2_WEIGHT * lmScore;
275
+ }
276
+
277
+ return {
278
+ word: entry.candidate.word,
279
+ freqScore: entry.freqScore,
280
+ semanticScore: entry.semanticScore,
281
+ lmScore,
282
+ finalScore,
283
+ };
284
+ });
285
+
286
+ scored.sort((a, b) => {
287
+ if (b.finalScore !== a.finalScore) {
288
+ return b.finalScore - a.finalScore;
289
+ }
290
+ return a.word.length - b.word.length;
291
+ });
292
+
293
+ return { candidates: scored, grammarMeta };
294
+ }
@@ -0,0 +1,15 @@
1
+ {
2
+ "name": "@atlaskit/editor-plugin-autocomplete/src/pm-plugins/slow-lane-client",
3
+ "main": "../../../dist/cjs/pm-plugins/slow-lane-client.js",
4
+ "module": "../../../dist/esm/pm-plugins/slow-lane-client.js",
5
+ "module:es2019": "../../../dist/es2019/pm-plugins/slow-lane-client.js",
6
+ "sideEffects": false,
7
+ "types": "../../../dist/types/pm-plugins/slow-lane-client.d.ts",
8
+ "typesVersions": {
9
+ ">=4.5 <5.9": {
10
+ "*": [
11
+ "../../../dist/types-ts4.5/pm-plugins/slow-lane-client.d.ts"
12
+ ]
13
+ }
14
+ }
15
+ }
@@ -0,0 +1,222 @@
1
+ /**
2
+ * Slow Lane Client: Backend context encoding for autocomplete.
3
+ *
4
+ * Fires a BE request on word boundaries to encode document context.
5
+ * Expects the typeahead-encodings API format:
6
+ * Request: { text, session_id }
7
+ * Response: { semantic_vector: number[], lm_logits: Record<string, number> }
8
+ */
9
+
10
+ // ─── Types ───────────────────────────────────────────────────────────────────
11
+
12
+ /** Request payload for typeahead-encodings endpoint. */
13
+ export interface TypeaheadEncodingsRequest {
14
+ session_id: string;
15
+ text: string;
16
+ }
17
+
18
+ /** Response from typeahead-encodings endpoint. */
19
+ export interface TypeaheadEncodingsResponse {
20
+ lm_logits: Record<string, number>;
21
+ semantic_vector: number[];
22
+ }
23
+
24
+ // ─── Constants ───────────────────────────────────────────────────────────────
25
+
26
+ // eslint-disable-next-line require-unicode-regexp
27
+ const WORD_BOUNDARY_CHARS = /[\s.,;:!?]/;
28
+ const DEFAULT_DEBOUNCE_MS = 300;
29
+
30
+ /**
31
+ * Check if text ends with a word boundary character (space or punctuation).
32
+ */
33
+ export const isWordBoundary = (text: string): boolean => {
34
+ if (!text || text.length === 0) {
35
+ return false;
36
+ }
37
+ const lastChar = text[text.length - 1];
38
+ return WORD_BOUNDARY_CHARS.test(lastChar);
39
+ };
40
+
41
+ // ─── Slow Lane Client ────────────────────────────────────────────────────────
42
+
43
+ export interface SlowLaneClientConfig {
44
+ baseUrl: string;
45
+ debounceMs?: number;
46
+ endpoint?: string;
47
+ fetchFn?: typeof fetch;
48
+ onUpdate?: (opts: { hasLmLogits: boolean; hasVector: boolean; textLength: number }) => void;
49
+ productKey?: string;
50
+ sessionId?: string;
51
+ }
52
+
53
+ export const createSlowLaneClient = (
54
+ config: SlowLaneClientConfig,
55
+ ): {
56
+ getContextVector: () => Float32Array | null;
57
+ getLmLogits: () => Record<string, number> | null;
58
+ isWordBoundary: (text: string) => boolean;
59
+ setContextVector: (vector: Float32Array | null) => void;
60
+ setLmLogits: (logits: Record<string, number> | null) => void;
61
+ updateContext: (text: string) => void;
62
+ } => {
63
+ const {
64
+ baseUrl,
65
+ sessionId: configSessionId,
66
+ productKey = 'confluence',
67
+ endpoint = '/gateway/api/v1/autocomplete/typeahead-encodings',
68
+ debounceMs = DEFAULT_DEBOUNCE_MS,
69
+ fetchFn = fetch,
70
+ onUpdate,
71
+ } = config;
72
+
73
+ const sessionId = configSessionId ?? crypto.randomUUID();
74
+
75
+ const headers: Record<string, string> = {
76
+ 'Content-Type': 'application/json',
77
+ 'x-experience-id': 'confluence-smart-typeahead-encodings',
78
+ 'x-product': productKey,
79
+ };
80
+
81
+ let debounceTimer: ReturnType<typeof setTimeout> | null = null;
82
+ let lastRequestedText = '';
83
+ let storedContextVector: Float32Array | null = null;
84
+ let storedLmLogits: Record<string, number> | null = null;
85
+
86
+ const doUpdateContext = async (text: string): Promise<void> => {
87
+ if (!text || text.trim().length === 0) {
88
+ return;
89
+ }
90
+
91
+ // eslint-disable-next-line require-unicode-regexp
92
+ const url = `${baseUrl.replace(/\/$/, '')}${endpoint}`;
93
+ const payload: TypeaheadEncodingsRequest = {
94
+ text,
95
+ session_id: sessionId,
96
+ };
97
+
98
+ // Log the exact payload being sent so the comment/reply structure is visible
99
+ // eslint-disable-next-line no-console
100
+ console.groupCollapsed(
101
+ `%c[SlowLane] %cšŸ“¤ Sending context | ${text.length} chars`,
102
+ 'color: #9c27b0; font-weight: bold;',
103
+ 'color: inherit;',
104
+ );
105
+ text.split('\n').forEach((line, i, arr) => {
106
+ // eslint-disable-next-line no-console
107
+ console.log(` ${i === arr.length - 1 ? 'ā–¶' : ' '} ${line}`);
108
+ });
109
+ // eslint-disable-next-line no-console
110
+ console.groupEnd();
111
+
112
+ try {
113
+ const res = await fetchFn(url, {
114
+ method: 'POST',
115
+ headers,
116
+ body: JSON.stringify(payload),
117
+ });
118
+
119
+ if (!res.ok) {
120
+ storedContextVector = null;
121
+ storedLmLogits = null;
122
+ // eslint-disable-next-line no-console
123
+ console.log(
124
+ `%c[SlowLane] %cāŒ Request failed (${res.status})`,
125
+ 'color: #9c27b0; font-weight: bold;',
126
+ 'color: #f44336;',
127
+ );
128
+ return;
129
+ }
130
+
131
+ const data = (await res.json()) as TypeaheadEncodingsResponse;
132
+
133
+ if (data.semantic_vector && Array.isArray(data.semantic_vector)) {
134
+ storedContextVector = new Float32Array(data.semantic_vector);
135
+ } else {
136
+ storedContextVector = null;
137
+ }
138
+
139
+ if (data.lm_logits && typeof data.lm_logits === 'object') {
140
+ storedLmLogits = data.lm_logits;
141
+ } else {
142
+ storedLmLogits = null;
143
+ }
144
+
145
+ // Log what came back so it can be correlated with the next prediction group
146
+ // eslint-disable-next-line no-console
147
+ console.groupCollapsed(
148
+ `%c[SlowLane] %cšŸ“„ Response received`,
149
+ 'color: #9c27b0; font-weight: bold;',
150
+ 'color: inherit;',
151
+ );
152
+ // eslint-disable-next-line no-console
153
+ console.log(
154
+ storedContextVector
155
+ ? `āœ… semantic_vector: ${storedContextVector.length} dims`
156
+ : 'āŒ No semantic_vector',
157
+ );
158
+ // eslint-disable-next-line no-console
159
+ console.log(
160
+ storedLmLogits
161
+ ? `āœ… lm_logits: ${Object.keys(storedLmLogits).length} tokens`
162
+ : 'āŒ No lm_logits',
163
+ );
164
+ // eslint-disable-next-line no-console
165
+ console.groupEnd();
166
+
167
+ onUpdate?.({
168
+ textLength: text.length,
169
+ hasVector: storedContextVector !== null,
170
+ hasLmLogits: storedLmLogits !== null,
171
+ });
172
+ // eslint-disable-next-line no-unused-vars
173
+ } catch (e) {
174
+ storedContextVector = null;
175
+ storedLmLogits = null;
176
+ // eslint-disable-next-line no-console
177
+ console.log(
178
+ '%c[SlowLane] %cāŒ Network error — context cleared',
179
+ 'color: #9c27b0; font-weight: bold;',
180
+ 'color: #f44336;',
181
+ );
182
+ }
183
+ };
184
+
185
+ const updateContextDebounced = (text: string): void => {
186
+ if (debounceTimer) {
187
+ clearTimeout(debounceTimer);
188
+ }
189
+ lastRequestedText = text;
190
+ debounceTimer = setTimeout(() => {
191
+ debounceTimer = null;
192
+ doUpdateContext(lastRequestedText);
193
+ }, debounceMs);
194
+ };
195
+
196
+ return {
197
+ updateContext: updateContextDebounced,
198
+ getContextVector: () => storedContextVector,
199
+ getLmLogits: () => storedLmLogits,
200
+ setContextVector: (vector) => {
201
+ storedContextVector = vector;
202
+ },
203
+ setLmLogits: (logits) => {
204
+ storedLmLogits = logits;
205
+ },
206
+ isWordBoundary,
207
+ };
208
+ };
209
+
210
+ let defaultSlowLaneClient: ReturnType<typeof createSlowLaneClient> | null = null;
211
+
212
+ export const setDefaultSlowLaneClient = (
213
+ client: ReturnType<typeof createSlowLaneClient> | null,
214
+ ): void => {
215
+ defaultSlowLaneClient = client;
216
+ };
217
+
218
+ export const getStoredContextVector = (): Float32Array | null =>
219
+ defaultSlowLaneClient?.getContextVector() ?? null;
220
+
221
+ export const getStoredLmLogits = (): Record<string, number> | null =>
222
+ defaultSlowLaneClient?.getLmLogits() ?? null;
@@ -0,0 +1,15 @@
1
+ {
2
+ "name": "@atlaskit/editor-plugin-autocomplete/src/pm-plugins/text-predictor",
3
+ "main": "../../../dist/cjs/pm-plugins/text-predictor.js",
4
+ "module": "../../../dist/esm/pm-plugins/text-predictor.js",
5
+ "module:es2019": "../../../dist/es2019/pm-plugins/text-predictor.js",
6
+ "sideEffects": false,
7
+ "types": "../../../dist/types/pm-plugins/text-predictor.d.ts",
8
+ "typesVersions": {
9
+ ">=4.5 <5.9": {
10
+ "*": [
11
+ "../../../dist/types-ts4.5/pm-plugins/text-predictor.d.ts"
12
+ ]
13
+ }
14
+ }
15
+ }