@atlaskit/editor-plugin-autocomplete 7.0.0 → 9.0.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 +86 -0
- package/compass.yml +2 -2
- package/dist/cjs/analytics/ufo.js +2 -1
- package/dist/cjs/pm-plugins/artifact-loader.js +135 -0
- package/dist/cjs/pm-plugins/artifacts-manifest.js +3 -0
- package/dist/cjs/pm-plugins/autocomplete-plugin.js +416 -129
- package/dist/cjs/pm-plugins/canonical-lm-scoring.js +371 -0
- package/dist/cjs/pm-plugins/debug-mode.js +90 -7
- package/dist/cjs/pm-plugins/ghost-text-decoration.js +12 -3
- package/dist/cjs/pm-plugins/local-slow-lane-client.js +1202 -453
- package/dist/cjs/pm-plugins/scoring-pipeline.js +164 -19
- package/dist/cjs/pm-plugins/slow-lane-client.js +100 -37
- package/dist/cjs/pm-plugins/text-predictor.js +1951 -273
- package/dist/es2019/analytics/ufo.js +2 -0
- package/dist/es2019/pm-plugins/artifact-loader.js +70 -0
- package/dist/es2019/pm-plugins/artifacts-manifest.js +3 -0
- package/dist/es2019/pm-plugins/autocomplete-plugin.js +425 -130
- package/dist/es2019/pm-plugins/canonical-lm-scoring.js +270 -0
- package/dist/es2019/pm-plugins/debug-mode.js +87 -7
- package/dist/es2019/pm-plugins/ghost-text-decoration.js +12 -3
- package/dist/es2019/pm-plugins/local-slow-lane-client.js +940 -326
- package/dist/es2019/pm-plugins/scoring-pipeline.js +137 -13
- package/dist/es2019/pm-plugins/slow-lane-client.js +94 -37
- package/dist/es2019/pm-plugins/text-predictor.js +1603 -199
- package/dist/esm/analytics/ufo.js +2 -1
- package/dist/esm/pm-plugins/artifact-loader.js +128 -0
- package/dist/esm/pm-plugins/artifacts-manifest.js +3 -0
- package/dist/esm/pm-plugins/autocomplete-plugin.js +419 -132
- package/dist/esm/pm-plugins/canonical-lm-scoring.js +364 -0
- package/dist/esm/pm-plugins/debug-mode.js +89 -6
- package/dist/esm/pm-plugins/ghost-text-decoration.js +12 -3
- package/dist/esm/pm-plugins/local-slow-lane-client.js +1200 -451
- package/dist/esm/pm-plugins/scoring-pipeline.js +163 -18
- package/dist/esm/pm-plugins/slow-lane-client.js +100 -37
- package/dist/esm/pm-plugins/text-predictor.js +1953 -277
- package/dist/types/analytics/ufo.d.ts +1 -0
- package/dist/types/pm-plugins/artifact-loader.d.ts +44 -0
- package/dist/types/pm-plugins/artifacts-manifest.d.ts +3 -0
- package/dist/types/pm-plugins/autocomplete-plugin.d.ts +39 -0
- package/dist/types/pm-plugins/canonical-lm-scoring.d.ts +158 -0
- package/dist/types/pm-plugins/debug-mode.d.ts +36 -1
- package/dist/types/pm-plugins/ghost-text-decoration.d.ts +5 -2
- package/dist/types/pm-plugins/local-slow-lane-client.d.ts +87 -54
- package/dist/types/pm-plugins/scoring-pipeline.d.ts +33 -1
- package/dist/types/pm-plugins/slow-lane-client.d.ts +51 -3
- package/dist/types/pm-plugins/text-predictor.d.ts +120 -2
- package/package.json +3 -3
- package/src/analytics/ufo.ts +5 -0
- package/src/pm-plugins/artifact-loader.ts +92 -0
- package/src/pm-plugins/artifacts-manifest.ts +3 -0
- package/src/pm-plugins/autocomplete-plugin.ts +517 -133
- package/src/pm-plugins/canonical-lm-scoring.ts +412 -0
- package/src/pm-plugins/debug-mode.ts +109 -7
- package/src/pm-plugins/ghost-text-decoration.ts +13 -4
- package/src/pm-plugins/local-slow-lane-client.ts +1302 -386
- package/src/pm-plugins/scoring-pipeline.ts +167 -17
- package/src/pm-plugins/slow-lane-client.ts +147 -62
- package/src/pm-plugins/text-predictor.ts +2006 -253
- package/CANONICAL_FIX__DO_NOT_USE_ME_A/package.json +0 -8
- package/CANONICAL_FIX__DO_NOT_USE_ME_B/package.json +0 -8
- package/CANONICAL_FIX__DO_NOT_USE_ME_C/package.json +0 -8
- package/dist/cjs/entry-points/src-pm-plugins-autocomplete-plugin.js +0 -18
- package/dist/cjs/entry-points/src-pm-plugins-slow-lane-client.js +0 -36
- package/dist/cjs/entry-points/src-pm-plugins-text-predictor.js +0 -66
- package/dist/es2019/entry-points/src-pm-plugins-autocomplete-plugin.js +0 -2
- package/dist/es2019/entry-points/src-pm-plugins-slow-lane-client.js +0 -2
- package/dist/es2019/entry-points/src-pm-plugins-text-predictor.js +0 -2
- package/dist/esm/entry-points/src-pm-plugins-autocomplete-plugin.js +0 -2
- package/dist/esm/entry-points/src-pm-plugins-slow-lane-client.js +0 -2
- package/dist/esm/entry-points/src-pm-plugins-text-predictor.js +0 -2
- package/dist/types/entry-points/src-pm-plugins-autocomplete-plugin.d.ts +0 -2
- package/dist/types/entry-points/src-pm-plugins-slow-lane-client.d.ts +0 -2
- package/dist/types/entry-points/src-pm-plugins-text-predictor.d.ts +0 -2
- package/scripts/gen_first_token_to_words.py +0 -170
- package/src/entry-points/src-pm-plugins-autocomplete-plugin.ts +0 -7
- package/src/entry-points/src-pm-plugins-slow-lane-client.ts +0 -13
- package/src/entry-points/src-pm-plugins-text-predictor.ts +0 -14
- package/src/pm-plugins/autocomplete-plugin/package.json +0 -8
- package/src/pm-plugins/slow-lane-client/package.json +0 -8
- package/src/pm-plugins/text-predictor/package.json +0 -8
|
@@ -0,0 +1,412 @@
|
|
|
1
|
+
import type { LogitProcessor } from '@mlc-ai/web-llm';
|
|
2
|
+
|
|
3
|
+
export type SeparatorKind = 'whitespace' | 'document-start' | 'non-space';
|
|
4
|
+
|
|
5
|
+
export type CanonicalLmEvidenceSource =
|
|
6
|
+
| 'canonical-first-token'
|
|
7
|
+
| 'canonical-full-surface'
|
|
8
|
+
| 'network-logit';
|
|
9
|
+
|
|
10
|
+
export interface BoundaryLmState {
|
|
11
|
+
contextKey: string;
|
|
12
|
+
prompt: string;
|
|
13
|
+
rawLogits: Float32Array;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface SurfaceScore {
|
|
17
|
+
contextKey: string;
|
|
18
|
+
meanTokenLogProbability: number;
|
|
19
|
+
surface: string;
|
|
20
|
+
tokenCount: number;
|
|
21
|
+
totalLogProbability: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface CanonicalCandidateContext {
|
|
25
|
+
canonicalTokenIds: number[] | null;
|
|
26
|
+
contextBeforeSurface: string;
|
|
27
|
+
contextKey: string;
|
|
28
|
+
separatorKind: SeparatorKind;
|
|
29
|
+
surfaceStart: number;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface CanonicalBoundaryContext {
|
|
33
|
+
contextKey: string;
|
|
34
|
+
prompt: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface CanonicalLmEvidence {
|
|
38
|
+
contextKey: string;
|
|
39
|
+
logProbability?: number;
|
|
40
|
+
meanTokenLogProbability?: number;
|
|
41
|
+
rawLogit?: number;
|
|
42
|
+
score: number;
|
|
43
|
+
separatorKind: 'whitespace';
|
|
44
|
+
source: CanonicalLmEvidenceSource;
|
|
45
|
+
totalLogProbability?: number;
|
|
46
|
+
totalSurfaceCharCount: number;
|
|
47
|
+
totalTokenCount: number;
|
|
48
|
+
verifiedCharCount: number;
|
|
49
|
+
verifiedTokenCount: number;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface BoundaryPrimeRequest {
|
|
53
|
+
contextKey: string;
|
|
54
|
+
familyKey: string;
|
|
55
|
+
priority: number;
|
|
56
|
+
prompt: string;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface SurfaceScoreRequest {
|
|
60
|
+
candidates: Array<{
|
|
61
|
+
rankHint?: number;
|
|
62
|
+
surface: string;
|
|
63
|
+
tokenIds: number[];
|
|
64
|
+
}>;
|
|
65
|
+
contextKey: string;
|
|
66
|
+
familyKey: string;
|
|
67
|
+
prompt: string;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface ProgressiveSurfaceEvidence {
|
|
71
|
+
meanTokenLogProbabilityUpperBound: number;
|
|
72
|
+
scoredTokenCount: number;
|
|
73
|
+
totalLogProbability: number;
|
|
74
|
+
totalTokenCount: number;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export interface TokenPrefixExpansion {
|
|
78
|
+
contextKey: string;
|
|
79
|
+
rawNextTokenLogits: Float32Array;
|
|
80
|
+
tokenPrefix: number[];
|
|
81
|
+
totalLogProbability: number;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export interface TokenPrefixGroup {
|
|
85
|
+
surfaces: string[];
|
|
86
|
+
tokenPrefix: number[];
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
interface CanonicalTokenTrieNode {
|
|
90
|
+
children: Map<number, CanonicalTokenTrieNode>;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const createTokenTrieNode = (): CanonicalTokenTrieNode => ({
|
|
94
|
+
children: new Map(),
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Token trie for the producer's canonical leading-space token sequences.
|
|
99
|
+
* It lets the scorer expand a shared token prefix once for every surface below
|
|
100
|
+
* that node (`root cause`, `root directory`, ...), rather than guessing two
|
|
101
|
+
* arbitrary full surfaces from a broad one-character string prefix.
|
|
102
|
+
*/
|
|
103
|
+
export class CanonicalSurfaceTokenTrie {
|
|
104
|
+
private readonly root = createTokenTrieNode();
|
|
105
|
+
private readonly tokenIdsBySurface = new Map<string, number[]>();
|
|
106
|
+
|
|
107
|
+
constructor(entries: Iterable<readonly [string, number[]]> = []) {
|
|
108
|
+
for (const [surface, tokenIds] of entries) {
|
|
109
|
+
this.insert(surface, tokenIds);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
insert(surface: string, tokenIds: number[]): void {
|
|
114
|
+
if (tokenIds.length === 0) {
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
const normalizedSurface = surface.toLowerCase();
|
|
118
|
+
this.tokenIdsBySurface.set(normalizedSurface, [...tokenIds]);
|
|
119
|
+
let node = this.root;
|
|
120
|
+
for (const tokenId of tokenIds) {
|
|
121
|
+
let child = node.children.get(tokenId);
|
|
122
|
+
if (!child) {
|
|
123
|
+
child = createTokenTrieNode();
|
|
124
|
+
node.children.set(tokenId, child);
|
|
125
|
+
}
|
|
126
|
+
node = child;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
getTokenIds(surface: string): number[] | null {
|
|
131
|
+
const tokenIds = this.tokenIdsBySurface.get(surface.toLowerCase());
|
|
132
|
+
return tokenIds ? [...tokenIds] : null;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
groupByScoredPrefix(
|
|
136
|
+
candidates: Array<{ scoredTokenCount: number; surface: string; tokenIds: number[] }>,
|
|
137
|
+
): TokenPrefixGroup[] {
|
|
138
|
+
const groups = new Map<string, TokenPrefixGroup>();
|
|
139
|
+
for (const candidate of candidates) {
|
|
140
|
+
if (
|
|
141
|
+
candidate.scoredTokenCount <= 0 ||
|
|
142
|
+
candidate.scoredTokenCount >= candidate.tokenIds.length
|
|
143
|
+
) {
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
const tokenPrefix = candidate.tokenIds.slice(0, candidate.scoredTokenCount);
|
|
147
|
+
let node: CanonicalTokenTrieNode | undefined = this.root;
|
|
148
|
+
for (const tokenId of tokenPrefix) {
|
|
149
|
+
node = node.children.get(tokenId);
|
|
150
|
+
if (!node) {
|
|
151
|
+
break;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
if (!node) {
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
const key = tokenPrefix.join(',');
|
|
158
|
+
const group = groups.get(key) ?? { surfaces: [], tokenPrefix };
|
|
159
|
+
group.surfaces.push(candidate.surface);
|
|
160
|
+
groups.set(key, group);
|
|
161
|
+
}
|
|
162
|
+
return Array.from(groups.values());
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const WHITESPACE_REGEX = /\s/u;
|
|
167
|
+
|
|
168
|
+
const makeContextKey = (prompt: string, separatorKind: SeparatorKind): string =>
|
|
169
|
+
`${separatorKind}\u0000${prompt}`;
|
|
170
|
+
|
|
171
|
+
/** The part of a candidate's context that depends only on where its surface starts. */
|
|
172
|
+
interface SurfacePositionContext {
|
|
173
|
+
contextBeforeSurface: string;
|
|
174
|
+
contextKey: string;
|
|
175
|
+
separatorKind: SeparatorKind;
|
|
176
|
+
surfaceStart: number;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Per-pass store for the position-derived half of a candidate's context.
|
|
181
|
+
*
|
|
182
|
+
* Every unigram candidate for a keystroke shares one surface start, and the
|
|
183
|
+
* phrase candidates share a handful more, so without this the same slice,
|
|
184
|
+
* `trimEnd` and context key are rebuilt for each of the couple of hundred
|
|
185
|
+
* candidates. Valid only for a single `textBeforeCursor`.
|
|
186
|
+
*/
|
|
187
|
+
export type CanonicalContextPositionCache = Map<number, SurfacePositionContext>;
|
|
188
|
+
|
|
189
|
+
export const createCanonicalContextPositionCache = (): CanonicalContextPositionCache => new Map();
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Derive the causal prompt and artifact eligibility from the exact, untrimmed
|
|
193
|
+
* pre-cursor text. The shipped artifact contains only tokenizations of
|
|
194
|
+
* `" " + surface`, so it is valid only when the surface is actually preceded
|
|
195
|
+
* by whitespace.
|
|
196
|
+
*/
|
|
197
|
+
export const deriveCanonicalCandidateContext = (
|
|
198
|
+
textBeforeCursor: string,
|
|
199
|
+
matchedPrefixLen: number,
|
|
200
|
+
surface: string,
|
|
201
|
+
getTokenIds: (surface: string) => number[] | null,
|
|
202
|
+
surfaceStartOverride?: number,
|
|
203
|
+
positionCache?: CanonicalContextPositionCache,
|
|
204
|
+
): CanonicalCandidateContext => {
|
|
205
|
+
const derivedSurfaceStart = textBeforeCursor.length - matchedPrefixLen;
|
|
206
|
+
const surfaceStart = Math.max(
|
|
207
|
+
0,
|
|
208
|
+
Math.min(textBeforeCursor.length, surfaceStartOverride ?? derivedSurfaceStart),
|
|
209
|
+
);
|
|
210
|
+
|
|
211
|
+
let position = positionCache?.get(surfaceStart);
|
|
212
|
+
if (position === undefined) {
|
|
213
|
+
const precedingChar = surfaceStart > 0 ? textBeforeCursor[surfaceStart - 1] : null;
|
|
214
|
+
const separatorKind: SeparatorKind =
|
|
215
|
+
precedingChar === null
|
|
216
|
+
? 'document-start'
|
|
217
|
+
: WHITESPACE_REGEX.test(precedingChar)
|
|
218
|
+
? 'whitespace'
|
|
219
|
+
: 'non-space';
|
|
220
|
+
const contextBeforeSurface = textBeforeCursor.slice(0, surfaceStart).trimEnd();
|
|
221
|
+
position = {
|
|
222
|
+
contextBeforeSurface,
|
|
223
|
+
contextKey: makeContextKey(contextBeforeSurface, separatorKind),
|
|
224
|
+
separatorKind,
|
|
225
|
+
surfaceStart,
|
|
226
|
+
};
|
|
227
|
+
positionCache?.set(surfaceStart, position);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return {
|
|
231
|
+
...position,
|
|
232
|
+
canonicalTokenIds: position.separatorKind === 'whitespace' ? getTokenIds(surface) : null,
|
|
233
|
+
};
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Derive the next surface's leading-space context at an actual word boundary.
|
|
238
|
+
* This lets the local LM start its one-token prime on the space keystroke,
|
|
239
|
+
* before the user has typed the first character of the next word.
|
|
240
|
+
*/
|
|
241
|
+
export const deriveWhitespaceBoundaryContext = (
|
|
242
|
+
textBeforeCursor: string,
|
|
243
|
+
): CanonicalBoundaryContext | null => {
|
|
244
|
+
if (textBeforeCursor.length === 0 || !WHITESPACE_REGEX.test(textBeforeCursor.at(-1) ?? '')) {
|
|
245
|
+
return null;
|
|
246
|
+
}
|
|
247
|
+
const prompt = textBeforeCursor.trimEnd();
|
|
248
|
+
if (prompt.length === 0) {
|
|
249
|
+
return null;
|
|
250
|
+
}
|
|
251
|
+
return {
|
|
252
|
+
contextKey: makeContextKey(prompt, 'whitespace'),
|
|
253
|
+
prompt,
|
|
254
|
+
};
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Return one prime per exact context, ordered from the latest surface start
|
|
259
|
+
* (normally the unigram window) to wider phrase windows.
|
|
260
|
+
*/
|
|
261
|
+
export const selectBoundaryPrimeRequests = (
|
|
262
|
+
familyKey: string,
|
|
263
|
+
candidates: Array<CanonicalCandidateContext>,
|
|
264
|
+
maxPrimes: number,
|
|
265
|
+
): BoundaryPrimeRequest[] => {
|
|
266
|
+
const byContext = new Map<string, CanonicalCandidateContext>();
|
|
267
|
+
for (const candidate of candidates) {
|
|
268
|
+
if (candidate.canonicalTokenIds === null) {
|
|
269
|
+
continue;
|
|
270
|
+
}
|
|
271
|
+
const current = byContext.get(candidate.contextKey);
|
|
272
|
+
if (!current || candidate.surfaceStart > current.surfaceStart) {
|
|
273
|
+
byContext.set(candidate.contextKey, candidate);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
return Array.from(byContext.values())
|
|
278
|
+
.sort((a, b) => b.surfaceStart - a.surfaceStart)
|
|
279
|
+
.slice(0, Math.max(0, maxPrimes))
|
|
280
|
+
.map((candidate, priority) => ({
|
|
281
|
+
familyKey,
|
|
282
|
+
contextKey: candidate.contextKey,
|
|
283
|
+
prompt: candidate.contextBeforeSurface,
|
|
284
|
+
priority,
|
|
285
|
+
}));
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* `log(sum(exp(values)))`, shifted by the maximum so the sum cannot overflow or
|
|
290
|
+
* underflow to zero for the sequence log-likelihoods this is called with.
|
|
291
|
+
*
|
|
292
|
+
* Returns `-Infinity` for an empty or wholly non-finite input, which makes
|
|
293
|
+
* `exp(value - logSumExp(values))` evaluate to zero rather than `NaN`.
|
|
294
|
+
*/
|
|
295
|
+
export const logSumExp = (values: readonly number[]): number => {
|
|
296
|
+
let max = -Infinity;
|
|
297
|
+
for (const value of values) {
|
|
298
|
+
if (Number.isFinite(value) && value > max) {
|
|
299
|
+
max = value;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
if (!Number.isFinite(max)) {
|
|
303
|
+
return -Infinity;
|
|
304
|
+
}
|
|
305
|
+
let sumExp = 0;
|
|
306
|
+
for (const value of values) {
|
|
307
|
+
if (Number.isFinite(value)) {
|
|
308
|
+
sumExp += Math.exp(value - max);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
return sumExp > 0 ? max + Math.log(sumExp) : -Infinity;
|
|
312
|
+
};
|
|
313
|
+
|
|
314
|
+
/** The two shift-and-sum terms of a distribution's log-partition. */
|
|
315
|
+
interface LogPartitionTerms {
|
|
316
|
+
logSumTerm: number;
|
|
317
|
+
maxLogit: number;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* The log-partition depends only on the buffer, but scoring reads the same
|
|
322
|
+
* buffer once per candidate — so without this the two full-vocabulary passes
|
|
323
|
+
* below run N times per boundary for an answer that cannot change.
|
|
324
|
+
*
|
|
325
|
+
* Keying on the buffer itself is safe because captured logits are never
|
|
326
|
+
* written to again (see `CanonicalLogitProcessor.processLogits`, which copies
|
|
327
|
+
* out of WebLLM's reused buffer), and the weak reference lets a retired
|
|
328
|
+
* boundary's entry go with it.
|
|
329
|
+
*/
|
|
330
|
+
const logPartitionTermsCache = new WeakMap<Float32Array, LogPartitionTerms | null>();
|
|
331
|
+
|
|
332
|
+
const getLogPartitionTerms = (logits: Float32Array): LogPartitionTerms | null => {
|
|
333
|
+
const cached = logPartitionTermsCache.get(logits);
|
|
334
|
+
if (cached !== undefined) {
|
|
335
|
+
return cached;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
let maxLogit = -Infinity;
|
|
339
|
+
for (let i = 0; i < logits.length; i++) {
|
|
340
|
+
if (logits[i] > maxLogit) {
|
|
341
|
+
maxLogit = logits[i];
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
let terms: LogPartitionTerms | null = null;
|
|
346
|
+
if (Number.isFinite(maxLogit)) {
|
|
347
|
+
let sumExp = 0;
|
|
348
|
+
for (let i = 0; i < logits.length; i++) {
|
|
349
|
+
sumExp += Math.exp(logits[i] - maxLogit);
|
|
350
|
+
}
|
|
351
|
+
if (sumExp > 0) {
|
|
352
|
+
terms = { maxLogit, logSumTerm: Math.log(sumExp) };
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
logPartitionTermsCache.set(logits, terms);
|
|
357
|
+
return terms;
|
|
358
|
+
};
|
|
359
|
+
|
|
360
|
+
export const logSoftmaxAt = (logits: Float32Array, target: number): number => {
|
|
361
|
+
if (target < 0 || target >= logits.length) {
|
|
362
|
+
return -Infinity;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
const terms = getLogPartitionTerms(logits);
|
|
366
|
+
if (terms === null) {
|
|
367
|
+
return -Infinity;
|
|
368
|
+
}
|
|
369
|
+
return logits[target] - terms.maxLogit - terms.logSumTerm;
|
|
370
|
+
};
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* One processor is registered for the causal model so the scheduler can read
|
|
374
|
+
* the next-token distribution off any forward pass.
|
|
375
|
+
*
|
|
376
|
+
* The scheduler drives decoding token by token — it prefills a prompt, then
|
|
377
|
+
* feeds each continuation token itself — so the processor never has to force
|
|
378
|
+
* the model onto a target path. It only has to hand back the distribution the
|
|
379
|
+
* forward pass produced, which it does without altering the logits.
|
|
380
|
+
*/
|
|
381
|
+
export class CanonicalLogitProcessor implements LogitProcessor {
|
|
382
|
+
private captured: Float32Array | null = null;
|
|
383
|
+
|
|
384
|
+
processLogits = (logits: Float32Array): Float32Array => {
|
|
385
|
+
// WebLLM reuses its logits buffer between forwards, so keep a private copy.
|
|
386
|
+
this.captured = new Float32Array(logits);
|
|
387
|
+
return logits;
|
|
388
|
+
};
|
|
389
|
+
|
|
390
|
+
processSampledToken = (): void => {
|
|
391
|
+
// The scheduler chooses the next token; WebLLM's sample is discarded.
|
|
392
|
+
};
|
|
393
|
+
|
|
394
|
+
resetState = (): void => {
|
|
395
|
+
// WebLLM resets the processor after the caller has armed a capture and
|
|
396
|
+
// immediately before it prefills a completion prompt, so this only ever
|
|
397
|
+
// clears a stale reading from the previous run.
|
|
398
|
+
this.captured = null;
|
|
399
|
+
};
|
|
400
|
+
|
|
401
|
+
startCapture = (): void => {
|
|
402
|
+
this.captured = null;
|
|
403
|
+
};
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* The captured distribution, or null when the forward pass never ran.
|
|
407
|
+
*
|
|
408
|
+
* `processLogits` already copied it, and every capture allocates afresh, so
|
|
409
|
+
* the buffer handed out here is never written to again.
|
|
410
|
+
*/
|
|
411
|
+
getCapturedLogits = (): Float32Array | null => this.captured;
|
|
412
|
+
}
|
|
@@ -19,24 +19,115 @@
|
|
|
19
19
|
declare global {
|
|
20
20
|
interface Window {
|
|
21
21
|
__atlCtcDebug__?: {
|
|
22
|
-
enable: () => void;
|
|
23
22
|
disable: () => void;
|
|
23
|
+
/**
|
|
24
|
+
* Enable CTC debug logging for this session. Pass `'verbose'` to also
|
|
25
|
+
* emit the per-prediction candidate table, canonical LM, and grammar detail:
|
|
26
|
+
* `__atlCtcDebug__.enable('verbose')`.
|
|
27
|
+
*/
|
|
28
|
+
enable: (level?: 'verbose') => void;
|
|
24
29
|
isEnabled: () => boolean;
|
|
30
|
+
/** Whether verbose logging (candidate tables + extra detail) is on. */
|
|
31
|
+
isVerbose: () => boolean;
|
|
25
32
|
};
|
|
26
33
|
}
|
|
27
34
|
}
|
|
28
35
|
|
|
29
|
-
|
|
36
|
+
/**
|
|
37
|
+
* Shared `%c` styles for all CTC console output, so every log — lifecycle,
|
|
38
|
+
* async signals, and per-prediction groups — reads consistently.
|
|
39
|
+
*/
|
|
40
|
+
export const CTC_STYLES = {
|
|
41
|
+
brand: 'color: #00b8d9; font-weight: bold;',
|
|
42
|
+
section: 'color: #9c27b0; font-weight: bold;',
|
|
43
|
+
dim: 'color: #888; font-style: italic;',
|
|
44
|
+
good: 'color: #4caf50; font-weight: bold;',
|
|
45
|
+
warn: 'color: #ff9800; font-weight: bold;',
|
|
46
|
+
bad: 'color: #f44336; font-weight: bold;',
|
|
47
|
+
cold: 'color: #9e9e9e; font-weight: bold;',
|
|
48
|
+
lm: 'color: #e83e8c; font-weight: bold;',
|
|
49
|
+
body: 'color: inherit; font-weight: normal;',
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Log one aligned section line inside a per-prediction group, e.g.
|
|
54
|
+
* `INPUT raw: "…"`. Label is padded so the bodies line up. No-ops unless
|
|
55
|
+
* debug is enabled, so callers don't need to guard.
|
|
56
|
+
*/
|
|
57
|
+
export const ctcSection = (label: string, body: string): void => {
|
|
58
|
+
if (!isAutocompleteDebugEnabled()) {
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
// eslint-disable-next-line no-console
|
|
62
|
+
console.log(`%c${label.padEnd(10)}%c${body}`, CTC_STYLES.section, CTC_STYLES.body);
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Log a lifecycle / async-signal line (outside the per-prediction groups),
|
|
67
|
+
* tagged `[CTC:<tag>]`. No-ops unless debug is enabled, so callers don't need to
|
|
68
|
+
* guard. Use tags like `init` (loads) and `signal` (slow-lane arrivals).
|
|
69
|
+
*/
|
|
70
|
+
export const ctcTag = (tag: string, body: string, tagStyle: string = CTC_STYLES.brand): void => {
|
|
71
|
+
if (!isAutocompleteDebugEnabled()) {
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
// eslint-disable-next-line no-console
|
|
75
|
+
console.log(`%c[CTC:${tag}]%c ${body}`, tagStyle, CTC_STYLES.body);
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const readUrlDebugFlag = (): { enabled: boolean; verbose: boolean } => {
|
|
30
79
|
if (typeof window === 'undefined') {
|
|
31
|
-
return false;
|
|
80
|
+
return { enabled: false, verbose: false };
|
|
32
81
|
}
|
|
33
82
|
try {
|
|
34
|
-
|
|
83
|
+
const value = new URLSearchParams(window.location.search).get('atlCtcDebug');
|
|
84
|
+
return { enabled: value === '1' || value === 'verbose', verbose: value === 'verbose' };
|
|
35
85
|
} catch {
|
|
36
|
-
return false;
|
|
86
|
+
return { enabled: false, verbose: false };
|
|
37
87
|
}
|
|
38
88
|
};
|
|
39
89
|
|
|
90
|
+
const printLegend = (verbose: boolean): void => {
|
|
91
|
+
// eslint-disable-next-line no-console
|
|
92
|
+
console.groupCollapsed(
|
|
93
|
+
`%c[CTC]%c debug enabled${verbose ? ' (verbose)' : ''} — Contextual Typeahead Completion`,
|
|
94
|
+
CTC_STYLES.brand,
|
|
95
|
+
CTC_STYLES.body,
|
|
96
|
+
);
|
|
97
|
+
// eslint-disable-next-line no-console
|
|
98
|
+
console.log(
|
|
99
|
+
'%cPipeline%c INPUT → SIGNALS → CANONICAL → GENERATE → SCORE → ARBITRATE → STABILIZE → CANDIDATES',
|
|
100
|
+
CTC_STYLES.section,
|
|
101
|
+
CTC_STYLES.body,
|
|
102
|
+
);
|
|
103
|
+
// eslint-disable-next-line no-console
|
|
104
|
+
console.log(
|
|
105
|
+
'%cEvidence%c Tier A = exact-context first token · prefix = one more decoded token · exact = every token of the surface scored · absent = Stage 1',
|
|
106
|
+
CTC_STYLES.section,
|
|
107
|
+
CTC_STYLES.body,
|
|
108
|
+
);
|
|
109
|
+
// eslint-disable-next-line no-console
|
|
110
|
+
console.log(
|
|
111
|
+
'%cLM queue%c one serialised GPU queue drains both: prompt prefills and single-token decode steps',
|
|
112
|
+
CTC_STYLES.section,
|
|
113
|
+
CTC_STYLES.body,
|
|
114
|
+
);
|
|
115
|
+
// eslint-disable-next-line no-console
|
|
116
|
+
console.log(
|
|
117
|
+
'%cPlanes%c [CTC:init] loads · [CTC:signal] semantic/network · [CTC:model] primes/exact · [CTC:model-cost] prefill/decode · [CTC:readiness] deadline progress',
|
|
118
|
+
CTC_STYLES.section,
|
|
119
|
+
CTC_STYLES.body,
|
|
120
|
+
);
|
|
121
|
+
// eslint-disable-next-line no-console
|
|
122
|
+
console.log(
|
|
123
|
+
'%cInspect%c __atlCtcDebug__.enable("verbose") · .disable()',
|
|
124
|
+
CTC_STYLES.section,
|
|
125
|
+
CTC_STYLES.body,
|
|
126
|
+
);
|
|
127
|
+
// eslint-disable-next-line no-console
|
|
128
|
+
console.groupEnd();
|
|
129
|
+
};
|
|
130
|
+
|
|
40
131
|
// State lives on the window object (not a module closure) so duplicate copies of this
|
|
41
132
|
// module across separate bundles/realms share one source of truth and the console API
|
|
42
133
|
// controls them all. In-memory only; persisted across reload via the URL flag.
|
|
@@ -45,15 +136,24 @@ const getDebugApi = (): Window['__atlCtcDebug__'] => {
|
|
|
45
136
|
return undefined;
|
|
46
137
|
}
|
|
47
138
|
if (!window.__atlCtcDebug__) {
|
|
48
|
-
|
|
139
|
+
const initial = readUrlDebugFlag();
|
|
140
|
+
let debugEnabled = initial.enabled;
|
|
141
|
+
let debugVerbose = initial.verbose;
|
|
142
|
+
if (debugEnabled) {
|
|
143
|
+
printLegend(debugVerbose);
|
|
144
|
+
}
|
|
49
145
|
window.__atlCtcDebug__ = {
|
|
50
|
-
enable: () => {
|
|
146
|
+
enable: (level?: 'verbose') => {
|
|
51
147
|
debugEnabled = true;
|
|
148
|
+
debugVerbose = level === 'verbose';
|
|
149
|
+
printLegend(debugVerbose);
|
|
52
150
|
},
|
|
53
151
|
disable: () => {
|
|
54
152
|
debugEnabled = false;
|
|
153
|
+
debugVerbose = false;
|
|
55
154
|
},
|
|
56
155
|
isEnabled: () => debugEnabled,
|
|
156
|
+
isVerbose: () => debugEnabled && debugVerbose,
|
|
57
157
|
};
|
|
58
158
|
}
|
|
59
159
|
return window.__atlCtcDebug__;
|
|
@@ -61,5 +161,7 @@ const getDebugApi = (): Window['__atlCtcDebug__'] => {
|
|
|
61
161
|
|
|
62
162
|
export const isAutocompleteDebugEnabled = (): boolean => getDebugApi()?.isEnabled() ?? false;
|
|
63
163
|
|
|
164
|
+
export const isAutocompleteDebugVerbose = (): boolean => getDebugApi()?.isVerbose() ?? false;
|
|
165
|
+
|
|
64
166
|
// Eagerly install so the console API is available on load, regardless of call order.
|
|
65
167
|
getDebugApi();
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { Node as PMNode } from '@atlaskit/editor-prosemirror/model';
|
|
2
2
|
import { Decoration, DecorationSet } from '@atlaskit/editor-prosemirror/view';
|
|
3
3
|
|
|
4
4
|
const GHOST_TEXT_CLASS = 'autocomplete-ghost-text';
|
|
@@ -6,9 +6,12 @@ const GHOST_TEXT_CLASS = 'autocomplete-ghost-text';
|
|
|
6
6
|
/**
|
|
7
7
|
* Creates a DecorationSet containing a ghost text widget at the given position.
|
|
8
8
|
* The ghost text is rendered as a styled <span> that appears after the cursor.
|
|
9
|
+
*
|
|
10
|
+
* Takes the document rather than the whole state so it can also be called from
|
|
11
|
+
* `apply`, where only the post-transaction doc exists.
|
|
9
12
|
*/
|
|
10
13
|
export const createGhostTextDecorationSet = (
|
|
11
|
-
|
|
14
|
+
doc: PMNode,
|
|
12
15
|
position: number,
|
|
13
16
|
text: string,
|
|
14
17
|
): DecorationSet => {
|
|
@@ -39,9 +42,15 @@ export const createGhostTextDecorationSet = (
|
|
|
39
42
|
},
|
|
40
43
|
{
|
|
41
44
|
side: 1, // Render after content at this position
|
|
42
|
-
key
|
|
45
|
+
// A matching key short-circuits `WidgetType.eq`, so ProseMirror reuses
|
|
46
|
+
// the rendered node and never calls `toDOM` again. Keying on the text
|
|
47
|
+
// keeps that reuse when nothing changed while still forcing a redraw
|
|
48
|
+
// when the ghost advances through a keystroke that confirmed it —
|
|
49
|
+
// otherwise the stale tail stays on screen and Tab inserts a
|
|
50
|
+
// character the user already typed.
|
|
51
|
+
key: `autocomplete-ghost-text:${text}`,
|
|
43
52
|
},
|
|
44
53
|
);
|
|
45
54
|
|
|
46
|
-
return DecorationSet.create(
|
|
55
|
+
return DecorationSet.create(doc, [decoration]);
|
|
47
56
|
};
|