@atlaskit/editor-plugin-autocomplete 2.4.0 → 2.5.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 +31 -0
- package/afm-cc/tsconfig.json +3 -0
- package/afm-products/tsconfig.json +3 -0
- package/dist/cjs/analytics/ufo.js +111 -0
- package/dist/cjs/pm-plugins/autocomplete-plugin.js +126 -64
- package/dist/cjs/pm-plugins/local-slow-lane-client.js +447 -0
- package/dist/cjs/pm-plugins/slow-lane-client.js +45 -16
- package/dist/cjs/pm-plugins/text-predictor.js +72 -40
- package/dist/es2019/analytics/ufo.js +110 -0
- package/dist/es2019/pm-plugins/autocomplete-plugin.js +132 -72
- package/dist/es2019/pm-plugins/local-slow-lane-client.js +355 -0
- package/dist/es2019/pm-plugins/slow-lane-client.js +29 -2
- package/dist/es2019/pm-plugins/text-predictor.js +48 -15
- package/dist/esm/analytics/ufo.js +105 -0
- package/dist/esm/pm-plugins/autocomplete-plugin.js +126 -64
- package/dist/esm/pm-plugins/local-slow-lane-client.js +439 -0
- package/dist/esm/pm-plugins/slow-lane-client.js +45 -16
- package/dist/esm/pm-plugins/text-predictor.js +73 -40
- package/dist/types/analytics/ufo.d.ts +38 -0
- package/dist/types/pm-plugins/autocomplete-plugin.d.ts +5 -0
- package/dist/types/pm-plugins/local-slow-lane-client.d.ts +102 -0
- package/dist/types-ts4.5/analytics/ufo.d.ts +38 -0
- package/dist/types-ts4.5/pm-plugins/autocomplete-plugin.d.ts +5 -0
- package/dist/types-ts4.5/pm-plugins/local-slow-lane-client.d.ts +102 -0
- package/package.json +7 -2
- package/src/analytics/ufo.ts +139 -0
- package/src/pm-plugins/autocomplete-plugin.ts +126 -64
- package/src/pm-plugins/local-slow-lane-client.ts +480 -0
- package/src/pm-plugins/slow-lane-client.ts +28 -2
- package/src/pm-plugins/text-predictor.ts +42 -12
- package/tsconfig.app.json +3 -0
|
@@ -24,6 +24,8 @@ function _arrayLikeToArray(r, a) { (null == a || a > r.length) && (a = r.length)
|
|
|
24
24
|
* via incrementSessionFreq(), called on word boundaries from the plugin.
|
|
25
25
|
*/
|
|
26
26
|
|
|
27
|
+
import { EXPERIENCE_NAME, failExp, startExp, succeedExp } from '../analytics/ufo';
|
|
28
|
+
|
|
27
29
|
// import bigramsData from './data/bigrams.json';
|
|
28
30
|
import l3VocabularyData from './data/l3_vocabulary.json';
|
|
29
31
|
import vocabularyData from './data/vocabulary_10k.json';
|
|
@@ -706,38 +708,46 @@ export var loadVectorsAsync = /*#__PURE__*/function () {
|
|
|
706
708
|
return _context.abrupt("return");
|
|
707
709
|
case 5:
|
|
708
710
|
vectorsLoadStarted = true;
|
|
709
|
-
|
|
710
|
-
_context.
|
|
711
|
+
startExp(EXPERIENCE_NAME.LOAD_VECTORS, 'singleton');
|
|
712
|
+
_context.prev = 7;
|
|
713
|
+
_context.next = 10;
|
|
711
714
|
return options.getBinaryUrl();
|
|
712
|
-
case
|
|
715
|
+
case 10:
|
|
713
716
|
url = _context.sent;
|
|
714
|
-
_context.next =
|
|
717
|
+
_context.next = 19;
|
|
715
718
|
break;
|
|
716
|
-
case
|
|
717
|
-
_context.prev =
|
|
718
|
-
_context.t0 = _context["catch"](
|
|
719
|
+
case 13:
|
|
720
|
+
_context.prev = 13;
|
|
721
|
+
_context.t0 = _context["catch"](7);
|
|
719
722
|
vectorsLoadStarted = false;
|
|
723
|
+
failExp(EXPERIENCE_NAME.LOAD_VECTORS, 'singleton', {
|
|
724
|
+
errorType: 'resolve_url'
|
|
725
|
+
});
|
|
720
726
|
// eslint-disable-next-line no-console
|
|
721
727
|
console.warn('[text-predictor] Failed to resolve vectors URL:', _context.t0);
|
|
722
728
|
return _context.abrupt("return");
|
|
723
|
-
case
|
|
724
|
-
_context.prev =
|
|
725
|
-
_context.next =
|
|
729
|
+
case 19:
|
|
730
|
+
_context.prev = 19;
|
|
731
|
+
_context.next = 22;
|
|
726
732
|
return fetch(url);
|
|
727
|
-
case
|
|
733
|
+
case 22:
|
|
728
734
|
res = _context.sent;
|
|
729
735
|
if (res.ok) {
|
|
730
|
-
_context.next =
|
|
736
|
+
_context.next = 28;
|
|
731
737
|
break;
|
|
732
738
|
}
|
|
733
739
|
vectorsLoadStarted = false;
|
|
740
|
+
failExp(EXPERIENCE_NAME.LOAD_VECTORS, 'singleton', {
|
|
741
|
+
status: res.status,
|
|
742
|
+
errorType: 'http_error'
|
|
743
|
+
});
|
|
734
744
|
// eslint-disable-next-line no-console
|
|
735
745
|
console.warn("[text-predictor] Failed to load vectors: ".concat(res.status));
|
|
736
746
|
return _context.abrupt("return");
|
|
737
|
-
case
|
|
738
|
-
_context.next =
|
|
747
|
+
case 28:
|
|
748
|
+
_context.next = 30;
|
|
739
749
|
return res.arrayBuffer();
|
|
740
|
-
case
|
|
750
|
+
case 30:
|
|
741
751
|
buffer = _context.sent;
|
|
742
752
|
float32 = new Float32Array(buffer);
|
|
743
753
|
wordIndex = wordIndexData;
|
|
@@ -748,6 +758,11 @@ export var loadVectorsAsync = /*#__PURE__*/function () {
|
|
|
748
758
|
wordIndex: wordIndex,
|
|
749
759
|
dim: dim
|
|
750
760
|
};
|
|
761
|
+
succeedExp(EXPERIENCE_NAME.LOAD_VECTORS, 'singleton', {
|
|
762
|
+
wordCount: nWords,
|
|
763
|
+
dim: dim,
|
|
764
|
+
sizeBytes: float32.byteLength
|
|
765
|
+
});
|
|
751
766
|
if (isAutocompleteDebugEnabled()) {
|
|
752
767
|
// eslint-disable-next-line no-console
|
|
753
768
|
console.log('[text-predictor] Vectors loaded:', {
|
|
@@ -756,19 +771,22 @@ export var loadVectorsAsync = /*#__PURE__*/function () {
|
|
|
756
771
|
sizeBytes: float32.byteLength
|
|
757
772
|
});
|
|
758
773
|
}
|
|
759
|
-
_context.next =
|
|
774
|
+
_context.next = 45;
|
|
760
775
|
break;
|
|
761
|
-
case
|
|
762
|
-
_context.prev =
|
|
763
|
-
_context.t1 = _context["catch"](
|
|
776
|
+
case 40:
|
|
777
|
+
_context.prev = 40;
|
|
778
|
+
_context.t1 = _context["catch"](19);
|
|
764
779
|
vectorsLoadStarted = false;
|
|
780
|
+
failExp(EXPERIENCE_NAME.LOAD_VECTORS, 'singleton', {
|
|
781
|
+
errorType: 'network'
|
|
782
|
+
});
|
|
765
783
|
// eslint-disable-next-line no-console
|
|
766
784
|
console.warn('[text-predictor] Failed to load vectors:', _context.t1);
|
|
767
|
-
case
|
|
785
|
+
case 45:
|
|
768
786
|
case "end":
|
|
769
787
|
return _context.stop();
|
|
770
788
|
}
|
|
771
|
-
}, _callee, null, [[
|
|
789
|
+
}, _callee, null, [[7, 13], [19, 40]]);
|
|
772
790
|
}));
|
|
773
791
|
return function loadVectorsAsync(_x) {
|
|
774
792
|
return _ref6.apply(this, arguments);
|
|
@@ -778,24 +796,39 @@ export var initVectors = function initVectors(store) {
|
|
|
778
796
|
vectorStore = store;
|
|
779
797
|
};
|
|
780
798
|
export var loadDefaultVocabulary = function loadDefaultVocabulary() {
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
799
|
+
if (isInitialized) {
|
|
800
|
+
return;
|
|
801
|
+
}
|
|
802
|
+
startExp(EXPERIENCE_NAME.LOAD_VOCABULARY, 'singleton');
|
|
803
|
+
try {
|
|
804
|
+
// 1. Load the Atlassian Domain (L2)
|
|
805
|
+
var data = vocabularyData;
|
|
806
|
+
var terms = Object.entries(data.words).map(function (_ref7) {
|
|
807
|
+
var _ref8 = _slicedToArray(_ref7, 2),
|
|
808
|
+
word = _ref8[0],
|
|
809
|
+
stats = _ref8[1];
|
|
810
|
+
return {
|
|
811
|
+
word: word,
|
|
812
|
+
freq: stats.freq,
|
|
813
|
+
docFreq: stats.doc_freq,
|
|
814
|
+
authorFreq: stats.author_freq
|
|
815
|
+
};
|
|
816
|
+
});
|
|
817
|
+
initVocabulary({
|
|
818
|
+
terms: terms
|
|
819
|
+
});
|
|
797
820
|
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
821
|
+
// 2. Load General English (L3)
|
|
822
|
+
var l3Words = l3VocabularyData;
|
|
823
|
+
initL3Vocabulary(l3Words);
|
|
824
|
+
succeedExp(EXPERIENCE_NAME.LOAD_VOCABULARY, 'singleton', {
|
|
825
|
+
l2WordCount: terms.length,
|
|
826
|
+
l3WordCount: l3Words.length
|
|
827
|
+
});
|
|
828
|
+
} catch (e) {
|
|
829
|
+
failExp(EXPERIENCE_NAME.LOAD_VOCABULARY, 'singleton', {
|
|
830
|
+
errorType: 'parse_error'
|
|
831
|
+
});
|
|
832
|
+
throw e;
|
|
833
|
+
}
|
|
801
834
|
};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* UFO experience tracking helpers for `@atlaskit/editor-plugin-autocomplete`.
|
|
3
|
+
*
|
|
4
|
+
* Pattern follows `packages/linking-platform/smart-card/src/state/analytics/ufoExperiences.ts`
|
|
5
|
+
* (ConcurrentExperience keyed per request/instance) plus a try/catch safety wrap
|
|
6
|
+
* inspired by `packages/editor/collab-provider/src/analytics/ufo.ts` so a UFO
|
|
7
|
+
* runtime error can never break the autocomplete feature.
|
|
8
|
+
*
|
|
9
|
+
* Experience names must be registered in DataPortal's `task` attribute before
|
|
10
|
+
* they can power FE Reliability SLOs:
|
|
11
|
+
* https://hello.atlassian.net/wiki/spaces/AA6/pages/3961393753
|
|
12
|
+
*/
|
|
13
|
+
import { type CustomData } from '@atlaskit/ufo';
|
|
14
|
+
/**
|
|
15
|
+
* Experience name strings are surfaced downstream by the UFO pipeline as
|
|
16
|
+
* `platform.fe.<type>.<platform.component>.<name>` (e.g.
|
|
17
|
+
* `platform.fe.operation.editor-plugin-autocomplete.slow-lane-fetch`), so
|
|
18
|
+
* names here should be short, dot-free, and must not repeat the component
|
|
19
|
+
* name. This matches the convention used by `@atlaskit/emoji`'s
|
|
20
|
+
* `ufoExperiences` map (e.g. `'emoji-rendered'`).
|
|
21
|
+
*/
|
|
22
|
+
/**
|
|
23
|
+
* Only experiences with meaningful async work and a real success/failure
|
|
24
|
+
* outcome belong on UFO (which measures latency and success rate for FE
|
|
25
|
+
* Reliability SLOs). Per-keystroke suggestion lifecycle counters (viewed /
|
|
26
|
+
* inserted / dismissed) are tracked via analytics-next instead — they would
|
|
27
|
+
* otherwise emit zero-duration events on every word boundary.
|
|
28
|
+
*/
|
|
29
|
+
export declare const EXPERIENCE_NAME: {
|
|
30
|
+
readonly SLOW_LANE_FETCH: "slow-lane-fetch";
|
|
31
|
+
readonly LOAD_VOCABULARY: "load-vocabulary";
|
|
32
|
+
readonly LOAD_VECTORS: "load-vectors";
|
|
33
|
+
};
|
|
34
|
+
export type AutocompleteExperienceName = (typeof EXPERIENCE_NAME)[keyof typeof EXPERIENCE_NAME];
|
|
35
|
+
export declare const startExp: (name: AutocompleteExperienceName, id: string, metadata?: CustomData) => void;
|
|
36
|
+
export declare const succeedExp: (name: AutocompleteExperienceName, id: string, metadata?: CustomData) => void;
|
|
37
|
+
export declare const failExp: (name: AutocompleteExperienceName, id: string, metadata?: CustomData) => void;
|
|
38
|
+
export declare const abortExp: (name: AutocompleteExperienceName, id: string, reason?: string) => void;
|
|
@@ -40,5 +40,10 @@ export interface AutocompletePluginOptions {
|
|
|
40
40
|
* Use this to serve vectors from a CDN or media service in production.
|
|
41
41
|
*/
|
|
42
42
|
getVectorsBinaryUrl?: () => Promise<string>;
|
|
43
|
+
/**
|
|
44
|
+
* When true, uses on-device inference via WebGPU (MLC WebLLM) instead of
|
|
45
|
+
* the network-based slow-lane backend. Defaults to false (network client).
|
|
46
|
+
*/
|
|
47
|
+
useLocalModel?: boolean;
|
|
43
48
|
}
|
|
44
49
|
export declare const createAutocompletePlugin: (options?: AutocompletePluginOptions, api?: ExtractInjectionAPI<AutocompletePlugin>) => SafePlugin<AutocompletePluginState>;
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local Slow Lane Client: On-device inference via @mlc-ai/web-llm.
|
|
3
|
+
*
|
|
4
|
+
* Drop-in replacement for the network-based slow-lane-client. Instead of
|
|
5
|
+
* calling a backend API, this client uses MLC WebLLM to run a small language
|
|
6
|
+
* model (SmolLM 135M) directly in the browser via WebGPU.
|
|
7
|
+
*
|
|
8
|
+
* ── Why main thread (no Web Worker)? ─────────────────────────────────────
|
|
9
|
+
* SmolLM 135M is small enough (~270 MB weights, 350-400 MB VRAM) that
|
|
10
|
+
* WebGPU inference on the main thread is production-viable:
|
|
11
|
+
*
|
|
12
|
+
* - WebGPU GPU compute is inherently async (doesn't block the main thread)
|
|
13
|
+
* - CPU overhead (tokenization + post-processing) is only 5-10 ms
|
|
14
|
+
* - Single forward pass latency is 50-150 ms — well within autocomplete
|
|
15
|
+
* expectations (~250 ms between word boundaries)
|
|
16
|
+
*
|
|
17
|
+
* This avoids all the complexity of Web Workers:
|
|
18
|
+
* - No CSP workarounds (blob URLs, inline scripts)
|
|
19
|
+
* - No bundler configuration (worker-plugin, import.meta.url)
|
|
20
|
+
* - No message passing protocol
|
|
21
|
+
* - Standard npm import — just works
|
|
22
|
+
*
|
|
23
|
+
* ── Interface ────────────────────────────────────────────────────────────
|
|
24
|
+
* Same shape as createSlowLaneClient so text-predictor.ts needs zero changes.
|
|
25
|
+
* The client exposes getContextVector() and getLmLogits() which are populated
|
|
26
|
+
* asynchronously after each updateContext() call.
|
|
27
|
+
*/
|
|
28
|
+
export interface LocalSlowLaneClientConfig {
|
|
29
|
+
/**
|
|
30
|
+
* Optional custom model registration for models not in web-llm's
|
|
31
|
+
* built-in list. When provided, the model is appended to the app
|
|
32
|
+
* config before engine creation.
|
|
33
|
+
*/
|
|
34
|
+
customModelConfig?: {
|
|
35
|
+
/** Context window size override (optional) */
|
|
36
|
+
contextWindowSize?: number;
|
|
37
|
+
/** HuggingFace URL to the model weights (e.g. "https://huggingface.co/HuggingFaceTB/smollm-135M-instruct-add-basics-q0f16-MLC") */
|
|
38
|
+
model: string;
|
|
39
|
+
/** URL to the compiled WASM library for this model architecture */
|
|
40
|
+
modelLib: string;
|
|
41
|
+
/** VRAM required in MB (optional, for resource planning) */
|
|
42
|
+
vramRequiredMB?: number;
|
|
43
|
+
};
|
|
44
|
+
/** Debounce interval in ms before sending context for inference. */
|
|
45
|
+
debounceMs?: number;
|
|
46
|
+
/**
|
|
47
|
+
* MLC model identifier.
|
|
48
|
+
* Defaults to the built-in SmolLM2-135M-Instruct-q0f16-MLC.
|
|
49
|
+
*
|
|
50
|
+
* To use a custom HuggingFace model, provide both `modelId` and
|
|
51
|
+
* `customModelConfig` with the model URL and WASM library URL.
|
|
52
|
+
*/
|
|
53
|
+
modelId?: string;
|
|
54
|
+
/** Callback fired with status messages (model loading progress, etc.). */
|
|
55
|
+
onStatus?: (message: string) => void;
|
|
56
|
+
/** Callback fired when inference returns new results. */
|
|
57
|
+
onUpdate?: (opts: {
|
|
58
|
+
hasLmLogits: boolean;
|
|
59
|
+
hasVector: boolean;
|
|
60
|
+
textLength: number;
|
|
61
|
+
}) => void;
|
|
62
|
+
}
|
|
63
|
+
export interface LocalSlowLaneClient {
|
|
64
|
+
/** Clean up resources. */
|
|
65
|
+
destroy: () => void;
|
|
66
|
+
getContextVector: () => Float32Array | null;
|
|
67
|
+
getLmLogits: () => Record<string, number> | null;
|
|
68
|
+
/** Whether the model is loaded and ready for inference. */
|
|
69
|
+
isReady: () => boolean;
|
|
70
|
+
isWordBoundary: (text: string) => boolean;
|
|
71
|
+
setContextVector: (vector: Float32Array | null) => void;
|
|
72
|
+
setLmLogits: (logits: Record<string, number> | null) => void;
|
|
73
|
+
updateContext: (text: string) => void;
|
|
74
|
+
}
|
|
75
|
+
export declare const LOCAL_MLC_MODEL_ID = "SmolLM2-135M-Instruct-q0f16-MLC";
|
|
76
|
+
/** HF root for the default weights (includes `tensor-cache.json` for WebLLM 0.2+). */
|
|
77
|
+
export declare const LOCAL_MLC_HF_MODEL_REPO = "https://huggingface.co/mlc-ai/SmolLM2-135M-Instruct-q0f16-MLC";
|
|
78
|
+
export declare const LOCAL_MLC_MODEL_LIB_WASM_NAME = "SmolLM2-135M-Instruct-q0f16-ctx4k_cs1k-webgpu.wasm";
|
|
79
|
+
/**
|
|
80
|
+
* Original target repo (add-basics fine-tune). **Not compatible with WebLLM 0.2.x** (no `tensor-cache.json`).
|
|
81
|
+
* @see module doc above
|
|
82
|
+
*/
|
|
83
|
+
export declare const HUGGINGFACE_TB_SMOLLM_ADD_BASICS_REPO = "https://huggingface.co/HuggingFaceTB/smollm-135M-instruct-add-basics-q0f16-MLC";
|
|
84
|
+
/**
|
|
85
|
+
* Create a local slow-lane client powered by MLC WebLLM.
|
|
86
|
+
*
|
|
87
|
+
* The engine is initialised lazily — model weights are downloaded (and cached
|
|
88
|
+
* in IndexedDB) on first use. Subsequent page loads skip the download.
|
|
89
|
+
*
|
|
90
|
+
* Usage:
|
|
91
|
+
* ```ts
|
|
92
|
+
* const client = createLocalSlowLaneClient({ debounceMs: 300 });
|
|
93
|
+
* // On word boundaries:
|
|
94
|
+
* client.updateContext(docText);
|
|
95
|
+
* // In scoring pipeline:
|
|
96
|
+
* const vec = client.getContextVector();
|
|
97
|
+
* const logits = client.getLmLogits();
|
|
98
|
+
* // On plugin teardown:
|
|
99
|
+
* client.destroy();
|
|
100
|
+
* ```
|
|
101
|
+
*/
|
|
102
|
+
export declare const createLocalSlowLaneClient: (config?: LocalSlowLaneClientConfig) => LocalSlowLaneClient;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* UFO experience tracking helpers for `@atlaskit/editor-plugin-autocomplete`.
|
|
3
|
+
*
|
|
4
|
+
* Pattern follows `packages/linking-platform/smart-card/src/state/analytics/ufoExperiences.ts`
|
|
5
|
+
* (ConcurrentExperience keyed per request/instance) plus a try/catch safety wrap
|
|
6
|
+
* inspired by `packages/editor/collab-provider/src/analytics/ufo.ts` so a UFO
|
|
7
|
+
* runtime error can never break the autocomplete feature.
|
|
8
|
+
*
|
|
9
|
+
* Experience names must be registered in DataPortal's `task` attribute before
|
|
10
|
+
* they can power FE Reliability SLOs:
|
|
11
|
+
* https://hello.atlassian.net/wiki/spaces/AA6/pages/3961393753
|
|
12
|
+
*/
|
|
13
|
+
import { type CustomData } from '@atlaskit/ufo';
|
|
14
|
+
/**
|
|
15
|
+
* Experience name strings are surfaced downstream by the UFO pipeline as
|
|
16
|
+
* `platform.fe.<type>.<platform.component>.<name>` (e.g.
|
|
17
|
+
* `platform.fe.operation.editor-plugin-autocomplete.slow-lane-fetch`), so
|
|
18
|
+
* names here should be short, dot-free, and must not repeat the component
|
|
19
|
+
* name. This matches the convention used by `@atlaskit/emoji`'s
|
|
20
|
+
* `ufoExperiences` map (e.g. `'emoji-rendered'`).
|
|
21
|
+
*/
|
|
22
|
+
/**
|
|
23
|
+
* Only experiences with meaningful async work and a real success/failure
|
|
24
|
+
* outcome belong on UFO (which measures latency and success rate for FE
|
|
25
|
+
* Reliability SLOs). Per-keystroke suggestion lifecycle counters (viewed /
|
|
26
|
+
* inserted / dismissed) are tracked via analytics-next instead — they would
|
|
27
|
+
* otherwise emit zero-duration events on every word boundary.
|
|
28
|
+
*/
|
|
29
|
+
export declare const EXPERIENCE_NAME: {
|
|
30
|
+
readonly SLOW_LANE_FETCH: "slow-lane-fetch";
|
|
31
|
+
readonly LOAD_VOCABULARY: "load-vocabulary";
|
|
32
|
+
readonly LOAD_VECTORS: "load-vectors";
|
|
33
|
+
};
|
|
34
|
+
export type AutocompleteExperienceName = (typeof EXPERIENCE_NAME)[keyof typeof EXPERIENCE_NAME];
|
|
35
|
+
export declare const startExp: (name: AutocompleteExperienceName, id: string, metadata?: CustomData) => void;
|
|
36
|
+
export declare const succeedExp: (name: AutocompleteExperienceName, id: string, metadata?: CustomData) => void;
|
|
37
|
+
export declare const failExp: (name: AutocompleteExperienceName, id: string, metadata?: CustomData) => void;
|
|
38
|
+
export declare const abortExp: (name: AutocompleteExperienceName, id: string, reason?: string) => void;
|
|
@@ -40,5 +40,10 @@ export interface AutocompletePluginOptions {
|
|
|
40
40
|
* Use this to serve vectors from a CDN or media service in production.
|
|
41
41
|
*/
|
|
42
42
|
getVectorsBinaryUrl?: () => Promise<string>;
|
|
43
|
+
/**
|
|
44
|
+
* When true, uses on-device inference via WebGPU (MLC WebLLM) instead of
|
|
45
|
+
* the network-based slow-lane backend. Defaults to false (network client).
|
|
46
|
+
*/
|
|
47
|
+
useLocalModel?: boolean;
|
|
43
48
|
}
|
|
44
49
|
export declare const createAutocompletePlugin: (options?: AutocompletePluginOptions, api?: ExtractInjectionAPI<AutocompletePlugin>) => SafePlugin<AutocompletePluginState>;
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local Slow Lane Client: On-device inference via @mlc-ai/web-llm.
|
|
3
|
+
*
|
|
4
|
+
* Drop-in replacement for the network-based slow-lane-client. Instead of
|
|
5
|
+
* calling a backend API, this client uses MLC WebLLM to run a small language
|
|
6
|
+
* model (SmolLM 135M) directly in the browser via WebGPU.
|
|
7
|
+
*
|
|
8
|
+
* ── Why main thread (no Web Worker)? ─────────────────────────────────────
|
|
9
|
+
* SmolLM 135M is small enough (~270 MB weights, 350-400 MB VRAM) that
|
|
10
|
+
* WebGPU inference on the main thread is production-viable:
|
|
11
|
+
*
|
|
12
|
+
* - WebGPU GPU compute is inherently async (doesn't block the main thread)
|
|
13
|
+
* - CPU overhead (tokenization + post-processing) is only 5-10 ms
|
|
14
|
+
* - Single forward pass latency is 50-150 ms — well within autocomplete
|
|
15
|
+
* expectations (~250 ms between word boundaries)
|
|
16
|
+
*
|
|
17
|
+
* This avoids all the complexity of Web Workers:
|
|
18
|
+
* - No CSP workarounds (blob URLs, inline scripts)
|
|
19
|
+
* - No bundler configuration (worker-plugin, import.meta.url)
|
|
20
|
+
* - No message passing protocol
|
|
21
|
+
* - Standard npm import — just works
|
|
22
|
+
*
|
|
23
|
+
* ── Interface ────────────────────────────────────────────────────────────
|
|
24
|
+
* Same shape as createSlowLaneClient so text-predictor.ts needs zero changes.
|
|
25
|
+
* The client exposes getContextVector() and getLmLogits() which are populated
|
|
26
|
+
* asynchronously after each updateContext() call.
|
|
27
|
+
*/
|
|
28
|
+
export interface LocalSlowLaneClientConfig {
|
|
29
|
+
/**
|
|
30
|
+
* Optional custom model registration for models not in web-llm's
|
|
31
|
+
* built-in list. When provided, the model is appended to the app
|
|
32
|
+
* config before engine creation.
|
|
33
|
+
*/
|
|
34
|
+
customModelConfig?: {
|
|
35
|
+
/** Context window size override (optional) */
|
|
36
|
+
contextWindowSize?: number;
|
|
37
|
+
/** HuggingFace URL to the model weights (e.g. "https://huggingface.co/HuggingFaceTB/smollm-135M-instruct-add-basics-q0f16-MLC") */
|
|
38
|
+
model: string;
|
|
39
|
+
/** URL to the compiled WASM library for this model architecture */
|
|
40
|
+
modelLib: string;
|
|
41
|
+
/** VRAM required in MB (optional, for resource planning) */
|
|
42
|
+
vramRequiredMB?: number;
|
|
43
|
+
};
|
|
44
|
+
/** Debounce interval in ms before sending context for inference. */
|
|
45
|
+
debounceMs?: number;
|
|
46
|
+
/**
|
|
47
|
+
* MLC model identifier.
|
|
48
|
+
* Defaults to the built-in SmolLM2-135M-Instruct-q0f16-MLC.
|
|
49
|
+
*
|
|
50
|
+
* To use a custom HuggingFace model, provide both `modelId` and
|
|
51
|
+
* `customModelConfig` with the model URL and WASM library URL.
|
|
52
|
+
*/
|
|
53
|
+
modelId?: string;
|
|
54
|
+
/** Callback fired with status messages (model loading progress, etc.). */
|
|
55
|
+
onStatus?: (message: string) => void;
|
|
56
|
+
/** Callback fired when inference returns new results. */
|
|
57
|
+
onUpdate?: (opts: {
|
|
58
|
+
hasLmLogits: boolean;
|
|
59
|
+
hasVector: boolean;
|
|
60
|
+
textLength: number;
|
|
61
|
+
}) => void;
|
|
62
|
+
}
|
|
63
|
+
export interface LocalSlowLaneClient {
|
|
64
|
+
/** Clean up resources. */
|
|
65
|
+
destroy: () => void;
|
|
66
|
+
getContextVector: () => Float32Array | null;
|
|
67
|
+
getLmLogits: () => Record<string, number> | null;
|
|
68
|
+
/** Whether the model is loaded and ready for inference. */
|
|
69
|
+
isReady: () => boolean;
|
|
70
|
+
isWordBoundary: (text: string) => boolean;
|
|
71
|
+
setContextVector: (vector: Float32Array | null) => void;
|
|
72
|
+
setLmLogits: (logits: Record<string, number> | null) => void;
|
|
73
|
+
updateContext: (text: string) => void;
|
|
74
|
+
}
|
|
75
|
+
export declare const LOCAL_MLC_MODEL_ID = "SmolLM2-135M-Instruct-q0f16-MLC";
|
|
76
|
+
/** HF root for the default weights (includes `tensor-cache.json` for WebLLM 0.2+). */
|
|
77
|
+
export declare const LOCAL_MLC_HF_MODEL_REPO = "https://huggingface.co/mlc-ai/SmolLM2-135M-Instruct-q0f16-MLC";
|
|
78
|
+
export declare const LOCAL_MLC_MODEL_LIB_WASM_NAME = "SmolLM2-135M-Instruct-q0f16-ctx4k_cs1k-webgpu.wasm";
|
|
79
|
+
/**
|
|
80
|
+
* Original target repo (add-basics fine-tune). **Not compatible with WebLLM 0.2.x** (no `tensor-cache.json`).
|
|
81
|
+
* @see module doc above
|
|
82
|
+
*/
|
|
83
|
+
export declare const HUGGINGFACE_TB_SMOLLM_ADD_BASICS_REPO = "https://huggingface.co/HuggingFaceTB/smollm-135M-instruct-add-basics-q0f16-MLC";
|
|
84
|
+
/**
|
|
85
|
+
* Create a local slow-lane client powered by MLC WebLLM.
|
|
86
|
+
*
|
|
87
|
+
* The engine is initialised lazily — model weights are downloaded (and cached
|
|
88
|
+
* in IndexedDB) on first use. Subsequent page loads skip the download.
|
|
89
|
+
*
|
|
90
|
+
* Usage:
|
|
91
|
+
* ```ts
|
|
92
|
+
* const client = createLocalSlowLaneClient({ debounceMs: 300 });
|
|
93
|
+
* // On word boundaries:
|
|
94
|
+
* client.updateContext(docText);
|
|
95
|
+
* // In scoring pipeline:
|
|
96
|
+
* const vec = client.getContextVector();
|
|
97
|
+
* const logits = client.getLmLogits();
|
|
98
|
+
* // On plugin teardown:
|
|
99
|
+
* client.destroy();
|
|
100
|
+
* ```
|
|
101
|
+
*/
|
|
102
|
+
export declare const createLocalSlowLaneClient: (config?: LocalSlowLaneClientConfig) => LocalSlowLaneClient;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@atlaskit/editor-plugin-autocomplete",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.5.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",
|
|
@@ -28,12 +28,14 @@
|
|
|
28
28
|
"atlaskit:src": "src/index.ts",
|
|
29
29
|
"dependencies": {
|
|
30
30
|
"@atlaskit/editor-prosemirror": "^7.3.0",
|
|
31
|
+
"@atlaskit/ufo": "^0.5.0",
|
|
31
32
|
"@babel/runtime": "^7.0.0",
|
|
33
|
+
"@mlc-ai/web-llm": "^0.2.78",
|
|
32
34
|
"compromise": "^14.15.0",
|
|
33
35
|
"wink-nlp": "^2.4.0"
|
|
34
36
|
},
|
|
35
37
|
"peerDependencies": {
|
|
36
|
-
"@atlaskit/editor-common": "^114.
|
|
38
|
+
"@atlaskit/editor-common": "^114.43.0",
|
|
37
39
|
"@atlaskit/editor-plugin-analytics": "^10.1.0",
|
|
38
40
|
"react": "^18.2.0"
|
|
39
41
|
},
|
|
@@ -49,5 +51,8 @@
|
|
|
49
51
|
"file-and-folder-level"
|
|
50
52
|
]
|
|
51
53
|
}
|
|
54
|
+
},
|
|
55
|
+
"devDependencies": {
|
|
56
|
+
"react": "^18.2.0"
|
|
52
57
|
}
|
|
53
58
|
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* UFO experience tracking helpers for `@atlaskit/editor-plugin-autocomplete`.
|
|
3
|
+
*
|
|
4
|
+
* Pattern follows `packages/linking-platform/smart-card/src/state/analytics/ufoExperiences.ts`
|
|
5
|
+
* (ConcurrentExperience keyed per request/instance) plus a try/catch safety wrap
|
|
6
|
+
* inspired by `packages/editor/collab-provider/src/analytics/ufo.ts` so a UFO
|
|
7
|
+
* runtime error can never break the autocomplete feature.
|
|
8
|
+
*
|
|
9
|
+
* Experience names must be registered in DataPortal's `task` attribute before
|
|
10
|
+
* they can power FE Reliability SLOs:
|
|
11
|
+
* https://hello.atlassian.net/wiki/spaces/AA6/pages/3961393753
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import {
|
|
15
|
+
ConcurrentExperience,
|
|
16
|
+
type CustomData,
|
|
17
|
+
ExperiencePerformanceTypes,
|
|
18
|
+
ExperienceTypes,
|
|
19
|
+
} from '@atlaskit/ufo';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Experience name strings are surfaced downstream by the UFO pipeline as
|
|
23
|
+
* `platform.fe.<type>.<platform.component>.<name>` (e.g.
|
|
24
|
+
* `platform.fe.operation.editor-plugin-autocomplete.slow-lane-fetch`), so
|
|
25
|
+
* names here should be short, dot-free, and must not repeat the component
|
|
26
|
+
* name. This matches the convention used by `@atlaskit/emoji`'s
|
|
27
|
+
* `ufoExperiences` map (e.g. `'emoji-rendered'`).
|
|
28
|
+
*/
|
|
29
|
+
/**
|
|
30
|
+
* Only experiences with meaningful async work and a real success/failure
|
|
31
|
+
* outcome belong on UFO (which measures latency and success rate for FE
|
|
32
|
+
* Reliability SLOs). Per-keystroke suggestion lifecycle counters (viewed /
|
|
33
|
+
* inserted / dismissed) are tracked via analytics-next instead — they would
|
|
34
|
+
* otherwise emit zero-duration events on every word boundary.
|
|
35
|
+
*/
|
|
36
|
+
export const EXPERIENCE_NAME = {
|
|
37
|
+
SLOW_LANE_FETCH: 'slow-lane-fetch',
|
|
38
|
+
LOAD_VOCABULARY: 'load-vocabulary',
|
|
39
|
+
LOAD_VECTORS: 'load-vectors',
|
|
40
|
+
} as const;
|
|
41
|
+
|
|
42
|
+
export type AutocompleteExperienceName =
|
|
43
|
+
(typeof EXPERIENCE_NAME)[keyof typeof EXPERIENCE_NAME];
|
|
44
|
+
|
|
45
|
+
const isUfoEnabled = (): boolean => !process?.env?.REACT_SSR;
|
|
46
|
+
|
|
47
|
+
const operationConfig = {
|
|
48
|
+
platform: { component: 'editor-plugin-autocomplete' },
|
|
49
|
+
type: ExperienceTypes.Operation,
|
|
50
|
+
performanceType: ExperiencePerformanceTypes.Custom,
|
|
51
|
+
performanceConfig: {
|
|
52
|
+
histogram: {
|
|
53
|
+
[ExperiencePerformanceTypes.Custom]: {
|
|
54
|
+
duration: '50_100_250_500_1000_2000_4000',
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const experiences: Record<AutocompleteExperienceName, ConcurrentExperience> = {
|
|
61
|
+
[EXPERIENCE_NAME.SLOW_LANE_FETCH]: new ConcurrentExperience(
|
|
62
|
+
EXPERIENCE_NAME.SLOW_LANE_FETCH,
|
|
63
|
+
operationConfig,
|
|
64
|
+
),
|
|
65
|
+
[EXPERIENCE_NAME.LOAD_VOCABULARY]: new ConcurrentExperience(
|
|
66
|
+
EXPERIENCE_NAME.LOAD_VOCABULARY,
|
|
67
|
+
operationConfig,
|
|
68
|
+
),
|
|
69
|
+
[EXPERIENCE_NAME.LOAD_VECTORS]: new ConcurrentExperience(
|
|
70
|
+
EXPERIENCE_NAME.LOAD_VECTORS,
|
|
71
|
+
operationConfig,
|
|
72
|
+
),
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
export const startExp = (
|
|
76
|
+
name: AutocompleteExperienceName,
|
|
77
|
+
id: string,
|
|
78
|
+
metadata?: CustomData,
|
|
79
|
+
): void => {
|
|
80
|
+
if (!isUfoEnabled()) {
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
try {
|
|
84
|
+
const instance = experiences[name].getInstance(id);
|
|
85
|
+
instance.start();
|
|
86
|
+
if (metadata) {
|
|
87
|
+
instance.addMetadata(metadata);
|
|
88
|
+
}
|
|
89
|
+
} catch {
|
|
90
|
+
// UFO errors must never break the plugin
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
export const succeedExp = (
|
|
95
|
+
name: AutocompleteExperienceName,
|
|
96
|
+
id: string,
|
|
97
|
+
metadata?: CustomData,
|
|
98
|
+
): void => {
|
|
99
|
+
if (!isUfoEnabled()) {
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
try {
|
|
103
|
+
experiences[name].getInstance(id).success({ metadata });
|
|
104
|
+
} catch {
|
|
105
|
+
// UFO errors must never break the plugin
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
export const failExp = (
|
|
110
|
+
name: AutocompleteExperienceName,
|
|
111
|
+
id: string,
|
|
112
|
+
metadata?: CustomData,
|
|
113
|
+
): void => {
|
|
114
|
+
if (!isUfoEnabled()) {
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
try {
|
|
118
|
+
experiences[name].getInstance(id).failure({ metadata });
|
|
119
|
+
} catch {
|
|
120
|
+
// UFO errors must never break the plugin
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
export const abortExp = (
|
|
125
|
+
name: AutocompleteExperienceName,
|
|
126
|
+
id: string,
|
|
127
|
+
reason?: string,
|
|
128
|
+
): void => {
|
|
129
|
+
if (!isUfoEnabled()) {
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
try {
|
|
133
|
+
experiences[name]
|
|
134
|
+
.getInstance(id)
|
|
135
|
+
.abort(reason ? { metadata: { reason } } : undefined);
|
|
136
|
+
} catch {
|
|
137
|
+
// UFO errors must never break the plugin
|
|
138
|
+
}
|
|
139
|
+
};
|