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

import '../../../ui/components/tooltips/tooltips.js';

import type * as Host from '../../../core/host/host.js';
import * as i18n from '../../../core/i18n/i18n.js';
import type * as Platform from '../../../core/platform/platform.js';
import * as SDK from '../../../core/sdk/sdk.js';
import * as Protocol from '../../../generated/protocol.js';
import * as AiAssistanceModel from '../../../models/ai_assistance/ai_assistance.js';
import * as PanelsCommon from '../../../panels/common/common.js';
import * as PanelUtils from '../../../panels/utils/utils.js';
import * as Buttons from '../../../ui/components/buttons/buttons.js';
import * as Input from '../../../ui/components/input/input.js';
import * as Snackbars from '../../../ui/components/snackbars/snackbars.js';
import * as UI from '../../../ui/legacy/legacy.js';
import * as Lit from '../../../ui/lit/lit.js';
import * as VisualLogging from '../../../ui/visual_logging/visual_logging.js';

import chatInputStyles from './chatInput.css.js';

const {html, Directives: {createRef, ref}} = Lit;
const {widget} = UI.Widget;

const UIStrings = {
  /**
   * @description Label added to the text input to describe the context for screen readers. Not shown visibly on screen.
   */
  inputTextAriaDescription: 'You can also use one of the suggested prompts above to start your conversation',
  /**
   * @description Label added to the button that reveals the selected context item in DevTools
   */
  revealContextDescription: 'Reveal the selected context item in DevTools',
  /**
   * @description The footer disclaimer that links to more information about the AI feature.
   */
  learnAbout: 'Learn about AI in DevTools',
} as const;

/*
* Strings that don't need to be translated at this time.
*/
const UIStringsNotTranslate = {
  /**
   * @description Title for the send icon button.
   */
  sendButtonTitle: 'Send',
  /**
   * @description Title for the start new chat
   */
  startNewChat: 'Start new chat',
  /**
   * @description Title for the cancel icon button.
   */
  cancelButtonTitle: 'Cancel',
  /**
   * @description Label for the "select an element" button.
   */
  selectAnElement: 'Select an element',
  /**
   * @description Title for the take screenshot button.
   */
  takeScreenshotButtonTitle: 'Take screenshot',
  /**
   * @description Title for the remove image input button.
   */
  removeImageInputButtonTitle: 'Remove image input',
  /**
   * @description Title for the add image button.
   */
  addImageButtonTitle: 'Add image',
  /**
   * @description Text displayed when the chat input is disabled due to reading past conversation.
   */
  pastConversation: 'You\'re viewing a past conversation.',
  /**
   * @description Message displayed in toast in case of any failures while taking a screenshot of the page.
   */
  screenshotFailureMessage: 'Failed to take a screenshot. Please try again.',
  /**
   * @description Message displayed in toast in case of any failures while uploading an image file as input.
   */
  uploadImageFailureMessage: 'Failed to upload image. Please try again.',
  /**
   * @description Label added to the button that add selected context from the current panel in AI Assistance panel.
   */
  addContext: 'Add item for context',
  /**
   * @description Label added to the button that remove the currently selected element in AI Assistance panel.
   */
  removeContextElement: 'Remove element from context',
  /**
   * @description Label added to the button that remove the currently selected context in AI Assistance panel.
   */
  removeContextRequest: 'Remove request from context',
  /**
   * @description Label added to the button that remove the currently selected context in AI Assistance panel.
   */
  removeContextFile: 'Remove file from context',
  /**
   * @description Label added to the button that remove the currently selected context in AI Assistance panel.
   */
  removeContextPerfInsight: 'Remove performance insight from context',
  /**
   * @description Label added to the button that remove the currently selected context in AI Assistance panel.
   */
  removeContext: 'Remove from context',
} as const;

const str_ = i18n.i18n.registerUIStrings('panels/ai_assistance/components/ChatInput.ts', UIStrings);
const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);
const lockedString = i18n.i18n.lockedString;

const SCREENSHOT_QUALITY = 80;
const JPEG_MIME_TYPE = 'image/jpeg';
const SHOW_LOADING_STATE_TIMEOUT = 100;

