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

Internal change #1038

Open
wants to merge 1 commit into
base: dev
Choose a base branch
from
Open
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
124 changes: 124 additions & 0 deletions lit_nlp/client/elements/lit_input_field.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
/**
* @license
* Copyright 2023 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

/**
* @fileoverview A custom element that maps a LitType and (optionally) its
* corresponding value to an HTML representation for editing or display.
*/

// tslint:disable:no-new-decorators

import {html} from 'lit';
import {customElement, property} from 'lit/decorators';
import {AnnotationCluster, EdgeLabel, ScoredTextCandidate, ScoredTextCandidates, SpanLabel} from '../lib/dtypes';
import {ReactiveElement} from '../lib/elements';
import {BooleanLitType, EdgeLabels, Embeddings, GeneratedTextCandidates, LitType, MultiSegmentAnnotations, Scalar, SpanLabels, StringLitType} from '../lib/lit_types';
import {formatAnnotationCluster, formatBoolean, formatEdgeLabel, formatEmbeddings, formatNumber, formatScoredTextCandidate, formatScoredTextCandidates, formatScoredTextCandidatesList, formatSpanLabel} from '../lib/type_rendering';
import {chunkWords} from '../lib/utils';

function isGeneratedTextCandidate(input: unknown): boolean {
return Array.isArray(input) && input.length === 2 &&
typeof input[0] === 'string' &&
(input[1] == null || typeof input[1] === 'number');
}

/**
* An element for mapping LitType fields to the correct HTML input type.
*/
@customElement('lit-input-field')
export class LitInputField extends ReactiveElement {
@property({type: String}) name = '';
@property({type: Object}) type = new LitType();
@property({type: Boolean}) readonly = false;
@property() value: unknown;
@property({type: Boolean}) limitWords = false;

override render() {
return this.readonly ? this.renderReadonly() : this.renderEditable();
}

/**
* Converts the input value to a readonly representaiton for display pruposes.
*/
private renderReadonly(): string|number {
if (Array.isArray(this.value)) {
if (isGeneratedTextCandidate(this.value)) {
return formatScoredTextCandidate(this.value as ScoredTextCandidate);
}

if (Array.isArray(this.value[0]) &&
isGeneratedTextCandidate(this.value[0])) {
return formatScoredTextCandidates(this.value as ScoredTextCandidates);
}

const strings = this.value.map((item) => {
if (typeof item === 'number') {return formatNumber(item);}
if (this.limitWords) {return chunkWords(item);}
return `${item}`;
});
return strings.join(', ');
} else if (this.type instanceof BooleanLitType) {
return formatBoolean(this.value as boolean);
} else if (this.type instanceof EdgeLabels) {
const formattedTags = (this.value as EdgeLabel[]).map(formatEdgeLabel);
return formattedTags.join(', ');
} else if (this.type instanceof Embeddings) {
return formatEmbeddings(this.value);
} else if (this.type instanceof GeneratedTextCandidates) {
return formatScoredTextCandidatesList(
this.value as ScoredTextCandidates[]);
} else if (this.type instanceof MultiSegmentAnnotations) {
const formattedTags =
(this.value as AnnotationCluster[]).map(formatAnnotationCluster);
return formattedTags.join(', ');
} else if (this.type instanceof Scalar || typeof this.value === 'number') {
return formatNumber(this.value as number);
} else if (this.type instanceof SpanLabels) {
const formattedTags =
(this.value as SpanLabel[]).map(s => formatSpanLabel(s)) as string[];
return formattedTags.join(', ');
} else if (this.type instanceof StringLitType ||
typeof this.value === 'string') {
return this.limitWords ?
chunkWords(this.value as string) : this.value as string;
} else {
return '';
}
}

/**
* Renders the appropriate HTML elements to enable user input to control the
* value of the field with which this element is associated.
*/
private renderEditable() {
if (this.type instanceof BooleanLitType) {
const change = (e: Event) => {
this.value = !!(e.target as HTMLInputElement).value;
dispatchEvent(new Event('change'));
};
return html`<lit-checkbox ?checked=${!!this.value} @change=${change}></lit-checkbox>`;
} else {
return html`Unsupported type '${this.type.name}' cannot be rnedered.`;
}
}
}

declare global {
interface HTMLElementTagNameMap {
'lit-input-field': LitInputField;
}
}
6 changes: 6 additions & 0 deletions lit_nlp/client/elements/lit_input_field_test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import 'jasmine';

describe('lit input field test', () => {
it('computes a thing when asked', () => {
});
});
143 changes: 143 additions & 0 deletions lit_nlp/client/lib/type_rendering.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
/**
* @license
* Copyright 2023 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* @fileoverview Functions for converting LitType classes to base HTML elements
* for editing or display.
*
* As a naming convention, exported functions that support editing via HTML
* inputs, selects, etc, start with "render" (because they used Lit.dev's HTML
* templates to render elements) and functions that are intended for readonly
* display start with "format" (because they typically convert the value to a
* string-like representation).
*/

