chromium/chrome/browser/resources/omnibox/ml/ml_calculator.ts

// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import {assert} from 'chrome://resources/js/assert.js';
import {CustomElement} from 'chrome://resources/js/custom_element.js';

import type {Signals} from '../omnibox.mojom-webui.js';
import {clamp, createEl, setFormattedClipboardForMl, signalNames} from '../omnibox_util.js';

import type {MlBrowserProxy} from './ml_browser_proxy.js';
/* eslint-disable-next-line @typescript-eslint/ban-ts-comment */
// @ts-ignore:next-line
import sheet from './ml_calculator.css' with {type : 'css'};
import {getTemplate} from './ml_calculator.html.js';

export class MlCalculatorElement extends CustomElement {
  private mlBrowserProxy_: MlBrowserProxy;
  private signalInputs: HTMLInputElement[];

  static override get template() {
    return getTemplate();
  }

  constructor() {
    super();
    this.shadowRoot!.adoptedStyleSheets = [sheet];
  }

  connectedCallback() {
    this.signalInputs = signalNames.map(signalName => {
      const label = createEl(
          'label', this.getRequiredElement('#signals'), ['input-row'],
          signalName + ': ');
      const input = createEl('input', label);
      input.type = 'number';
      input.placeholder = 'null';
      input.addEventListener('input', () => this.update());
      return input;
    });

    this.getRequiredElement('#copy').addEventListener('click', async () => {
      const promise = setFormattedClipboardForMl(
          {score: this.score}, this.signals, window.location.href,
          await this.mlBrowserProxy_.modelVersion);
      this.dispatchEvent(new CustomEvent('copied', {detail: promise}));
    });

    this.getRequiredElement('#clear').addEventListener('click', () => {
      this.signalInputs.forEach(el => el.value = el.placeholder);
      this.update();
    });

    try {
      const urlSignals =
          new URLSearchParams(window.location.search).get('signals');
      if (urlSignals) {
        this.signals =
            MlCalculatorElement.parseSignalStrings(urlSignals.split(','));
      }
    } catch (e) {
    }
  }

  set mlBrowserProxy(mlBrowserProxy: MlBrowserProxy) {
    this.mlBrowserProxy_ = mlBrowserProxy;
    mlBrowserProxy.modelVersion.then(version => {
      createEl('a', this.getRequiredElement('#version'), [], version.string)
          .href = version.url;
    });
    this.update();
  }

  private static parseSignalStrings(signalStrings: string[]): Signals {
    assert(signalStrings.length === signalNames.length);
    return Object.fromEntries(
        signalStrings
            .map(str => {
              // Handle `''` and `null`; otherwise `Number()` would convert them
              // to `0`.
              if (!str) {
                return null;
              }
              const num = Number(str);
              return Number.isNaN(num) ?
                  null :
                  clamp(Math.floor(num), -(2 ** 31), 2 ** 31 - 1);
            })
            .map((signal, i) => [signalNames[i], signal]));
  }

  get signals(): Signals {
    return MlCalculatorElement.parseSignalStrings(
        this.signalInputs.map(input => input.value));
  }

  set signals(signals: Signals) {
    // Signals can be numbers, booleans, or null.
    Object.values(signals).forEach(
        (signal, i) => this.signalInputs[i]!.value =
            signal === null ? '' : String(Number(signal)));
    this.update();
  }

  private get score(): number {
    return Number(this.getRequiredElement('#score').textContent);
  }

  private set score(score: number) {
    this.getRequiredElement('#score').textContent = String(score);
  }

  private async update() {
    if (!this.mlBrowserProxy_) {
      return;
    }
    this.signalInputs.forEach(
        input => input.classList.toggle('empty', !!input.textContent));
    this.score = await this.mlBrowserProxy_.makeMlRequest(this.signals);
    window.history.replaceState(
        null, '', `?signals=${Object.values(this.signals)}`);
    this.dispatchEvent(new CustomEvent('updated'));
  }
}

declare global {
  interface HTMLElementTagNameMap {
    'ml-calculator': MlCalculatorElement;
  }
}

customElements.define('ml-calculator', MlCalculatorElement);