const RELEVANT_DATA_LINK_CHAT_ID = 'relevant-data-link-chat';
const RELEVANT_DATA_LINK_FOOTER_ID = 'relevant-data-link-footer';

export type ImageInputData = {
  isLoading: true,
}|{
  isLoading: false,
  data: string,
  mimeType: string,
  inputType: AiAssistanceModel.AiAgent.MultimodalInputType,
};

export interface ViewInput {
  isLoading: boolean;
  isTextInputEmpty: boolean;
  blockedByCrossOrigin: boolean;
  isTextInputDisabled: boolean;
  inputPlaceholder: Platform.UIString.LocalizedString;
  context: AiAssistanceModel.AiAgent.ConversationContext<unknown>|null;
  isContextSelected: boolean;
  inspectElementToggled: boolean;
  disclaimerText: string;
  conversationType: AiAssistanceModel.AiHistoryStorage.ConversationType;
  multimodalInputEnabled: boolean;
  imageInput?: ImageInputData;
  uploadImageInputEnabled: boolean;
  isReadOnly: boolean;
  textAreaRef: Lit.Directives.Ref<HTMLTextAreaElement>;

  onContextClick: () => void;
  onInspectElementClick: () => void;
  onSubmit: (ev: SubmitEvent) => void;
  onTextAreaKeyDown: (ev: KeyboardEvent) => void;
  onCancel: (ev: SubmitEvent) => void;
  onNewConversation: () => void;
  onTextInputChange: (input: string) => void;
  onTakeScreenshot: () => void;
  onRemoveImageInput: () => void;
  onImageUpload: (ev: Event) => void;
  onImagePaste: (event: ClipboardEvent) => void;
  onImageDragOver: (event: DragEvent) => void;
  onImageDrop: (event: DragEvent) => void;
  onContextRemoved: (() => void)|null;
  onContextAdd: (() => void)|null;
}

export type ViewOutput = undefined;

function getContextRemoveLabel(context: AiAssistanceModel.AiAgent.ConversationContext<unknown>):
    Platform.UIString.LocalizedString {
  if (context instanceof AiAssistanceModel.FileAgent.FileContext) {
    return lockedString(UIStringsNotTranslate.removeContextFile);
  }
  if (context instanceof AiAssistanceModel.StylingAgent.NodeContext) {
    return lockedString(UIStringsNotTranslate.removeContextElement);
  }
  if (context instanceof AiAssistanceModel.NetworkAgent.RequestContext) {
    return lockedString(UIStringsNotTranslate.removeContextRequest);
  }
  if (context instanceof AiAssistanceModel.PerformanceAgent.PerformanceTraceContext) {
    return lockedString(UIStringsNotTranslate.removeContextPerfInsight);
  }
  return lockedString(UIStringsNotTranslate.removeContext);
}

