Merge branch 'feat/web/trace-insert-edits' into feat/web/compute-merge-split-effects

This commit is contained in:
Joshua Horton 2025-09-25 15:07:46 -05:00
commit 5bd7edbb28
4 changed files with 60 additions and 24 deletions

View file

@ -257,7 +257,7 @@ export class ContextState {
const tokens = resultTokenization.tokens;
const lastIndex = tokens.length - 1;
// Ignore a context-final empty '' token; the interesting one is what comes before.
const nonEmptyTail = tokens[lastIndex].sourceText != '' ? tokens[lastIndex] : tokens[lastIndex - 1];
const nonEmptyTail = !tokens[lastIndex].isEmptyToken ? tokens[lastIndex] : tokens[lastIndex - 1];
const appliedSuggestionTransitionId = nonEmptyTail?.appliedTransitionId;
// Used to construct and represent the part of the incoming transform that

View file

@ -16,6 +16,14 @@ import Distribution = LexicalModelTypes.Distribution;
import LexicalModel = LexicalModelTypes.LexicalModel;
import Transform = LexicalModelTypes.Transform;
/**
* Notes critical properties of the inputs comprising each ContextToken.
*/
export interface TokenInputSource {
trueTransform: Transform;
inputStartIndex: number;
}
/**
* Breaks apart a raw text string into individual, single-codepoint
* transforms, all set with the specified transform ID.
@ -65,7 +73,7 @@ export class ContextToken {
* applied to the actual context for the set of keystrokes contributing to
* this token.
*/
private _inputRange: Transform[];
private _inputRange: TokenInputSource[];
/**
* Constructs a new, empty instance for use with the specified LexicalModel.
@ -117,7 +125,10 @@ export class ContextToken {
return [{sample: transform, p: 1.0}];
});
rawTransformDistributions.forEach((entry) => {
this._inputRange.push(entry[0].sample);
this._inputRange.push({
trueTransform: entry[0].sample,
inputStartIndex: 0
});
this.searchSpace.addInput(entry);
});
}
@ -127,26 +138,52 @@ export class ContextToken {
* Call this to record the original keystroke Transforms for the context range
* corresponding to this token.
*/
addSourceInput(transform: Transform) {
this._inputRange.push(transform);
addInput(inputSource: TokenInputSource, distribution: Distribution<Transform>) {
this._inputRange.push(inputSource);
this.searchSpace.addInput(distribution);
}
/**
* Denotes the original keystroke Transforms comprising the range corresponding
* to this token.
*/
get inputRange(): Readonly<Transform[]> {
get inputRange(): Readonly<TokenInputSource[]> {
return this._inputRange;
}
/**
* Gets a simple, human-readable representation of `inputRange`.
* Indicates whether or not this ContextToken likely represents an empty token.
*/
get isEmptyToken(): boolean {
return this.exampleInput == '';
}
/**
* Gets a compact string-based representation of `inputRange` that
* maps compatible token source ranges to each other.
*/
get sourceRangeKey(): string {
const components: string[] = [];
for(const source of this.inputRange) {
const i = source.inputStartIndex;
components.push(`T${source.trueTransform.id}${i != 0 ? '@' + i : ''}`);
}
return components.join('+');
}
/**
* Gets a simple, compact string-based representation of `inputRange`.
*
* Should not actually be used in code - its use is intended only for
* debugging.
* This should only ever be used for debugging purposes.
*/
get sourceText(): string {
const composite = this._inputRange.reduce((accum, current) => buildMergedTransform(accum, current), { insert: '', deleteLeft: 0 });
const composite = this._inputRange.reduce((accum, current) => {
const alteredTransform = {...current.trueTransform};
alteredTransform.insert = alteredTransform.insert.slice(current.inputStartIndex);
return buildMergedTransform(accum, current.trueTransform)
}, { insert: '', deleteLeft: 0 });
const prefix = '\u{2421}'.repeat(composite.deleteLeft);
return prefix + composite.insert;
}
@ -166,7 +203,7 @@ export class ContextToken {
* most likely keystroke data afterward.
*/
const transforms = this.searchSpace.inputSequence.map((dist) => dist[0].sample)
const composite = transforms.reduce((accum, current) => buildMergedTransform(accum, current), { insert: '', deleteLeft: 0});
const composite = transforms.reduce((accum, current) => buildMergedTransform(accum, current), {insert: '', deleteLeft: 0});
return composite.insert;
}
}

View file

@ -75,11 +75,11 @@ export class ContextTokenization {
/**
* Returns plain-text strings representing the most probable representation for all
* tokens represented by this tokenization instance.
*
* Intended for debugging use only.
*/
get sourceText() {
return this.tokens
.filter(token => token.sourceText !== null)
.map(token => token.sourceText);
return this.tokens.map(token => token.sourceText);
}
/**
@ -87,10 +87,7 @@ export class ContextTokenization {
* tokens represented by this tokenization instance.
*/
get exampleInput(): string[] {
return this.tokens
// Hide any tokens representing invisible wordbreaks. (Thinking ahead to phrase-level possibilities)
.filter(token => token.exampleInput !== null)
.map(token => token.exampleInput);
return this.tokens.map(token => token.exampleInput);
}
/**
@ -104,7 +101,7 @@ export class ContextTokenization {
* the tokenization modeled by this instance.
*/
computeAlignment(incomingTokenization: string[], isSliding: boolean, noSubVerify?: boolean): ContextStateAlignment {
return computeAlignment(this.sourceText, incomingTokenization, isSliding, noSubVerify);
return computeAlignment(this.exampleInput, incomingTokenization, isSliding, noSubVerify);
}
/**
@ -368,6 +365,7 @@ export class ContextTokenization {
// edited, those edits occur to the left as well - and further left of whatever
// the new tail token is *if* tokens were removed.
const firstTailEditIndex = Math.min((1 - tailEditLength), 0) + Math.min(tailTokenShift, 0);
let primaryInputAppliedLen = 0;
for(let i = 0; i < tailEditLength; i++) {
const tailIndex = firstTailEditIndex + i;
@ -393,11 +391,12 @@ export class ContextTokenization {
// Erase any applied-suggestion transition ID; it is no longer valid.
token.appliedTransitionId = undefined;
const emptySample: ProbabilityMass<Transform> = { sample: { insert: '', deleteLeft: 0 }, p: 1 };
token.addSourceInput(primaryInput ?? emptySample.sample);
token.searchSpace.addInput(tokenDistribution.map((seq) => seq.get(tailIndex) ?? emptySample));
const dist = tokenDistribution.map((seq) => seq.get(tailIndex) ?? emptySample);
token.addInput({trueTransform: primaryInput ?? emptySample.sample, inputStartIndex: primaryInputAppliedLen}, dist);
}
tokenization[incomingIndex] = token;
primaryInputAppliedLen += KMWString.length(primaryInput?.insert ?? '');
}
if(tailTokenShift < 0) {
@ -454,11 +453,10 @@ export class ContextTokenization {
// If there are no entries in our would-be distribution, there's no
// reason to pass in what amounts to a no-op.
if(transformDistribution) {
pushedToken.addSourceInput(primaryInput);
// If we ever stop filtering tokenized transform distributions, it may
// be worth adding an empty transform here with weight to balance
// the distribution back to a cumulative prob sum of 1.
pushedToken.searchSpace.addInput(transformDistribution);
pushedToken.addInput({ trueTransform: primaryInput, inputStartIndex: primaryInputAppliedLen }, transformDistribution);
}
} else if(incomingToken.text) {
// We have no transform data to match against an inserted token with text; abort!
@ -470,6 +468,7 @@ export class ContextTokenization {
// Auto-replaces the search space to correspond with the new token.
tokenization.push(pushedToken);
primaryInputAppliedLen += KMWString.length(primaryInput.insert);
}
}

View file

@ -394,7 +394,7 @@ export function determineSuggestionAlignment(
// Did the wordbreaker (or similar) append a blank token before the caret? If so,
// preserve that by preventing corrections from triggering left-deletion.
if(transition.final.tokenization.tail.sourceText == '') {
if(transition.final.tokenization.tail.isEmptyToken) {
deleteLeft = 0;
}