diff --git a/lit_nlp/api/layout.py b/lit_nlp/api/layout.py
index fe65d412..637b3f19 100644
--- a/lit_nlp/api/layout.py
+++ b/lit_nlp/api/layout.py
@@ -91,6 +91,7 @@ class ModuleConfig(dtypes.DataTuple):
class LayoutSettings(dtypes.DataTuple):
hideToolbar: bool = False
mainHeight: int = 45
+ leftWidth: int = 50
centerPage: bool = False
diff --git a/lit_nlp/client/core/modules.ts b/lit_nlp/client/core/modules.ts
index d4503776..ffb53b85 100644
--- a/lit_nlp/client/core/modules.ts
+++ b/lit_nlp/client/core/modules.ts
@@ -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') {
@@ -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({
diff --git a/lit_nlp/client/elements/switch.ts b/lit_nlp/client/elements/switch.ts
index 7bd383e4..74118e4f 100644
--- a/lit_nlp/client/elements/switch.ts
+++ b/lit_nlp/client/elements/switch.ts
@@ -85,12 +85,17 @@ export class LitSwitch extends LitElement {
'selected': this.selected
});
+ // prettier-ignore
return html`
-
${this.labelLeft}
+
+ ${this.labelLeft}
+
-
${this.labelRight}
+
+ ${this.labelRight}
+
`;
}
diff --git a/lit_nlp/client/elements/token_chips.css b/lit_nlp/client/elements/token_chips.css
index 8cc33112..bea68608 100644
--- a/lit_nlp/client/elements/token_chips.css
+++ b/lit_nlp/client/elements/token_chips.css
@@ -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;
}
\ No newline at end of file
diff --git a/lit_nlp/client/elements/token_chips.ts b/lit_nlp/client/elements/token_chips.ts
index bafff398..2e14ffdc 100644
--- a/lit_nlp/client/elements/token_chips.ts
+++ b/lit_nlp/client/elements/token_chips.ts
@@ -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;
}
@@ -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 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];
@@ -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`` : null}
+ ${preSpace ? html`
` : null}
- ${tokenInfo.token}
+ ${tokenText}
-
`;
- // clang-format on
+
+ ${postBreak ? html`` : null}
+ `;
}
override render() {
@@ -92,9 +155,10 @@ 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`
${this.tokenGroupTitle ? this.tokenGroupTitle : ''}
@@ -102,7 +166,6 @@ export class TokenChips extends LitElement {
${tokensDOM}
`;
- // clang-format on
}
}
diff --git a/lit_nlp/client/elements/token_chips_test.ts b/lit_nlp/client/elements/token_chips_test.ts
index 03c36598..1cdb8a87 100644
--- a/lit_nlp/client/elements/token_chips_test.ts
+++ b/lit_nlp/client/elements/token_chips_test.ts
@@ -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', () => {
@@ -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(
+ '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(
+ '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(
diff --git a/lit_nlp/client/lib/types.ts b/lit_nlp/client/lib/types.ts
index 5f60fff0..c1bb8794 100644
--- a/lit_nlp/client/lib/types.ts
+++ b/lit_nlp/client/lib/types.ts
@@ -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;
}