export const DEFAULT_VIEW = (input: ViewInput, _output: ViewOutput, target: HTMLElement): void => {
  const chatInputContainerCls = Lit.Directives.classMap({
    'chat-input-container': true,
    'single-line-layout': !input.context,
    disabled: input.isTextInputDisabled,
  });

  const renderRelevantDataDisclaimer = (tooltipId: string): Lit.LitTemplate => {
    const classes = Lit.Directives.classMap({
      'chat-input-disclaimer': true,
      'hide-divider': !input.isLoading && input.blockedByCrossOrigin,
    });
    // clang-format off
    return html`
      <div class=${classes}>
        <button
          class="link"
          role="link"
          aria-details=${tooltipId}
          jslog=${VisualLogging.link('open-ai-settings').track({
            click: true,
          })}
          @click=${(ev: Event) => {
            ev.preventDefault();
            void UI.ViewManager.ViewManager.instance().showView('chrome-ai');
          }}
        >${lockedString('Relevant data')}</button>&nbsp;${lockedString('is sent to Google')}
        <devtools-tooltip
          id=${tooltipId}
          variant="rich"
        ><div class="info-tooltip-container">
          ${input.disclaimerText}
          <button
            class="link tooltip-link"
            role="link"
            jslog=${VisualLogging.link('open-ai-settings').track({
              click: true,
            })}
            @click=${() => {
              void UI.ViewManager.ViewManager.instance().showView('chrome-ai');
            }}>${i18nString(UIStrings.learnAbout)}
          </button>
        </div></devtools-tooltip>
      </div>
    `;
    // clang-format on
  };

  // clang-format off
  Lit.render(html`
    <style>${Input.textInputStyles}</style>
    <style>${chatInputStyles}</style>
    ${input.isReadOnly ?
      html`
        <div
          class="chat-readonly-container"
          jslog=${VisualLogging.section('read-only')}
        >
          <span>${lockedString(UIStringsNotTranslate.pastConversation)}</span>
          <devtools-button
            aria-label=${lockedString(UIStringsNotTranslate.startNewChat)}
            class="chat-inline-button"
            @click=${input.onNewConversation}
            .data=${{
              variant: Buttons.Button.Variant.TEXT,
              title: lockedString(UIStringsNotTranslate.startNewChat),
              jslogContext: 'start-new-chat',
            } as Buttons.Button.ButtonData}
          >${lockedString(UIStringsNotTranslate.startNewChat)}</devtools-button>
        </div>`
      :
      html`
        <form class="input-form" @submit=${input.onSubmit}>
          <div class=${chatInputContainerCls}>
            ${(input.multimodalInputEnabled && input.imageInput && !input.isTextInputDisabled) ?
              html`
                <div class="image-input-container">
                  <devtools-button
                    aria-label=${lockedString(UIStringsNotTranslate.removeImageInputButtonTitle)}
                    @click=${input.onRemoveImageInput}
                    .data=${{
                      variant: Buttons.Button.Variant.ICON,
                      size: Buttons.Button.Size.MICRO,
                      iconName: 'cross',
                      title: lockedString(UIStringsNotTranslate.removeImageInputButtonTitle),
                    } as Buttons.Button.ButtonData}
                  ></devtools-button>
                  ${input.imageInput.isLoading ?
                    html`
                      <div class="loading">
                        <devtools-spinner></devtools-spinner>
                      </div>`
                    :
                    html`
                      <img src="data:${input.imageInput.mimeType};base64, ${input.imageInput.data}" alt="Image input" />`
                  }
                </div>`
              : Lit.nothing}
            <textarea
              class="chat-input"
              .disabled=${input.isTextInputDisabled}
              wrap="hard"
              maxlength="10000"
              @keydown=${input.onTextAreaKeyDown}
              @paste=${input.onImagePaste}
              @dragover=${input.onImageDragOver}
              @drop=${input.onImageDrop}
              @input=${(event: KeyboardEvent) => {
                input.onTextInputChange((event.target as HTMLInputElement).value);
              }}
              placeholder=${input.inputPlaceholder}
              jslog=${VisualLogging.textField('query').track({
                change: true,
                keydown: 'Enter',
              })}
              aria-description=${i18nString(UIStrings.inputTextAriaDescription)}
              ${ref(input.textAreaRef)}
            ></textarea>
            <div class="chat-input-actions">
              <div class="chat-input-actions-left">
                ${input.context ?
                  html`
                    <div class="select-element">
                      ${input.conversationType === AiAssistanceModel.AiHistoryStorage.ConversationType.STYLING ?
                        html`
                          <devtools-button
                            .data=${{
                              variant: Buttons.Button.Variant.ICON_TOGGLE,
                              size: Buttons.Button.Size.SMALL,
                              iconName: 'select-element',
                              toggledIconName: 'select-element',
                              toggleType: Buttons.Button.ToggleType.PRIMARY,
                              toggled: input.inspectElementToggled,
                              title: lockedString(UIStringsNotTranslate.selectAnElement),
                              jslogContext: 'select-element',
                              disabled: input.isTextInputDisabled,
                            } as Buttons.Button.ButtonData}
                            @click=${input.onInspectElementClick}
                          ></devtools-button>`
                        : Lit.nothing}
                      <div
                        class=${Lit.Directives.classMap({
                          'resource-link': true,
                          disabled: !input.isContextSelected,
                        })}
                      >
                        ${
                          input.context instanceof AiAssistanceModel.StylingAgent.NodeContext ?
                            html`
                              <devtools-widget
                                class="title"
                                ${widget(PanelsCommon.DOMLinkifier.DOMNodeLink, {
                                  node: input.context.getItem(),
                                  options: {
                                    disabled: !input.isContextSelected,
                                    hiddenClassList: input.context.getItem().classNames().filter(
                                      className => className.startsWith(AiAssistanceModel.Injected.AI_ASSISTANCE_CSS_CLASS_NAME)),
                                    ariaDescription: i18nString(UIStrings.revealContextDescription),
                                  },
                                })}
                              ></devtools-widget>` :
                            html`
                          ${input.context instanceof AiAssistanceModel.NetworkAgent.RequestContext ?
                            PanelUtils.PanelUtils.getIconForNetworkRequest(input.context.getItem()) :
                            input.context instanceof AiAssistanceModel.FileAgent.FileContext ?
                            PanelUtils.PanelUtils.getIconForSourceFile(input.context.getItem()) :
                            input.context instanceof AiAssistanceModel.AccessibilityAgent.AccessibilityContext ?
                            html`<devtools-icon class="icon" name="performance" title="Lighthouse"></devtools-icon>` :
                            input.context instanceof AiAssistanceModel.PerformanceAgent.PerformanceTraceContext ?
                            html`<devtools-icon class="icon" name="performance" title="Performance"></devtools-icon>` :
                            Lit.nothing}
                            <span
                              role="button"
                              class="title"
                              tabindex="0"
                              @click=${input.onContextClick}
                              @keydown=${(ev: KeyboardEvent) => {
                                if (ev.key === 'Enter' || ev.key === ' ') {
                                  void input.onContextClick();
                                }
                              }}
                              aria-description=${i18nString(UIStrings.revealContextDescription)}
                            >${input.context.getTitle()}</span>`
                        }
                        ${input.isContextSelected && input.onContextRemoved ? html`
                                  <devtools-button
                                    title=${getContextRemoveLabel(input.context)}
                                    aria-label=${getContextRemoveLabel(input.context)}
                                    class="remove-context"
                                    .iconName=${'cross'}
                                    .size=${Buttons.Button.Size.MICRO}
                                    .jslogContext=${'context-removed'}
                                    .variant=${Buttons.Button.Variant.ICON}
                                    @click=${input.onContextRemoved}></devtools-button>` : Lit.nothing}
                      ${!input.isContextSelected && input.onContextAdd ? html`
                                    <devtools-button
                                      title=${lockedString(UIStringsNotTranslate.addContext)}
                                      aria-label=${lockedString(UIStringsNotTranslate.addContext)}
                                      class="add-context"
                                      .iconName=${'plus'}
                                      .size=${Buttons.Button.Size.MICRO}
                                      .jslogContext=${'context-added'}
                                      .variant=${Buttons.Button.Variant.ICON}
                                      @click=${input.onContextAdd}></devtools-button>` : Lit.nothing}
                      </div>
                    </div>`
                  : Lit.nothing}
              </div>
              <div class="chat-input-actions-right">
                <div class="chat-input-disclaimer-container">
                  ${renderRelevantDataDisclaimer(RELEVANT_DATA_LINK_CHAT_ID)}
                </div>
                ${(input.multimodalInputEnabled && !input.blockedByCrossOrigin) ?
                  html`
                    ${input.uploadImageInputEnabled ?
                      html`
                        <devtools-button
                          class="chat-input-button"
                          aria-label=${lockedString(UIStringsNotTranslate.addImageButtonTitle)}
                          @click=${input.onImageUpload}
                          .data=${{
                            variant: Buttons.Button.Variant.ICON,
                            size: Buttons.Button.Size.REGULAR,
                            disabled: input.isTextInputDisabled || input.imageInput?.isLoading,
                            iconName: 'add-photo',
                            title: lockedString(UIStringsNotTranslate.addImageButtonTitle),
                            jslogContext: 'upload-image',
                          } as Buttons.Button.ButtonData}
                        ></devtools-button>`
                      : Lit.nothing}
                    <devtools-button
                      class="chat-input-button"
                      aria-label=${lockedString(UIStringsNotTranslate.takeScreenshotButtonTitle)}
                      @click=${input.onTakeScreenshot}
                      .data=${{
                        variant: Buttons.Button.Variant.ICON,
                        size: Buttons.Button.Size.REGULAR,
                        disabled: input.isTextInputDisabled || input.imageInput?.isLoading,
                        iconName: 'photo-camera',
                        title: lockedString(UIStringsNotTranslate.takeScreenshotButtonTitle),
                        jslogContext: 'take-screenshot',
                      } as Buttons.Button.ButtonData}
                    ></devtools-button>`
                  : Lit.nothing}
                ${input.isLoading ?
                  html`
                    <devtools-button
                      class="chat-input-button"
                      aria-label=${lockedString(UIStringsNotTranslate.cancelButtonTitle)}
                      @click=${input.onCancel}
                      .data=${{
                        variant: Buttons.Button.Variant.ICON,
                        size: Buttons.Button.Size.REGULAR,
                        iconName: 'record-stop',
                        title: lockedString(UIStringsNotTranslate.cancelButtonTitle),
                        jslogContext: 'stop',
                      } as Buttons.Button.ButtonData}
                    ></devtools-button>`
                  :
                  input.blockedByCrossOrigin ?
                    html`
                      <devtools-button
                        class="start-new-chat-button"
                        aria-label=${lockedString(UIStringsNotTranslate.startNewChat)}
                        @click=${input.onNewConversation}
                        .data=${{
                          variant: Buttons.Button.Variant.OUTLINED,
                          size: Buttons.Button.Size.SMALL,
                          title: lockedString(UIStringsNotTranslate.startNewChat),
                          jslogContext: 'start-new-chat',
                        } as Buttons.Button.ButtonData}
                      >${lockedString(UIStringsNotTranslate.startNewChat)}</devtools-button>`
                    :
                    html`
                      <devtools-button
                        class="chat-input-button"
                        aria-label=${lockedString(UIStringsNotTranslate.sendButtonTitle)}
                        .data=${{
                          type: 'submit',
                          variant: Buttons.Button.Variant.ICON,
                          size: Buttons.Button.Size.REGULAR,
                          disabled: input.isTextInputDisabled || input.isTextInputEmpty || input.imageInput?.isLoading,
                          iconName: 'send',
                          title: lockedString(UIStringsNotTranslate.sendButtonTitle),
                          jslogContext: 'send',
                        } as Buttons.Button.ButtonData}
                      ></devtools-button>`
                }
              </div>
            </div>
          </div>
        </form>`
    }
    <footer
      class=${Lit.Directives.classMap({
        'chat-input-footer': true,
        'is-read-only': input.isReadOnly,
      })}
      jslog=${VisualLogging.section('footer')}
    >
      ${renderRelevantDataDisclaimer(RELEVANT_DATA_LINK_FOOTER_ID)}
    </footer>
  `, target,);
  // clang-format on
};

