Skip to content

Commit

Permalink
Misc frontend changes to support sequence salience.
Browse files Browse the repository at this point in the history
- 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
  • Loading branch information
iftenney authored and LIT team committed Feb 12, 2024
1 parent ab294bd commit 5cffc4d
Show file tree
Hide file tree
Showing 7 changed files with 212 additions and 16 deletions.
1 change: 1 addition & 0 deletions lit_nlp/api/layout.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ class ModuleConfig(dtypes.DataTuple):
class LayoutSettings(dtypes.DataTuple):
hideToolbar: bool = False
mainHeight: int = 45
leftWidth: int = 50
centerPage: bool = False


Expand Down
14 changes: 13 additions & 1 deletion lit_nlp/client/core/modules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,12 @@ export class LitModules extends ReactiveElement {
(mainHeight) => {
if (mainHeight != null) {this.upperHeight = `${mainHeight}%`;}
});
this.reactImmediately(
() => this.modulesService.getSetting('leftWidth'), (leftWidth) => {
if (leftWidth != null) {
this.leftColumnWidth = `${leftWidth}%`;
}
});

document.addEventListener('keydown', (e: KeyboardEvent) => {
if (e.key === 'Escape') {
Expand Down Expand Up @@ -422,7 +428,13 @@ export class LitModules extends ReactiveElement {
const columnSeparatorDoubleClick = (event: DragEvent) => {
event.stopPropagation();
event.preventDefault();
this.leftColumnWidth = LEFT_COLUMN_DEFAULT_WIDTH;
const layoutDefaultLeftWidth =
this.modulesService.getSetting('leftWidth');
if (layoutDefaultLeftWidth != null) {
this.leftColumnWidth = `${layoutDefaultLeftWidth}%`;
} else {
this.leftColumnWidth = LEFT_COLUMN_DEFAULT_WIDTH;
}
};

const leftColumnStyles = styleMap({
Expand Down
9 changes: 7 additions & 2 deletions lit_nlp/client/elements/switch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,12 +85,17 @@ export class LitSwitch extends LitElement {
'selected': this.selected
});

// prettier-ignore
return html`
<div class=${containerClasses} @click=${toggleState}>
<div class='switch-label label-left'>${this.labelLeft}</div>
<div class='switch-label label-left'>
${this.labelLeft}<slot name="labelLeft"></slot>
</div>
<mwc-switch ?selected=${this.selected} ?disabled=${this.disabled}>
</mwc-switch>
<div class='switch-label label-right'>${this.labelRight}</div>
<div class='switch-label label-right'>
<slot name="labelRight"></slot>${this.labelRight}
</div>
</div>
`;
}
Expand Down
47 changes: 47 additions & 0 deletions lit_nlp/client/elements/token_chips.css
Original file line number Diff line number Diff line change
Expand Up @@ -52,4 +52,51 @@

.pre-wrap {
white-space: pre-wrap;
}

.row-break {
flex-basis: 100%;
height: 0;
}

.word-spacer {
width: 1em;
}

.tokens-holder-dense .word-spacer {
width: 0.5em;
}

/* block mode */
.tokens-holder-display-block {
display: block;
font-size: 0; /* hack to get zero spacing between elements */
line-height: 22px;
}

.tokens-holder-display-block > * {
/* TODO: set this for all modes? */
font-size: 13px; /* restore standard font size */
}

.tokens-holder-display-block .salient-token {
display: inline;
min-height: 1lh;
vertical-align: baseline;
}

.tokens-holder-display-block.tokens-holder-dense .salient-token span {
/* hack to remove extra whitespace. ugh. */
margin-right: -0.445ch;
}

.tokens-holder-display-block .word-spacer {
display: inline;
vertical-align: baseline;
white-space: pre-wrap;
}

.tokens-holder-display-block lit-tooltip {
--anchor-display-mode: 'inline';
--tooltip-position-left: 0;
}
85 changes: 74 additions & 11 deletions lit_nlp/client/elements/token_chips.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,9 @@ export interface TokenWithWeight {
weight: number;
selected?: boolean;
pinned?: boolean;
onClick?: (e: Event) => void;
onMouseover?: (e: Event) => void;
onMouseout?: (e: Event) => void;
onClick?: (e: MouseEvent) => void;
onMouseover?: (e: MouseEvent) => void;
onMouseout?: (e: MouseEvent) => void;
disableHover?: boolean;
forceShowTooltip?: boolean;
}
Expand All @@ -50,9 +50,33 @@ export class TokenChips extends LitElement {
// List of tokens to display
@property({type: Array}) tokensWithWeights: TokenWithWeight[] = [];
@property({type: Object}) cmap: SalienceCmap = new UnsignedSalienceCmap();
@property({type: String})
tokenGroupTitle?: string; // can be used for gradKey
// Group title, such as the name of the active salience method.
@property({type: String}) tokenGroupTitle?: string;
/**
* Dense mode, for less padding and smaller margins around each chip.
*/
@property({type: Boolean}) dense = false;
/**
* Block mode uses display: block and inline elements for chips, instead of
* a flex-row layout. This allows chips to flow across line breaks, behaving
* more like <span> elements and giving a much better experience for larger
* segments like sentences. However, this comes at the cost of more spacing
* artifacts and occasional issues with tooltip positioning.
*/
@property({type: Boolean}) displayBlock = false;
/**
* breakNewlines removes \n at the beginning or end of a segment and inserts
* explicit row break elements instead. Improves readability in many settings,
* at the cost of "faithfulness" to the original token text.
*/
@property({type: Boolean}) breakNewlines = false;
/**
* preSpace removes a leading space from a token and inserts an explicit
* spacer element instead. Improves readability in many settings by giving
* natural space between the highlight area for adjacent words, albeit at the
* cost of hiding where the actual spaces are in the tokenization.
*/
@property({type: Boolean}) preSpace = false;

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

// clang-format off
let tokenText = tokenInfo.token;

let preSpace = false;
if (this.preSpace && tokenText.startsWith(' ')) {
preSpace = true;
tokenText = tokenText.slice(1);
}

// TODO(b/324955623): render a gray '⏎' for newlines?
// Maybe make this a toggleable option, as it can be distracting.
// TODO(b/324955623): better rendering for multiple newlines, like \n\n\n ?
// Consider adding an extra ' ' on each line.

let preBreak = false;
let postBreak = false;
if (this.breakNewlines) {
// Logic:
// - \n : post-break, so blank space goes on previous line
// - foo\n : post-break
// - \nfoo : pre-break
// - \n\n : pre- and post-break, shows a space on its own line
// - \n\n\n : pre- and post-break, two lines with only spaces
if (tokenText.endsWith('\n')) {
// Prefer post-break because this puts the blank space on the end of the
// previous line, rather than creating an awkward indent on the next
// one.
tokenText = tokenText.slice(0, -1) + ' ';
postBreak = true;
}
if (tokenText.startsWith('\n')) {
// Pre-break only if \n precedes some other text.
preBreak = true;
tokenText = ' ' + tokenText.slice(1);
}
}

// prettier-ignore
return html`
${preBreak ? html`<div class='row-break'></div>` : null}
${preSpace ? html`<div class='word-spacer'> </div>` : null}
<div class=${tokenClass} style=${tokenStyle} @click=${tokenInfo.onClick}
@mouseover=${tokenInfo.onMouseover} @mouseout=${tokenInfo.onMouseout}>
<lit-tooltip content=${tokenInfo.weight.toPrecision(3)}
?forceShow=${Boolean(tokenInfo.forceShowTooltip)}
?disabled=${Boolean(tokenInfo.disableHover)}>
<span class='pre-wrap' slot="tooltip-anchor">${tokenInfo.token}</span>
<span class='pre-wrap' slot="tooltip-anchor">${tokenText}</span>
</lit-tooltip>
</div>`;
// clang-format on
</div>
${postBreak ? html`<div class='row-break'></div>` : null}
`;
}

override render() {
Expand All @@ -92,17 +155,17 @@ export class TokenChips extends LitElement {
const holderClass = classMap({
'tokens-holder': true,
'tokens-holder-dense': this.dense,
'tokens-holder-display-block': this.displayBlock,
});

// clang-format off
// prettier-ignore
return html`
<div class="tokens-group">
${this.tokenGroupTitle ? this.tokenGroupTitle : ''}
<div class=${holderClass}>
${tokensDOM}
</div>
</div>`;
// clang-format on
}
}

Expand Down
68 changes: 67 additions & 1 deletion lit_nlp/client/elements/token_chips_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,21 @@ const TESTDATA: Array<{tokensWithWeights: TokenWithWeight[]}> = [
{token: 'hello', weight: 0.7, selected: true, pinned: true},
{token: 'world', weight: 0.3}
],
}
},
{
// for testing preSpace mode
tokensWithWeights: [
{token: 'foo', weight: 0.7, selected: true, pinned: true},
{token: ' bar', weight: 0.3}, {token: 'baz', weight: 0.5}
],
},
{
// for testing breakNewlines mode
tokensWithWeights: [
{token: 'foo', weight: 0.7}, {token: '\nbar', weight: 0.3},
{token: '\n\n', weight: 0.1}, {token: 'baz\n', weight: 0.5}
],
},
];

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

it('should break spaces in preSpace mode', async () => {
tokenChips.preSpace = true;
await tokenChips.updateComplete;

const tokenElements =
tokenChips.renderRoot.querySelectorAll<HTMLDivElement>(
'div.salient-token');
expect(tokenElements.length).toEqual(tokensWithWeights.length);
for (let i = 0; i < tokenElements.length; i++) {
const elem = tokenElements[i];
const expectedToken = tokensWithWeights[i].token;
if (expectedToken.startsWith(' ')) {
// Space moved to a word spacer.
expect(elem.innerText).toEqual(expectedToken.slice(1));
expect(elem.previousElementSibling?.classList ?? [])
.toContain('word-spacer');
} else {
// Space intact, no word spacer.
expect(elem.innerText).toEqual(expectedToken);
if (i > 0) {
expect(elem.previousElementSibling?.classList ?? [])
.toContain('salient-token');
}
}
}
});

it('should break newlines in breakNewlines mode', async () => {
tokenChips.breakNewlines = true;
await tokenChips.updateComplete;

const tokenElements =
tokenChips.renderRoot.querySelectorAll<HTMLDivElement>(
'div.salient-token');
expect(tokenElements.length).toEqual(tokensWithWeights.length);
for (let i = 0; i < tokenElements.length; i++) {
const elem = tokenElements[i];
let expectedToken = tokensWithWeights[i].token;
if (expectedToken.endsWith('\n')) {
expectedToken = expectedToken.slice(0, -1) + ' ';
expect(elem.nextElementSibling?.classList ?? [])
.toContain('row-break');
}
if (expectedToken.startsWith('\n')) {
expectedToken = ' ' + expectedToken.slice(1);
expect(elem.previousElementSibling?.classList ?? [])
.toContain('row-break');
}
expect(elem.innerText).toEqual(expectedToken);
}
});

it('should mark a selected token', async () => {
const tokenElements =
tokenChips.renderRoot.querySelectorAll<HTMLDivElement>(
Expand Down
4 changes: 3 additions & 1 deletion lit_nlp/client/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -410,8 +410,10 @@ export declare interface LitCanonicalLayout {
*/
export declare interface LayoutSettings {
hideToolbar?: boolean;
/** The default height of #upper-right, as a percentage of the parent. */
/** The default height of the 'upper' section, as a percentage. */
mainHeight?: number;
/** The default width of the 'left' section, as a percentage. */
leftWidth?: number;
centerPage?: boolean;
}

Expand Down

0 comments on commit 5cffc4d

Please sign in to comment.