@atlaskit/editor-plugin-autocomplete 2.0.0 → 2.2.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 +22 -0
- package/afm-cc/tsconfig.json +3 -0
- package/afm-products/tsconfig.json +3 -0
- package/dist/cjs/autocompletePlugin.js +3 -2
- package/dist/cjs/pm-plugins/autocomplete-plugin.js +32 -3
- package/dist/cjs/pm-plugins/scoring-pipeline.js +43 -17
- package/dist/cjs/pm-plugins/text-predictor.js +27 -13
- package/dist/es2019/autocompletePlugin.js +3 -2
- package/dist/es2019/pm-plugins/autocomplete-plugin.js +32 -3
- package/dist/es2019/pm-plugins/scoring-pipeline.js +32 -14
- package/dist/es2019/pm-plugins/text-predictor.js +18 -8
- package/dist/esm/autocompletePlugin.js +3 -2
- package/dist/esm/pm-plugins/autocomplete-plugin.js +32 -3
- package/dist/esm/pm-plugins/scoring-pipeline.js +42 -17
- package/dist/esm/pm-plugins/text-predictor.js +28 -14
- package/dist/types/autocompletePluginType.d.ts +3 -1
- package/dist/types/pm-plugins/autocomplete-plugin.d.ts +3 -1
- package/dist/types/pm-plugins/scoring-pipeline.d.ts +10 -0
- package/dist/types-ts4.5/autocompletePluginType.d.ts +5 -1
- package/dist/types-ts4.5/pm-plugins/autocomplete-plugin.d.ts +3 -1
- package/dist/types-ts4.5/pm-plugins/scoring-pipeline.d.ts +10 -0
- package/package.json +3 -2
- package/src/autocompletePlugin.tsx +2 -2
- package/src/autocompletePluginType.ts +3 -1
- package/src/pm-plugins/autocomplete-plugin.ts +33 -3
- package/src/pm-plugins/scoring-pipeline.ts +41 -15
- package/src/pm-plugins/text-predictor.ts +26 -8
- package/tsconfig.app.json +3 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,27 @@
|
|
|
1
1
|
# @atlaskit/editor-plugin-autocomplete
|
|
2
2
|
|
|
3
|
+
## 2.2.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [`6eb5747f5ba37`](https://bitbucket.org/atlassian/atlassian-frontend-monorepo/commits/6eb5747f5ba37) -
|
|
8
|
+
Add SAR analytics for autocomplete plugin
|
|
9
|
+
|
|
10
|
+
### Patch Changes
|
|
11
|
+
|
|
12
|
+
- Updated dependencies
|
|
13
|
+
|
|
14
|
+
## 2.1.0
|
|
15
|
+
|
|
16
|
+
### Minor Changes
|
|
17
|
+
|
|
18
|
+
- [`6b36a63af0057`](https://bitbucket.org/atlassian/atlassian-frontend-monorepo/commits/6b36a63af0057) -
|
|
19
|
+
Updated scoring math for contextual typeahead autocomplete
|
|
20
|
+
|
|
21
|
+
### Patch Changes
|
|
22
|
+
|
|
23
|
+
- Updated dependencies
|
|
24
|
+
|
|
3
25
|
## 2.0.0
|
|
4
26
|
|
|
5
27
|
### Patch Changes
|
package/afm-cc/tsconfig.json
CHANGED
|
@@ -6,7 +6,8 @@ Object.defineProperty(exports, "__esModule", {
|
|
|
6
6
|
exports.autocompletePlugin = void 0;
|
|
7
7
|
var _autocompletePlugin = require("./pm-plugins/autocomplete-plugin");
|
|
8
8
|
var autocompletePlugin = exports.autocompletePlugin = function autocompletePlugin(_ref) {
|
|
9
|
-
var options = _ref.config
|
|
9
|
+
var options = _ref.config,
|
|
10
|
+
api = _ref.api;
|
|
10
11
|
return {
|
|
11
12
|
name: 'autocomplete',
|
|
12
13
|
getSharedState: function getSharedState(editorState) {
|
|
@@ -19,7 +20,7 @@ var autocompletePlugin = exports.autocompletePlugin = function autocompletePlugi
|
|
|
19
20
|
return [{
|
|
20
21
|
name: 'autocomplete',
|
|
21
22
|
plugin: function plugin() {
|
|
22
|
-
return (0, _autocompletePlugin.createAutocompletePlugin)(options);
|
|
23
|
+
return (0, _autocompletePlugin.createAutocompletePlugin)(options, api);
|
|
23
24
|
}
|
|
24
25
|
}];
|
|
25
26
|
}
|
|
@@ -6,6 +6,7 @@ Object.defineProperty(exports, "__esModule", {
|
|
|
6
6
|
});
|
|
7
7
|
exports.createAutocompletePlugin = exports.autocompletePluginKey = void 0;
|
|
8
8
|
var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty"));
|
|
9
|
+
var _analytics = require("@atlaskit/editor-common/analytics");
|
|
9
10
|
var _safePlugin = require("@atlaskit/editor-common/safe-plugin");
|
|
10
11
|
var _keymap = require("@atlaskit/editor-prosemirror/keymap");
|
|
11
12
|
var _state = require("@atlaskit/editor-prosemirror/state");
|
|
@@ -68,6 +69,7 @@ var setAutocompleteMeta = function setAutocompleteMeta(tr, meta) {
|
|
|
68
69
|
/**
|
|
69
70
|
* Apply a ghost text suggestion to the editor state.
|
|
70
71
|
*/
|
|
72
|
+
var lastShownGhostText = '';
|
|
71
73
|
var showGhostText = function showGhostText(view, text, position) {
|
|
72
74
|
var state = view.state,
|
|
73
75
|
dispatch = view.dispatch;
|
|
@@ -146,7 +148,7 @@ var buildSlowLaneText = function buildSlowLaneText(docText, context) {
|
|
|
146
148
|
lines.push("reply ".concat(nextReplyNumber, ": ").concat(docText));
|
|
147
149
|
return lines.join('\n');
|
|
148
150
|
};
|
|
149
|
-
var createAutocompletePlugin = exports.createAutocompletePlugin = function createAutocompletePlugin(options) {
|
|
151
|
+
var createAutocompletePlugin = exports.createAutocompletePlugin = function createAutocompletePlugin(options, api) {
|
|
150
152
|
var debounceTimer = null;
|
|
151
153
|
var hasIngestedPage = false;
|
|
152
154
|
var resolvedContext;
|
|
@@ -204,6 +206,15 @@ var createAutocompletePlugin = exports.createAutocompletePlugin = function creat
|
|
|
204
206
|
var prediction = (0, _textPredictor.predict)(textBefore);
|
|
205
207
|
if (prediction && prediction.length > 0) {
|
|
206
208
|
showGhostText(view, prediction, selection.from);
|
|
209
|
+
if (prediction !== lastShownGhostText) {
|
|
210
|
+
var _api$analytics;
|
|
211
|
+
lastShownGhostText = prediction;
|
|
212
|
+
api === null || api === void 0 || (_api$analytics = api.analytics) === null || _api$analytics === void 0 || _api$analytics.actions.fireAnalyticsEvent({
|
|
213
|
+
action: _analytics.ACTION.SUGGESTION_VIEWED,
|
|
214
|
+
actionSubject: _analytics.ACTION_SUBJECT.CONTEXTUAL_TYPEAHEAD,
|
|
215
|
+
eventType: _analytics.EVENT_TYPE.TRACK
|
|
216
|
+
});
|
|
217
|
+
}
|
|
207
218
|
}
|
|
208
219
|
}, DEBOUNCE_MS);
|
|
209
220
|
};
|
|
@@ -275,12 +286,30 @@ var createAutocompletePlugin = exports.createAutocompletePlugin = function creat
|
|
|
275
286
|
handleKeyDown: (0, _keymap.keydownHandler)({
|
|
276
287
|
Tab: function Tab(state, dispatch) {
|
|
277
288
|
var accepted = acceptGhostText(state, dispatch);
|
|
278
|
-
if (accepted)
|
|
289
|
+
if (accepted) {
|
|
290
|
+
var _api$analytics2;
|
|
291
|
+
justAccepted = true;
|
|
292
|
+
lastShownGhostText = '';
|
|
293
|
+
api === null || api === void 0 || (_api$analytics2 = api.analytics) === null || _api$analytics2 === void 0 || _api$analytics2.actions.fireAnalyticsEvent({
|
|
294
|
+
action: _analytics.ACTION.SUGGESTION_INSERTED,
|
|
295
|
+
actionSubject: _analytics.ACTION_SUBJECT.CONTEXTUAL_TYPEAHEAD,
|
|
296
|
+
eventType: _analytics.EVENT_TYPE.TRACK
|
|
297
|
+
});
|
|
298
|
+
}
|
|
279
299
|
return accepted;
|
|
280
300
|
},
|
|
281
301
|
ArrowRight: function ArrowRight(state, dispatch) {
|
|
282
302
|
var accepted = acceptGhostText(state, dispatch);
|
|
283
|
-
if (accepted)
|
|
303
|
+
if (accepted) {
|
|
304
|
+
var _api$analytics3;
|
|
305
|
+
justAccepted = true;
|
|
306
|
+
lastShownGhostText = '';
|
|
307
|
+
api === null || api === void 0 || (_api$analytics3 = api.analytics) === null || _api$analytics3 === void 0 || _api$analytics3.actions.fireAnalyticsEvent({
|
|
308
|
+
action: _analytics.ACTION.SUGGESTION_INSERTED,
|
|
309
|
+
actionSubject: _analytics.ACTION_SUBJECT.CONTEXTUAL_TYPEAHEAD,
|
|
310
|
+
eventType: _analytics.EVENT_TYPE.TRACK
|
|
311
|
+
});
|
|
312
|
+
}
|
|
284
313
|
return accepted;
|
|
285
314
|
},
|
|
286
315
|
Escape: function Escape(state, dispatch) {
|
|
@@ -4,6 +4,7 @@ var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefau
|
|
|
4
4
|
Object.defineProperty(exports, "__esModule", {
|
|
5
5
|
value: true
|
|
6
6
|
});
|
|
7
|
+
exports.STAGE2_WEIGHT = exports.STAGE1_WEIGHT = exports.MIN_STAGE1_SCORE = void 0;
|
|
7
8
|
exports.rankCandidates = rankCandidates;
|
|
8
9
|
var _slicedToArray2 = _interopRequireDefault(require("@babel/runtime/helpers/slicedToArray"));
|
|
9
10
|
var _combined_l2_l3_pos_tags = _interopRequireDefault(require("./data/combined_l2_l3_pos_tags.json"));
|
|
@@ -26,10 +27,15 @@ function _arrayLikeToArray(r, a) { (null == a || a > r.length) && (a = r.length)
|
|
|
26
27
|
var ALPHA = 0.5;
|
|
27
28
|
var BETA = 0.5;
|
|
28
29
|
var NEUTRAL_SCORE = 0.5;
|
|
29
|
-
var STAGE1_WEIGHT = 0.
|
|
30
|
-
var STAGE2_WEIGHT = 0.
|
|
31
|
-
var MIN_STAGE1_SCORE = 0.35;
|
|
30
|
+
var STAGE1_WEIGHT = exports.STAGE1_WEIGHT = 0.35;
|
|
31
|
+
var STAGE2_WEIGHT = exports.STAGE2_WEIGHT = 0.65;
|
|
32
|
+
var MIN_STAGE1_SCORE = exports.MIN_STAGE1_SCORE = 0.35;
|
|
32
33
|
var L1_SESSION_CAP = 1.2;
|
|
34
|
+
// Minimum prefix-payload max LM probability before Stage 2 activates.
|
|
35
|
+
// Below this threshold the LM signal is too weak to suppress Stage 1 — finalScore
|
|
36
|
+
// falls back to stage1Score directly. Prevents weak prefixes (e.g. "ins" → "instances"
|
|
37
|
+
// at 0.00024) from triggering re-ranking.
|
|
38
|
+
var LM_GATE_THRESHOLD = 0.0005;
|
|
33
39
|
|
|
34
40
|
// ─── Grammar Data (loaded once on import) ───────────────────
|
|
35
41
|
|
|
@@ -193,6 +199,7 @@ function getLmScore(word, lmLogits) {
|
|
|
193
199
|
// ─── Public API ─────────────────────────────────────────────
|
|
194
200
|
|
|
195
201
|
function rankCandidates(candidates, contextVector, getWordVector, lmLogits, maxTenantFreq, previousWord) {
|
|
202
|
+
var _grammarMeta$dropped;
|
|
196
203
|
// Stage 1
|
|
197
204
|
var stage1Results = candidates.map(function (candidate) {
|
|
198
205
|
var _scoreStage = scoreStage1(candidate, contextVector, getWordVector, maxTenantFreq),
|
|
@@ -206,33 +213,48 @@ function rankCandidates(candidates, contextVector, getWordVector, lmLogits, maxT
|
|
|
206
213
|
stage1Score: stage1Score
|
|
207
214
|
};
|
|
208
215
|
});
|
|
209
|
-
var stage1Survivors =
|
|
210
|
-
|
|
211
|
-
|
|
216
|
+
var stage1Survivors = [];
|
|
217
|
+
var stage1Rejected = [];
|
|
218
|
+
var _iterator3 = _createForOfIteratorHelper(stage1Results),
|
|
219
|
+
_step3;
|
|
220
|
+
try {
|
|
221
|
+
for (_iterator3.s(); !(_step3 = _iterator3.n()).done;) {
|
|
222
|
+
var entry = _step3.value;
|
|
223
|
+
if (entry.stage1Score >= MIN_STAGE1_SCORE) {
|
|
224
|
+
stage1Survivors.push(entry);
|
|
225
|
+
} else {
|
|
226
|
+
stage1Rejected.push(entry.candidate.word);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
212
229
|
|
|
213
|
-
|
|
230
|
+
// Grammar Filter
|
|
231
|
+
} catch (err) {
|
|
232
|
+
_iterator3.e(err);
|
|
233
|
+
} finally {
|
|
234
|
+
_iterator3.f();
|
|
235
|
+
}
|
|
214
236
|
var _applyGrammarFilter = applyGrammarFilter(stage1Survivors, previousWord),
|
|
215
237
|
filtered = _applyGrammarFilter.filtered,
|
|
216
238
|
grammarMeta = _applyGrammarFilter.grammarMeta;
|
|
217
239
|
|
|
218
240
|
// Stage 2 + final assembly
|
|
219
241
|
var lmMax = 0;
|
|
220
|
-
if (lmLogits
|
|
242
|
+
if (lmLogits) {
|
|
221
243
|
var values = Object.values(lmLogits);
|
|
222
|
-
lmMax = Math.max.apply(Math, values);
|
|
244
|
+
if (values.length > 0) lmMax = Math.max.apply(Math, values);
|
|
223
245
|
}
|
|
224
246
|
var scored = filtered.map(function (entry) {
|
|
225
247
|
var lmScore = 0;
|
|
226
248
|
var finalScore = entry.stage1Score;
|
|
227
|
-
if (lmLogits &&
|
|
249
|
+
if (lmLogits && lmMax >= LM_GATE_THRESHOLD) {
|
|
228
250
|
var rawLm = getLmScore(entry.candidate.word, lmLogits);
|
|
229
251
|
if (rawLm !== 0) {
|
|
230
|
-
// The word was in the top_k! Score it normally.
|
|
231
252
|
var logitDiff = Math.log(rawLm) - Math.log(lmMax);
|
|
232
253
|
lmScore = Math.exp(logitDiff);
|
|
233
|
-
} else {
|
|
234
|
-
lmScore = 0.05;
|
|
235
254
|
}
|
|
255
|
+
// Words absent from the prefix-filtered payload get lmScore = 0,
|
|
256
|
+
// not 0.05, so they don't outrank genuine LM predictions.
|
|
257
|
+
|
|
236
258
|
finalScore = STAGE1_WEIGHT * entry.stage1Score + STAGE2_WEIGHT * lmScore;
|
|
237
259
|
}
|
|
238
260
|
return {
|
|
@@ -244,13 +266,17 @@ function rankCandidates(candidates, contextVector, getWordVector, lmLogits, maxT
|
|
|
244
266
|
};
|
|
245
267
|
});
|
|
246
268
|
scored.sort(function (a, b) {
|
|
247
|
-
if (b.finalScore !== a.finalScore)
|
|
248
|
-
return b.finalScore - a.finalScore;
|
|
249
|
-
}
|
|
269
|
+
if (b.finalScore !== a.finalScore) return b.finalScore - a.finalScore;
|
|
250
270
|
return a.word.length - b.word.length;
|
|
251
271
|
});
|
|
252
272
|
return {
|
|
253
273
|
candidates: scored,
|
|
254
|
-
grammarMeta: grammarMeta
|
|
274
|
+
grammarMeta: grammarMeta,
|
|
275
|
+
pipelineDebug: {
|
|
276
|
+
initial: candidates.length,
|
|
277
|
+
stage1Rejected: stage1Rejected,
|
|
278
|
+
grammarRejected: (_grammarMeta$dropped = grammarMeta === null || grammarMeta === void 0 ? void 0 : grammarMeta.dropped) !== null && _grammarMeta$dropped !== void 0 ? _grammarMeta$dropped : [],
|
|
279
|
+
final: scored.length
|
|
280
|
+
}
|
|
255
281
|
};
|
|
256
282
|
}
|
|
@@ -573,11 +573,21 @@ var predict = exports.predict = function predict(textBefore) {
|
|
|
573
573
|
sessionFreq: node.sessionFreq
|
|
574
574
|
};
|
|
575
575
|
});
|
|
576
|
+
|
|
577
|
+
// Filter the LM payload to only words matching the current prefix so that
|
|
578
|
+
// lmMax in rankCandidates reflects prefix-relevant signal, not the global distribution.
|
|
579
|
+
var prefix = currentWord.toLowerCase();
|
|
580
|
+
var prefixLmLogits = lmLogits ? Object.fromEntries(Object.entries(lmLogits).filter(function (_ref4) {
|
|
581
|
+
var _ref5 = (0, _slicedToArray2.default)(_ref4, 1),
|
|
582
|
+
word = _ref5[0];
|
|
583
|
+
return word.startsWith(prefix);
|
|
584
|
+
})) : null;
|
|
576
585
|
var _rankCandidates = (0, _scoringPipeline.rankCandidates)(scoringCandidates, contextVector, function (w) {
|
|
577
586
|
return getWordVector(w);
|
|
578
|
-
},
|
|
587
|
+
}, prefixLmLogits, wordTrie.maxTenantFreq, previousWord),
|
|
579
588
|
ranked = _rankCandidates.candidates,
|
|
580
|
-
grammarMeta = _rankCandidates.grammarMeta
|
|
589
|
+
grammarMeta = _rankCandidates.grammarMeta,
|
|
590
|
+
pipelineDebug = _rankCandidates.pipelineDebug;
|
|
581
591
|
var best = ranked[0];
|
|
582
592
|
var suggestion = best && best.finalScore >= MIN_SCORE_THRESHOLD ? best.word.slice(currentWord.length) : null;
|
|
583
593
|
if (debugMode) {
|
|
@@ -624,7 +634,7 @@ var predict = exports.predict = function predict(textBefore) {
|
|
|
624
634
|
console.log('%cSlow Lane:', 'color: #888; font-style: italic;', vectorStatus, '|', logitsStatus);
|
|
625
635
|
|
|
626
636
|
// 4. Scoring formula active this prediction
|
|
627
|
-
var formulaLabel = lmLogits && Object.keys(lmLogits).length > 0 ?
|
|
637
|
+
var formulaLabel = lmLogits && Object.keys(lmLogits).length > 0 ? "Stage1(\xD7".concat(_scoringPipeline.STAGE1_WEIGHT, ") + LM(\xD7").concat(_scoringPipeline.STAGE2_WEIGHT, ")") : 'Stage1 only (no LM logits)';
|
|
628
638
|
// eslint-disable-next-line no-console
|
|
629
639
|
console.log('%cFormula:', 'color: #888; font-style: italic;', formulaLabel);
|
|
630
640
|
|
|
@@ -638,17 +648,21 @@ var predict = exports.predict = function predict(textBefore) {
|
|
|
638
648
|
}
|
|
639
649
|
}
|
|
640
650
|
|
|
641
|
-
// 6.
|
|
651
|
+
// 6. Pipeline funnel
|
|
652
|
+
// eslint-disable-next-line no-console
|
|
653
|
+
console.log("%c[Pipeline Funnel] %c\uD83D\uDCE5 In: ".concat(pipelineDebug.initial, " | \u274C Stage 1 (< ").concat(_scoringPipeline.MIN_STAGE1_SCORE, "): -").concat(pipelineDebug.stage1Rejected.length, " | \u274C Grammar: -").concat(pipelineDebug.grammarRejected.length, " | \u2705 Final: ").concat(pipelineDebug.final), 'color: #9c27b0; font-weight: bold;', 'color: inherit;');
|
|
654
|
+
|
|
655
|
+
// 7. Candidate table
|
|
642
656
|
if (ranked.length > 0) {
|
|
643
657
|
var lmCoverage = ranked.slice(0, 10).filter(function (r) {
|
|
644
|
-
return r.lmScore > 0
|
|
658
|
+
return r.lmScore > 0;
|
|
645
659
|
}).length;
|
|
646
660
|
// eslint-disable-next-line no-console
|
|
647
661
|
console.log("%cLM coverage: ".concat(lmCoverage, "/").concat(Math.min(ranked.length, 10), " candidates had real logit scores"), 'color: #888; font-style: italic;');
|
|
648
662
|
var tableData = ranked.slice(0, 10).map(function (r) {
|
|
649
663
|
var rawLogit = 'Not in Payload';
|
|
650
|
-
if (
|
|
651
|
-
var val =
|
|
664
|
+
if (prefixLmLogits) {
|
|
665
|
+
var val = prefixLmLogits[r.word.toLowerCase()];
|
|
652
666
|
if (val !== undefined) {
|
|
653
667
|
rawLogit = Number(val.toFixed(5));
|
|
654
668
|
}
|
|
@@ -694,7 +708,7 @@ var predict = exports.predict = function predict(textBefore) {
|
|
|
694
708
|
// ─── Data Loading ────────────────────────────────────────────────────────────
|
|
695
709
|
|
|
696
710
|
var loadVectorsAsync = exports.loadVectorsAsync = /*#__PURE__*/function () {
|
|
697
|
-
var
|
|
711
|
+
var _ref6 = (0, _asyncToGenerator2.default)( /*#__PURE__*/_regenerator.default.mark(function _callee(options) {
|
|
698
712
|
var url, res, buffer, float32, wordIndex, nWords, dim;
|
|
699
713
|
return _regenerator.default.wrap(function _callee$(_context) {
|
|
700
714
|
while (1) switch (_context.prev = _context.next) {
|
|
@@ -780,7 +794,7 @@ var loadVectorsAsync = exports.loadVectorsAsync = /*#__PURE__*/function () {
|
|
|
780
794
|
}, _callee, null, [[6, 12], [17, 37]]);
|
|
781
795
|
}));
|
|
782
796
|
return function loadVectorsAsync(_x) {
|
|
783
|
-
return
|
|
797
|
+
return _ref6.apply(this, arguments);
|
|
784
798
|
};
|
|
785
799
|
}();
|
|
786
800
|
var initVectors = exports.initVectors = function initVectors(store) {
|
|
@@ -789,10 +803,10 @@ var initVectors = exports.initVectors = function initVectors(store) {
|
|
|
789
803
|
var loadDefaultVocabulary = exports.loadDefaultVocabulary = function loadDefaultVocabulary() {
|
|
790
804
|
// 1. Load the Atlassian Domain (L2)
|
|
791
805
|
var data = _vocabulary_10k.default;
|
|
792
|
-
var terms = Object.entries(data.words).map(function (
|
|
793
|
-
var
|
|
794
|
-
word =
|
|
795
|
-
stats =
|
|
806
|
+
var terms = Object.entries(data.words).map(function (_ref7) {
|
|
807
|
+
var _ref8 = (0, _slicedToArray2.default)(_ref7, 2),
|
|
808
|
+
word = _ref8[0],
|
|
809
|
+
stats = _ref8[1];
|
|
796
810
|
return {
|
|
797
811
|
word: word,
|
|
798
812
|
freq: stats.freq,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { autocompletePluginKey, createAutocompletePlugin } from './pm-plugins/autocomplete-plugin';
|
|
2
2
|
export const autocompletePlugin = ({
|
|
3
|
-
config: options
|
|
3
|
+
config: options,
|
|
4
|
+
api
|
|
4
5
|
}) => {
|
|
5
6
|
return {
|
|
6
7
|
name: 'autocomplete',
|
|
@@ -13,7 +14,7 @@ export const autocompletePlugin = ({
|
|
|
13
14
|
pmPlugins() {
|
|
14
15
|
return [{
|
|
15
16
|
name: 'autocomplete',
|
|
16
|
-
plugin: () => createAutocompletePlugin(options)
|
|
17
|
+
plugin: () => createAutocompletePlugin(options, api)
|
|
17
18
|
}];
|
|
18
19
|
}
|
|
19
20
|
};
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ACTION, ACTION_SUBJECT, EVENT_TYPE } from '@atlaskit/editor-common/analytics';
|
|
1
2
|
import { SafePlugin } from '@atlaskit/editor-common/safe-plugin';
|
|
2
3
|
import { keydownHandler } from '@atlaskit/editor-prosemirror/keymap';
|
|
3
4
|
import { PluginKey } from '@atlaskit/editor-prosemirror/state';
|
|
@@ -58,6 +59,7 @@ const setAutocompleteMeta = (tr, meta) => {
|
|
|
58
59
|
/**
|
|
59
60
|
* Apply a ghost text suggestion to the editor state.
|
|
60
61
|
*/
|
|
62
|
+
let lastShownGhostText = '';
|
|
61
63
|
const showGhostText = (view, text, position) => {
|
|
62
64
|
const {
|
|
63
65
|
state,
|
|
@@ -140,7 +142,7 @@ const buildSlowLaneText = (docText, context) => {
|
|
|
140
142
|
lines.push(`reply ${nextReplyNumber}: ${docText}`);
|
|
141
143
|
return lines.join('\n');
|
|
142
144
|
};
|
|
143
|
-
export const createAutocompletePlugin = options => {
|
|
145
|
+
export const createAutocompletePlugin = (options, api) => {
|
|
144
146
|
let debounceTimer = null;
|
|
145
147
|
let hasIngestedPage = false;
|
|
146
148
|
let resolvedContext;
|
|
@@ -202,6 +204,15 @@ export const createAutocompletePlugin = options => {
|
|
|
202
204
|
const prediction = predict(textBefore);
|
|
203
205
|
if (prediction && prediction.length > 0) {
|
|
204
206
|
showGhostText(view, prediction, selection.from);
|
|
207
|
+
if (prediction !== lastShownGhostText) {
|
|
208
|
+
var _api$analytics;
|
|
209
|
+
lastShownGhostText = prediction;
|
|
210
|
+
api === null || api === void 0 ? void 0 : (_api$analytics = api.analytics) === null || _api$analytics === void 0 ? void 0 : _api$analytics.actions.fireAnalyticsEvent({
|
|
211
|
+
action: ACTION.SUGGESTION_VIEWED,
|
|
212
|
+
actionSubject: ACTION_SUBJECT.CONTEXTUAL_TYPEAHEAD,
|
|
213
|
+
eventType: EVENT_TYPE.TRACK
|
|
214
|
+
});
|
|
215
|
+
}
|
|
205
216
|
}
|
|
206
217
|
}, DEBOUNCE_MS);
|
|
207
218
|
};
|
|
@@ -276,12 +287,30 @@ export const createAutocompletePlugin = options => {
|
|
|
276
287
|
handleKeyDown: keydownHandler({
|
|
277
288
|
Tab: (state, dispatch) => {
|
|
278
289
|
const accepted = acceptGhostText(state, dispatch);
|
|
279
|
-
if (accepted)
|
|
290
|
+
if (accepted) {
|
|
291
|
+
var _api$analytics2;
|
|
292
|
+
justAccepted = true;
|
|
293
|
+
lastShownGhostText = '';
|
|
294
|
+
api === null || api === void 0 ? void 0 : (_api$analytics2 = api.analytics) === null || _api$analytics2 === void 0 ? void 0 : _api$analytics2.actions.fireAnalyticsEvent({
|
|
295
|
+
action: ACTION.SUGGESTION_INSERTED,
|
|
296
|
+
actionSubject: ACTION_SUBJECT.CONTEXTUAL_TYPEAHEAD,
|
|
297
|
+
eventType: EVENT_TYPE.TRACK
|
|
298
|
+
});
|
|
299
|
+
}
|
|
280
300
|
return accepted;
|
|
281
301
|
},
|
|
282
302
|
ArrowRight: (state, dispatch) => {
|
|
283
303
|
const accepted = acceptGhostText(state, dispatch);
|
|
284
|
-
if (accepted)
|
|
304
|
+
if (accepted) {
|
|
305
|
+
var _api$analytics3;
|
|
306
|
+
justAccepted = true;
|
|
307
|
+
lastShownGhostText = '';
|
|
308
|
+
api === null || api === void 0 ? void 0 : (_api$analytics3 = api.analytics) === null || _api$analytics3 === void 0 ? void 0 : _api$analytics3.actions.fireAnalyticsEvent({
|
|
309
|
+
action: ACTION.SUGGESTION_INSERTED,
|
|
310
|
+
actionSubject: ACTION_SUBJECT.CONTEXTUAL_TYPEAHEAD,
|
|
311
|
+
eventType: EVENT_TYPE.TRACK
|
|
312
|
+
});
|
|
313
|
+
}
|
|
285
314
|
return accepted;
|
|
286
315
|
},
|
|
287
316
|
Escape: (state, dispatch) => {
|
|
@@ -18,10 +18,15 @@ import grammarTransitionsData from './data/grammar_transitions_10k.json';
|
|
|
18
18
|
const ALPHA = 0.5;
|
|
19
19
|
const BETA = 0.5;
|
|
20
20
|
const NEUTRAL_SCORE = 0.5;
|
|
21
|
-
const STAGE1_WEIGHT = 0.
|
|
22
|
-
const STAGE2_WEIGHT = 0.
|
|
23
|
-
const MIN_STAGE1_SCORE = 0.35;
|
|
21
|
+
export const STAGE1_WEIGHT = 0.35;
|
|
22
|
+
export const STAGE2_WEIGHT = 0.65;
|
|
23
|
+
export const MIN_STAGE1_SCORE = 0.35;
|
|
24
24
|
const L1_SESSION_CAP = 1.2;
|
|
25
|
+
// Minimum prefix-payload max LM probability before Stage 2 activates.
|
|
26
|
+
// Below this threshold the LM signal is too weak to suppress Stage 1 — finalScore
|
|
27
|
+
// falls back to stage1Score directly. Prevents weak prefixes (e.g. "ins" → "instances"
|
|
28
|
+
// at 0.00024) from triggering re-ranking.
|
|
29
|
+
const LM_GATE_THRESHOLD = 0.0005;
|
|
25
30
|
|
|
26
31
|
// ─── Grammar Data (loaded once on import) ───────────────────
|
|
27
32
|
|
|
@@ -158,6 +163,7 @@ function getLmScore(word, lmLogits) {
|
|
|
158
163
|
// ─── Public API ─────────────────────────────────────────────
|
|
159
164
|
|
|
160
165
|
export function rankCandidates(candidates, contextVector, getWordVector, lmLogits, maxTenantFreq, previousWord) {
|
|
166
|
+
var _grammarMeta$dropped;
|
|
161
167
|
// Stage 1
|
|
162
168
|
const stage1Results = candidates.map(candidate => {
|
|
163
169
|
const {
|
|
@@ -172,7 +178,15 @@ export function rankCandidates(candidates, contextVector, getWordVector, lmLogit
|
|
|
172
178
|
stage1Score
|
|
173
179
|
};
|
|
174
180
|
});
|
|
175
|
-
const stage1Survivors =
|
|
181
|
+
const stage1Survivors = [];
|
|
182
|
+
const stage1Rejected = [];
|
|
183
|
+
for (const entry of stage1Results) {
|
|
184
|
+
if (entry.stage1Score >= MIN_STAGE1_SCORE) {
|
|
185
|
+
stage1Survivors.push(entry);
|
|
186
|
+
} else {
|
|
187
|
+
stage1Rejected.push(entry.candidate.word);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
176
190
|
|
|
177
191
|
// Grammar Filter
|
|
178
192
|
const {
|
|
@@ -182,22 +196,22 @@ export function rankCandidates(candidates, contextVector, getWordVector, lmLogit
|
|
|
182
196
|
|
|
183
197
|
// Stage 2 + final assembly
|
|
184
198
|
let lmMax = 0;
|
|
185
|
-
if (lmLogits
|
|
199
|
+
if (lmLogits) {
|
|
186
200
|
const values = Object.values(lmLogits);
|
|
187
|
-
lmMax = Math.max(...values);
|
|
201
|
+
if (values.length > 0) lmMax = Math.max(...values);
|
|
188
202
|
}
|
|
189
203
|
const scored = filtered.map(entry => {
|
|
190
204
|
let lmScore = 0;
|
|
191
205
|
let finalScore = entry.stage1Score;
|
|
192
|
-
if (lmLogits &&
|
|
206
|
+
if (lmLogits && lmMax >= LM_GATE_THRESHOLD) {
|
|
193
207
|
const rawLm = getLmScore(entry.candidate.word, lmLogits);
|
|
194
208
|
if (rawLm !== 0) {
|
|
195
|
-
// The word was in the top_k! Score it normally.
|
|
196
209
|
const logitDiff = Math.log(rawLm) - Math.log(lmMax);
|
|
197
210
|
lmScore = Math.exp(logitDiff);
|
|
198
|
-
} else {
|
|
199
|
-
lmScore = 0.05;
|
|
200
211
|
}
|
|
212
|
+
// Words absent from the prefix-filtered payload get lmScore = 0,
|
|
213
|
+
// not 0.05, so they don't outrank genuine LM predictions.
|
|
214
|
+
|
|
201
215
|
finalScore = STAGE1_WEIGHT * entry.stage1Score + STAGE2_WEIGHT * lmScore;
|
|
202
216
|
}
|
|
203
217
|
return {
|
|
@@ -209,13 +223,17 @@ export function rankCandidates(candidates, contextVector, getWordVector, lmLogit
|
|
|
209
223
|
};
|
|
210
224
|
});
|
|
211
225
|
scored.sort((a, b) => {
|
|
212
|
-
if (b.finalScore !== a.finalScore)
|
|
213
|
-
return b.finalScore - a.finalScore;
|
|
214
|
-
}
|
|
226
|
+
if (b.finalScore !== a.finalScore) return b.finalScore - a.finalScore;
|
|
215
227
|
return a.word.length - b.word.length;
|
|
216
228
|
});
|
|
217
229
|
return {
|
|
218
230
|
candidates: scored,
|
|
219
|
-
grammarMeta
|
|
231
|
+
grammarMeta,
|
|
232
|
+
pipelineDebug: {
|
|
233
|
+
initial: candidates.length,
|
|
234
|
+
stage1Rejected,
|
|
235
|
+
grammarRejected: (_grammarMeta$dropped = grammarMeta === null || grammarMeta === void 0 ? void 0 : grammarMeta.dropped) !== null && _grammarMeta$dropped !== void 0 ? _grammarMeta$dropped : [],
|
|
236
|
+
final: scored.length
|
|
237
|
+
}
|
|
220
238
|
};
|
|
221
239
|
}
|
|
@@ -21,7 +21,7 @@ import l3VocabularyData from './data/l3_vocabulary.json';
|
|
|
21
21
|
import vocabularyData from './data/vocabulary_10k.json';
|
|
22
22
|
import wordIndexData from './data/word_index_10k.json';
|
|
23
23
|
// import { rankCandidates, isGrammarAllowed } from './scoring-pipeline';
|
|
24
|
-
import { rankCandidates } from './scoring-pipeline';
|
|
24
|
+
import { rankCandidates, STAGE1_WEIGHT, STAGE2_WEIGHT, MIN_STAGE1_SCORE } from './scoring-pipeline';
|
|
25
25
|
import { getStoredContextVector, getStoredLmLogits } from './slow-lane-client';
|
|
26
26
|
|
|
27
27
|
// ─── Constants ───────────────────────────────────────────────────────────────
|
|
@@ -452,10 +452,16 @@ export const predict = textBefore => {
|
|
|
452
452
|
authorFreq: node.authorFreq,
|
|
453
453
|
sessionFreq: node.sessionFreq
|
|
454
454
|
}));
|
|
455
|
+
|
|
456
|
+
// Filter the LM payload to only words matching the current prefix so that
|
|
457
|
+
// lmMax in rankCandidates reflects prefix-relevant signal, not the global distribution.
|
|
458
|
+
const prefix = currentWord.toLowerCase();
|
|
459
|
+
const prefixLmLogits = lmLogits ? Object.fromEntries(Object.entries(lmLogits).filter(([word]) => word.startsWith(prefix))) : null;
|
|
455
460
|
const {
|
|
456
461
|
candidates: ranked,
|
|
457
|
-
grammarMeta
|
|
458
|
-
|
|
462
|
+
grammarMeta,
|
|
463
|
+
pipelineDebug
|
|
464
|
+
} = rankCandidates(scoringCandidates, contextVector, w => getWordVector(w), prefixLmLogits, wordTrie.maxTenantFreq, previousWord);
|
|
459
465
|
const best = ranked[0];
|
|
460
466
|
const suggestion = best && best.finalScore >= MIN_SCORE_THRESHOLD ? best.word.slice(currentWord.length) : null;
|
|
461
467
|
if (debugMode) {
|
|
@@ -500,7 +506,7 @@ export const predict = textBefore => {
|
|
|
500
506
|
console.log('%cSlow Lane:', 'color: #888; font-style: italic;', vectorStatus, '|', logitsStatus);
|
|
501
507
|
|
|
502
508
|
// 4. Scoring formula active this prediction
|
|
503
|
-
const formulaLabel = lmLogits && Object.keys(lmLogits).length > 0 ?
|
|
509
|
+
const formulaLabel = lmLogits && Object.keys(lmLogits).length > 0 ? `Stage1(×${STAGE1_WEIGHT}) + LM(×${STAGE2_WEIGHT})` : 'Stage1 only (no LM logits)';
|
|
504
510
|
// eslint-disable-next-line no-console
|
|
505
511
|
console.log('%cFormula:', 'color: #888; font-style: italic;', formulaLabel);
|
|
506
512
|
|
|
@@ -514,15 +520,19 @@ export const predict = textBefore => {
|
|
|
514
520
|
}
|
|
515
521
|
}
|
|
516
522
|
|
|
517
|
-
// 6.
|
|
523
|
+
// 6. Pipeline funnel
|
|
524
|
+
// eslint-disable-next-line no-console
|
|
525
|
+
console.log(`%c[Pipeline Funnel] %c📥 In: ${pipelineDebug.initial} | ❌ Stage 1 (< ${MIN_STAGE1_SCORE}): -${pipelineDebug.stage1Rejected.length} | ❌ Grammar: -${pipelineDebug.grammarRejected.length} | ✅ Final: ${pipelineDebug.final}`, 'color: #9c27b0; font-weight: bold;', 'color: inherit;');
|
|
526
|
+
|
|
527
|
+
// 7. Candidate table
|
|
518
528
|
if (ranked.length > 0) {
|
|
519
|
-
const lmCoverage = ranked.slice(0, 10).filter(r => r.lmScore > 0
|
|
529
|
+
const lmCoverage = ranked.slice(0, 10).filter(r => r.lmScore > 0).length;
|
|
520
530
|
// eslint-disable-next-line no-console
|
|
521
531
|
console.log(`%cLM coverage: ${lmCoverage}/${Math.min(ranked.length, 10)} candidates had real logit scores`, 'color: #888; font-style: italic;');
|
|
522
532
|
const tableData = ranked.slice(0, 10).map(r => {
|
|
523
533
|
let rawLogit = 'Not in Payload';
|
|
524
|
-
if (
|
|
525
|
-
const val =
|
|
534
|
+
if (prefixLmLogits) {
|
|
535
|
+
const val = prefixLmLogits[r.word.toLowerCase()];
|
|
526
536
|
if (val !== undefined) {
|
|
527
537
|
rawLogit = Number(val.toFixed(5));
|
|
528
538
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { autocompletePluginKey, createAutocompletePlugin } from './pm-plugins/autocomplete-plugin';
|
|
2
2
|
export var autocompletePlugin = function autocompletePlugin(_ref) {
|
|
3
|
-
var options = _ref.config
|
|
3
|
+
var options = _ref.config,
|
|
4
|
+
api = _ref.api;
|
|
4
5
|
return {
|
|
5
6
|
name: 'autocomplete',
|
|
6
7
|
getSharedState: function getSharedState(editorState) {
|
|
@@ -13,7 +14,7 @@ export var autocompletePlugin = function autocompletePlugin(_ref) {
|
|
|
13
14
|
return [{
|
|
14
15
|
name: 'autocomplete',
|
|
15
16
|
plugin: function plugin() {
|
|
16
|
-
return createAutocompletePlugin(options);
|
|
17
|
+
return createAutocompletePlugin(options, api);
|
|
17
18
|
}
|
|
18
19
|
}];
|
|
19
20
|
}
|