/**
 * ChatInput is a presenter for the input area in the AI Assistance panel.
 */
export class ChatInput extends UI.Widget.Widget implements SDK.TargetManager.Observer {
  isLoading = false;
  blockedByCrossOrigin = false;
  isTextInputDisabled = false;
  inputPlaceholder = '' as Platform.UIString.LocalizedString;
  context: AiAssistanceModel.AiAgent.ConversationContext<unknown>|null = null;
  isContextSelected = false;
  inspectElementToggled = false;
  disclaimerText = '';
  conversationType = AiAssistanceModel.AiHistoryStorage.ConversationType.STYLING;
  multimodalInputEnabled = false;
  uploadImageInputEnabled = false;
  isReadOnly = false;

  #textAreaRef = createRef<HTMLTextAreaElement>();
  #imageInput?: ImageInputData;

  setInputValue(text: string): void {
    if (this.#textAreaRef.value) {
      this.#textAreaRef.value.value = text;
    }
    this.performUpdate();
  }

  #isTextInputEmpty(): boolean {
    return !this.#textAreaRef.value?.value?.trim();
  }

  onTextSubmit:
      (text: string, imageInput?: Host.AidaClient.Part,
       multimodalInputType?: AiAssistanceModel.AiAgent.MultimodalInputType) => void = () => {};
  onContextClick = (): void => {};
  onInspectElementClick = (): void => {};
  onCancelClick = (): void => {};
  onNewConversation = (): void => {};
  onContextRemoved: (() => void)|null = null;
  onContextAdd: (() => void)|null = null;

  async #handleTakeScreenshot(): Promise<void> {
    const mainTarget = SDK.TargetManager.TargetManager.instance().primaryPageTarget();
    if (!mainTarget) {
      throw new Error('Could not find main target');
    }
    const model = mainTarget.model(SDK.ScreenCaptureModel.ScreenCaptureModel);
    if (!model) {
      throw new Error('Could not find model');
    }
    const showLoadingTimeout = setTimeout(() => {
      this.#imageInput = {isLoading: true};
      this.performUpdate();
    }, SHOW_LOADING_STATE_TIMEOUT);
    const bytes = await model.captureScreenshot(
        Protocol.Page.CaptureScreenshotRequestFormat.Jpeg,
        SCREENSHOT_QUALITY,
        SDK.ScreenCaptureModel.ScreenshotMode.FROM_VIEWPORT,
    );
    clearTimeout(showLoadingTimeout);
    if (bytes) {
      this.#imageInput = {
        isLoading: false,
        data: bytes,
        mimeType: JPEG_MIME_TYPE,
        inputType: AiAssistanceModel.AiAgent.MultimodalInputType.SCREENSHOT
      };
      this.performUpdate();
      void this.updateComplete.then(() => {
        this.focusTextInput();
      });
    } else {
      this.#imageInput = undefined;
      this.performUpdate();
      Snackbars.Snackbar.Snackbar.show({message: lockedString(UIStringsNotTranslate.screenshotFailureMessage)});
    }
  }

  targetAdded(_target: SDK.Target.Target): void {
  }
  targetRemoved(_target: SDK.Target.Target): void {
  }

  #handleRemoveImageInput(): void {
    this.#imageInput = undefined;
    this.performUpdate();
    void this.updateComplete.then(() => {
      this.focusTextInput();
    });
  }

  #handleImageDataTransferEvent(dataTransfer: DataTransfer|null, event: Event): void {
    if (this.conversationType !== AiAssistanceModel.AiHistoryStorage.ConversationType.STYLING) {
      return;
    }

    const files = dataTransfer?.files;
    if (!files || files.length === 0) {
      return;
    }

    const imageFile = Array.from(files).find(file => file.type.startsWith('image/'));
    if (!imageFile) {
      return;
    }

    event.preventDefault();
    void this.#handleLoadImage(imageFile);
  }

  #handleImagePaste = (event: ClipboardEvent): void => {
    this.#handleImageDataTransferEvent(event.clipboardData, event);
  };

  #handleImageDragOver = (event: DragEvent): void => {
    if (this.conversationType !== AiAssistanceModel.AiHistoryStorage.ConversationType.STYLING) {
      return;
    }

    event.preventDefault();
  };

  #handleImageDrop = (event: DragEvent): void => {
    this.#handleImageDataTransferEvent(event.dataTransfer, event);
  };

  async #handleLoadImage(file: File): Promise<void> {
    const showLoadingTimeout = setTimeout(() => {
      this.#imageInput = {isLoading: true};
      this.performUpdate();
    }, SHOW_LOADING_STATE_TIMEOUT);
    try {
      const reader = new FileReader();
      const dataUrl = await new Promise<string>((resolve, reject) => {
        reader.onload = () => {
          if (typeof reader.result === 'string') {
            resolve(reader.result);
          } else {
            reject(new Error('FileReader result was not a string.'));
          }
        };
        reader.readAsDataURL(file);
      });
      const commaIndex = dataUrl.indexOf(',');
      const bytes = dataUrl.substring(commaIndex + 1);
      this.#imageInput = {
        isLoading: false,
        data: bytes,
        mimeType: file.type,
        inputType: AiAssistanceModel.AiAgent.MultimodalInputType.UPLOADED_IMAGE
      };
    } catch {
      this.#imageInput = undefined;
      Snackbars.Snackbar.Snackbar.show({message: lockedString(UIStringsNotTranslate.uploadImageFailureMessage)});
    }

    clearTimeout(showLoadingTimeout);
    this.performUpdate();
    void this.updateComplete.then(() => {
      this.focusTextInput();
    });
  }

  #view: typeof DEFAULT_VIEW;

  constructor(element?: HTMLElement, view?: typeof DEFAULT_VIEW) {
    super(element);
    this.#view = view ?? DEFAULT_VIEW;
  }

  override wasShown(): void {
    super.wasShown();
    SDK.TargetManager.TargetManager.instance().addModelListener(
        SDK.ResourceTreeModel.ResourceTreeModel, SDK.ResourceTreeModel.Events.PrimaryPageChanged,
        this.#onPrimaryPageChanged, this);
  }

  override willHide(): void {
    super.willHide();
    SDK.TargetManager.TargetManager.instance().removeModelListener(
        SDK.ResourceTreeModel.ResourceTreeModel, SDK.ResourceTreeModel.Events.PrimaryPageChanged,
        this.#onPrimaryPageChanged, this);
  }

  #onPrimaryPageChanged(): void {
    this.#imageInput = undefined;
    this.performUpdate();
  }

  override performUpdate(): void {
    this.#view(
        {
          inputPlaceholder: this.inputPlaceholder,
          isLoading: this.isLoading,
          blockedByCrossOrigin: this.blockedByCrossOrigin,
          isTextInputDisabled: this.isTextInputDisabled,
          context: this.context,
          isContextSelected: this.isContextSelected,
          inspectElementToggled: this.inspectElementToggled,
          isTextInputEmpty: this.#isTextInputEmpty(),
          disclaimerText: this.disclaimerText,
          conversationType: this.conversationType,
          multimodalInputEnabled: this.multimodalInputEnabled,
          imageInput: this.#imageInput,
          uploadImageInputEnabled: this.uploadImageInputEnabled,
          isReadOnly: this.isReadOnly,
          textAreaRef: this.#textAreaRef,
          onContextClick: this.onContextClick,
          onInspectElementClick: this.onInspectElementClick,
          onImagePaste: this.#handleImagePaste,
          onNewConversation: this.onNewConversation,
          onTextInputChange: () => {
            this.requestUpdate();
          },
          onTakeScreenshot: this.#handleTakeScreenshot.bind(this),
          onRemoveImageInput: this.#handleRemoveImageInput.bind(this),
          onSubmit: this.onSubmit,
          onTextAreaKeyDown: this.onTextAreaKeyDown,
          onCancel: this.onCancel,
          onImageUpload: this.onImageUpload,
          onImageDragOver: this.#handleImageDragOver,
          onImageDrop: this.#handleImageDrop,
          onContextRemoved: this.onContextRemoved,
          onContextAdd: this.onContextAdd,
        },
        undefined, this.contentElement);
  }

  focusTextInput(): void {
    this.#textAreaRef.value?.focus();
  }

  onSubmit = (event: SubmitEvent): void => {
    event.preventDefault();
    if (this.#imageInput?.isLoading) {
      return;
    }
    const imageInput = !this.#imageInput?.isLoading && this.#imageInput?.data ?
        {inlineData: {data: this.#imageInput.data, mimeType: this.#imageInput.mimeType}} :
        undefined;
    this.onTextSubmit(this.#textAreaRef.value?.value ?? '', imageInput, this.#imageInput?.inputType);
    this.#imageInput = undefined;
    this.setInputValue('');
  };

  onTextAreaKeyDown = (event: KeyboardEvent): void => {
    if (!event.target || !(event.target instanceof HTMLTextAreaElement)) {
      return;
    }

    // Go to a new line on Shift+Enter. On Enter, submit unless the
    // user is in IME composition.
    if (event.key === 'Enter' && !event.shiftKey && !event.isComposing) {
      event.preventDefault();
      if (!event.target?.value || this.#imageInput?.isLoading) {
        return;
      }
      const imageInput = !this.#imageInput?.isLoading && this.#imageInput?.data ?
          {inlineData: {data: this.#imageInput.data, mimeType: this.#imageInput.mimeType}} :
          undefined;
      this.onTextSubmit(event.target.value, imageInput, this.#imageInput?.inputType);
      this.#imageInput = undefined;
      this.setInputValue('');
    }
  };

  onCancel = (ev: SubmitEvent): void => {
    ev.preventDefault();

    if (!this.isLoading) {
      return;
    }

    this.onCancelClick();
  };

  onImageUpload = (ev: Event): void => {
    ev.stopPropagation();
    const fileSelector = UI.UIUtils.createFileSelectorElement(this.#handleLoadImage.bind(this), '.jpeg,.jpg,.png');
    fileSelector.click();
  };
}
