Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Misc frontend changes to support sequence salience. #1379

Merged
1 commit merged into from
Feb 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading