Skip to content

Commit 5cffc4d

Browse files
iftenneyLIT team
authored and
LIT team
committed
Misc frontend changes to support sequence salience.
- Updates to <lit-token-chips> - <lit-switch> accepts labels via slots, so don't need to be just text - Specify default left/right split for three-panel layouts PiperOrigin-RevId: 606392017
1 parent ab294bd commit 5cffc4d

File tree

7 files changed

+212
-16
lines changed

7 files changed

+212
-16
lines changed

lit_nlp/api/layout.py

+1
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ class ModuleConfig(dtypes.DataTuple):
9191
class LayoutSettings(dtypes.DataTuple):
9292
hideToolbar: bool = False
9393
mainHeight: int = 45
94+
leftWidth: int = 50
9495
centerPage: bool = False
9596

9697

lit_nlp/client/core/modules.ts

+13-1
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,12 @@ export class LitModules extends ReactiveElement {
151151
(mainHeight) => {
152152
if (mainHeight != null) {this.upperHeight = `${mainHeight}%`;}
153153
});
154+
this.reactImmediately(
155+
() => this.modulesService.getSetting('leftWidth'), (leftWidth) => {
156+
if (leftWidth != null) {
157+
this.leftColumnWidth = `${leftWidth}%`;
158+
}
159+
});
154160

155161
document.addEventListener('keydown', (e: KeyboardEvent) => {
156162
if (e.key === 'Escape') {
@@ -422,7 +428,13 @@ export class LitModules extends ReactiveElement {
422428
const columnSeparatorDoubleClick = (event: DragEvent) => {
423429
event.stopPropagation();
424430
event.preventDefault();
425-
this.leftColumnWidth = LEFT_COLUMN_DEFAULT_WIDTH;
431+
const layoutDefaultLeftWidth =
432+
this.modulesService.getSetting('leftWidth');
433+
if (layoutDefaultLeftWidth != null) {
434+
this.leftColumnWidth = `${layoutDefaultLeftWidth}%`;
435+
} else {
436+
this.leftColumnWidth = LEFT_COLUMN_DEFAULT_WIDTH;
437+
}
426438
};
427439

428440
const leftColumnStyles = styleMap({

lit_nlp/client/elements/switch.ts

+7-2
Original file line numberDiff line numberDiff line change
@@ -85,12 +85,17 @@ export class LitSwitch extends LitElement {
8585
'selected': this.selected
8686
});
8787

88+
// prettier-ignore
8889
return html`
8990
<div class=${containerClasses} @click=${toggleState}>
90-
<div class='switch-label label-left'>${this.labelLeft}</div>
91+
<div class='switch-label label-left'>
92+
${this.labelLeft}<slot name="labelLeft"></slot>
93+
</div>
9194
<mwc-switch ?selected=${this.selected} ?disabled=${this.disabled}>
9295
</mwc-switch>
93-
<div class='switch-label label-right'>${this.labelRight}</div>
96+
<div class='switch-label label-right'>
97+
<slot name="labelRight"></slot>${this.labelRight}
98+
</div>
9499
</div>
95100
`;
96101
}

lit_nlp/client/elements/token_chips.css

+47
Original file line numberDiff line numberDiff line change
@@ -52,4 +52,51 @@
5252

5353
.pre-wrap {
5454
white-space: pre-wrap;
55+
}
56+
57+
.row-break {
58+
flex-basis: 100%;
59+
height: 0;
60+
}
61+
62+
.word-spacer {
63+
width: 1em;
64+
}
65+
66+
.tokens-holder-dense .word-spacer {
67+
width: 0.5em;
68+
}
69+
70+
/* block mode */
71+
.tokens-holder-display-block {
72+
display: block;
73+
font-size: 0; /* hack to get zero spacing between elements */
74+
line-height: 22px;
75+
}
76+
77+
.tokens-holder-display-block > * {
78+
/* TODO: set this for all modes? */
79+
font-size: 13px; /* restore standard font size */
80+
}
81+
82+
.tokens-holder-display-block .salient-token {
83+
display: inline;
84+
min-height: 1lh;
85+
vertical-align: baseline;
86+
}
87+
88+
.tokens-holder-display-block.tokens-holder-dense .salient-token span {
89+
/* hack to remove extra whitespace. ugh. */
90+
margin-right: -0.445ch;
91+
}
92+
93+
.tokens-holder-display-block .word-spacer {
94+
display: inline;
95+
vertical-align: baseline;
96+
white-space: pre-wrap;
97+
}
98+
99+
.tokens-holder-display-block lit-tooltip {
100+
--anchor-display-mode: 'inline';
101+
--tooltip-position-left: 0;
55102
}

lit_nlp/client/elements/token_chips.ts

+74-11
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,9 @@ export interface TokenWithWeight {
3535
weight: number;
3636
selected?: boolean;
3737
pinned?: boolean;
38-
onClick?: (e: Event) => void;
39-
onMouseover?: (e: Event) => void;
40-
onMouseout?: (e: Event) => void;
38+
onClick?: (e: MouseEvent) => void;
39+
onMouseover?: (e: MouseEvent) => void;
40+
onMouseout?: (e: MouseEvent) => void;
4141
disableHover?: boolean;
4242
forceShowTooltip?: boolean;
4343
}
@@ -50,9 +50,33 @@ export class TokenChips extends LitElement {
5050
// List of tokens to display
5151
@property({type: Array}) tokensWithWeights: TokenWithWeight[] = [];
5252
@property({type: Object}) cmap: SalienceCmap = new UnsignedSalienceCmap();
53-
@property({type: String})
54-
tokenGroupTitle?: string; // can be used for gradKey
53+
// Group title, such as the name of the active salience method.
54+
@property({type: String}) tokenGroupTitle?: string;
55+
/**
56+
* Dense mode, for less padding and smaller margins around each chip.
57+
*/
5558
@property({type: Boolean}) dense = false;
59+
/**
60+
* Block mode uses display: block and inline elements for chips, instead of
61+
* a flex-row layout. This allows chips to flow across line breaks, behaving
62+
* more like <span> elements and giving a much better experience for larger
63+
* segments like sentences. However, this comes at the cost of more spacing
64+
* artifacts and occasional issues with tooltip positioning.
65+
*/
66+
@property({type: Boolean}) displayBlock = false;
67+
/**
68+
* breakNewlines removes \n at the beginning or end of a segment and inserts
69+
* explicit row break elements instead. Improves readability in many settings,
70+
* at the cost of "faithfulness" to the original token text.
71+
*/
72+
@property({type: Boolean}) breakNewlines = false;
73+
/**
74+
* preSpace removes a leading space from a token and inserts an explicit
75+
* spacer element instead. Improves readability in many settings by giving
76+
* natural space between the highlight area for adjacent words, albeit at the
77+
* cost of hiding where the actual spaces are in the tokenization.
78+
*/
79+
@property({type: Boolean}) preSpace = false;
5680

5781
static override get styles() {
5882
return [sharedStyles, styles];
@@ -71,17 +95,56 @@ export class TokenChips extends LitElement {
7195
'color': this.cmap.textCmap(tokenInfo.weight),
7296
});
7397

74-
// clang-format off
98+
let tokenText = tokenInfo.token;
99+
100+
let preSpace = false;
101+
if (this.preSpace && tokenText.startsWith(' ')) {
102+
preSpace = true;
103+
tokenText = tokenText.slice(1);
104+
}
105+
106+
// TODO(b/324955623): render a gray '⏎' for newlines?
107+
// Maybe make this a toggleable option, as it can be distracting.
108+
// TODO(b/324955623): better rendering for multiple newlines, like \n\n\n ?
109+
// Consider adding an extra ' ' on each line.
110+
111+
let preBreak = false;
112+
let postBreak = false;
113+
if (this.breakNewlines) {
114+
// Logic:
115+
// - \n : post-break, so blank space goes on previous line
116+
// - foo\n : post-break
117+
// - \nfoo : pre-break
118+
// - \n\n : pre- and post-break, shows a space on its own line
119+
// - \n\n\n : pre- and post-break, two lines with only spaces
120+
if (tokenText.endsWith('\n')) {
121+
// Prefer post-break because this puts the blank space on the end of the
122+
// previous line, rather than creating an awkward indent on the next
123+
// one.
124+
tokenText = tokenText.slice(0, -1) + ' ';
125+
postBreak = true;
126+
}
127+
if (tokenText.startsWith('\n')) {
128+
// Pre-break only if \n precedes some other text.
129+
preBreak = true;
130+
tokenText = ' ' + tokenText.slice(1);
131+
}
132+
}
133+
134+
// prettier-ignore
75135
return html`
136+
${preBreak ? html`<div class='row-break'></div>` : null}
137+
${preSpace ? html`<div class='word-spacer'> </div>` : null}
76138
<div class=${tokenClass} style=${tokenStyle} @click=${tokenInfo.onClick}
77139
@mouseover=${tokenInfo.onMouseover} @mouseout=${tokenInfo.onMouseout}>
78140
<lit-tooltip content=${tokenInfo.weight.toPrecision(3)}
79141
?forceShow=${Boolean(tokenInfo.forceShowTooltip)}
80142
?disabled=${Boolean(tokenInfo.disableHover)}>
81-
<span class='pre-wrap' slot="tooltip-anchor">${tokenInfo.token}</span>
143+
<span class='pre-wrap' slot="tooltip-anchor">${tokenText}</span>
82144
</lit-tooltip>
83-
</div>`;
84-
// clang-format on
145+
</div>
146+
${postBreak ? html`<div class='row-break'></div>` : null}
147+
`;
85148
}
86149

87150
override render() {
@@ -92,17 +155,17 @@ export class TokenChips extends LitElement {
92155
const holderClass = classMap({
93156
'tokens-holder': true,
94157
'tokens-holder-dense': this.dense,
158+
'tokens-holder-display-block': this.displayBlock,
95159
});
96160

97-
// clang-format off
161+
// prettier-ignore
98162
return html`
99163
<div class="tokens-group">
100164
${this.tokenGroupTitle ? this.tokenGroupTitle : ''}
101165
<div class=${holderClass}>
102166
${tokensDOM}
103167
</div>
104168
</div>`;
105-
// clang-format on
106169
}
107170
}
108171

lit_nlp/client/elements/token_chips_test.ts

+67-1
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,21 @@ const TESTDATA: Array<{tokensWithWeights: TokenWithWeight[]}> = [
3131
{token: 'hello', weight: 0.7, selected: true, pinned: true},
3232
{token: 'world', weight: 0.3}
3333
],
34-
}
34+
},
35+
{
36+
// for testing preSpace mode
37+
tokensWithWeights: [
38+
{token: 'foo', weight: 0.7, selected: true, pinned: true},
39+
{token: ' bar', weight: 0.3}, {token: 'baz', weight: 0.5}
40+
],
41+
},
42+
{
43+
// for testing breakNewlines mode
44+
tokensWithWeights: [
45+
{token: 'foo', weight: 0.7}, {token: '\nbar', weight: 0.3},
46+
{token: '\n\n', weight: 0.1}, {token: 'baz\n', weight: 0.5}
47+
],
48+
},
3549
];
3650

3751
describe('token chips test', () => {
@@ -60,6 +74,58 @@ describe('token chips test', () => {
6074
expect(tokenElements[0].children[0]).toBeInstanceOf(LitTooltip);
6175
});
6276

77+
it('should break spaces in preSpace mode', async () => {
78+
tokenChips.preSpace = true;
79+
await tokenChips.updateComplete;
80+
81+
const tokenElements =
82+
tokenChips.renderRoot.querySelectorAll<HTMLDivElement>(
83+
'div.salient-token');
84+
expect(tokenElements.length).toEqual(tokensWithWeights.length);
85+
for (let i = 0; i < tokenElements.length; i++) {
86+
const elem = tokenElements[i];
87+
const expectedToken = tokensWithWeights[i].token;
88+
if (expectedToken.startsWith(' ')) {
89+
// Space moved to a word spacer.
90+
expect(elem.innerText).toEqual(expectedToken.slice(1));
91+
expect(elem.previousElementSibling?.classList ?? [])
92+
.toContain('word-spacer');
93+
} else {
94+
// Space intact, no word spacer.
95+
expect(elem.innerText).toEqual(expectedToken);
96+
if (i > 0) {
97+
expect(elem.previousElementSibling?.classList ?? [])
98+
.toContain('salient-token');
99+
}
100+
}
101+
}
102+
});
103+
104+
it('should break newlines in breakNewlines mode', async () => {
105+
tokenChips.breakNewlines = true;
106+
await tokenChips.updateComplete;
107+
108+
const tokenElements =
109+
tokenChips.renderRoot.querySelectorAll<HTMLDivElement>(
110+
'div.salient-token');
111+
expect(tokenElements.length).toEqual(tokensWithWeights.length);
112+
for (let i = 0; i < tokenElements.length; i++) {
113+
const elem = tokenElements[i];
114+
let expectedToken = tokensWithWeights[i].token;
115+
if (expectedToken.endsWith('\n')) {
116+
expectedToken = expectedToken.slice(0, -1) + ' ';
117+
expect(elem.nextElementSibling?.classList ?? [])
118+
.toContain('row-break');
119+
}
120+
if (expectedToken.startsWith('\n')) {
121+
expectedToken = ' ' + expectedToken.slice(1);
122+
expect(elem.previousElementSibling?.classList ?? [])
123+
.toContain('row-break');
124+
}
125+
expect(elem.innerText).toEqual(expectedToken);
126+
}
127+
});
128+
63129
it('should mark a selected token', async () => {
64130
const tokenElements =
65131
tokenChips.renderRoot.querySelectorAll<HTMLDivElement>(

lit_nlp/client/lib/types.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -410,8 +410,10 @@ export declare interface LitCanonicalLayout {
410410
*/
411411
export declare interface LayoutSettings {
412412
hideToolbar?: boolean;
413-
/** The default height of #upper-right, as a percentage of the parent. */
413+
/** The default height of the 'upper' section, as a percentage. */
414414
mainHeight?: number;
415+
/** The default width of the 'left' section, as a percentage. */
416+
leftWidth?: number;
415417
centerPage?: boolean;
416418
}
417419

0 commit comments

Comments
 (0)