import {html} from 'lit';
import {StyleInfo, styleMap} from 'lit/directives/style-map';
import {AnnotationCluster, EdgeLabel, ScoredTextCandidate, ScoredTextCandidates, SpanLabel} from './dtypes';

type EventHandler = (e: Event) => void;

/** Formats an AnnotationCluster for display. */
export function formatAnnotationCluster(ac: AnnotationCluster): string {
return `${ac.label}${ac.score != null ? ` (${ac.score})` : ''}`;
}

/**
* Formats an AnnotationCluster[] (e.g., from MultiSegmentAnnotations) for
* display as a <div> of <div>s.
*/
export function formatAnnotationClusters(clusters: AnnotationCluster[]) {
return html`<div class="multi-segment-annotation">${clusters.map(c =>
html`<div class="annotation-cluster">
<div>${formatAnnotationCluster(c)}</div>
<ul>${c.spans.map(s => html`<li>${formatSpanLabel(s, true)}</li>`)}</ul>
</div>`)
}</div>`;
}

/** Formats a boolean value for display. */
export function formatBoolean(val: boolean): string {
return val ? '✔' : ' ';
}

/** Formats an EdgeLabel for display. */
export function formatEdgeLabel(e: EdgeLabel): string {
function formatSpan (s: [number, number]) {return `[${s[0]}, ${s[1]})`;}
const span1Text = formatSpan(e.span1);
const span2Text = e.span2 ? ` ← ${formatSpan(e.span2)}` : '';
// Add non-breaking control chars to keep this on one line
// TODO(lit-dev): get it to stop breaking between ) and :; \u2060 doesn't work
return `${span1Text}${span2Text}\u2060: ${e.label}`.replace(
/\ /g, '\u00a0' /* &nbsp; */);
}

/** Formats Embeddings for display. */
export function formatEmbeddings(val: unknown): string {
return Array.isArray(val) ? `<float>[${val.length}]` : '';
}

/** Formats a number to a fixed (3 decimal) value for display. */
export function formatNumber (item: number): number {
return Number.isInteger(item) ? item : Number(item.toFixed(3));
}

/** Formats a ScoredTextCandidate for display. */
export function formatScoredTextCandidate([t, s]: ScoredTextCandidate): string {
return `${t}${typeof s === 'number' ? ` (${formatNumber(s)})` : ''}`;
}

/** Formats a ScoredTextCandidates for display. */
export function formatScoredTextCandidates(stc: ScoredTextCandidates): string {
return stc.map(formatScoredTextCandidate).join('\n\n');
}

/** Formats a list of ScoredTextCandidates for display. */
export function formatScoredTextCandidatesList(
list: ScoredTextCandidates[]): string {
return list.map(formatScoredTextCandidates).join('\n\n');
}

/** Formats a SpanLabel for display.
*
* By default, it formats the SpanLabel as a string. If monospace is true, then
* the function returns an HTML Template wrapping the default string in a
* <div.monospace-label> element.
*/
export function formatSpanLabel(s: SpanLabel, monospace = false) {
let formatted = `[${s.start}, ${s.end})`;
if (s.align) {
formatted = `${s.align} ${formatted}`;
}
if (s.label) {
// TODO(lit-dev): Stop from breaking between ) and :, \u2060 doesn't work
formatted = `${formatted}\u2060: ${s.label}`;
}

formatted = formatted.replace(/\ /g, '\u00a0' /* &nbsp; */);
return monospace ?
html`<div class="monospace-label">${formatted}</div>` : formatted;
}

/** Formats a SpanLabel[] as a <div> of <div>s. */
export function formatSpanLabels(labels: SpanLabel[]) {
return html`<div class="span-labels">${
labels.map(s =>formatSpanLabel(s, true))
}</div>`;
}

/** Renders a <select> element with <option>s for each item in the vocab. */
export function renderCategoricalInput(
vocab: string[], change: EventHandler, value = '') {
return html`<select class="dropdown" @change=${change} .value=${value}>
${vocab.map(cat =>
html`<option value=${cat} ?selected=${value === cat}>${cat}</option>`)}
<option value="" ?selected=${value === ``}></option>
</select>`;
}

/** Renders a <textarea> for long-form textual input. */
export function renderTextInputLong(
input: EventHandler, value = '', styles: StyleInfo = {}) {
return html`<textarea class="input-box" style="${styleMap(styles)}"
@input=${input} .value=${value}></textarea>`;
}

/** Renders an <inut type="text"> for short-form textual input. */
export function renderTextInputShort(input: EventHandler, value = '') {
return html`<input type="text" class="input-short" @input=${input}
.value=${value}>`;
}
Loading