Onboarding flow for console insight

Bug: b/299423255
Change-Id: I81cf61499cbd87eebbcd385b8ebe53554b657efe
Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/5331780
Reviewed-by: Danil Somsikov <dsv@chromium.org>
Commit-Queue: Alex Rudenko <alexrudenko@chromium.org>
diff --git a/front_end/panels/explain/BUILD.gn b/front_end/panels/explain/BUILD.gn
index a507a4e..ad85390 100644
--- a/front_end/panels/explain/BUILD.gn
+++ b/front_end/panels/explain/BUILD.gn
@@ -24,6 +24,7 @@
 
   deps = [
     ":css_files",
+    "../../core/common:bundle",
     "../../core/host:bundle",
     "../../core/sdk:bundle",
     "../../models/logs:bundle",
@@ -73,10 +74,12 @@
   sources = [
     "PromptBuilder.test.ts",
     "components/ConsoleInsight.test.ts",
+    "components/MarkdownRenderer.test.ts",
   ]
 
   deps = [
     ":bundle",
+    "../../core/common:bundle",
     "../../testing",
   ]
 }
diff --git a/front_end/panels/explain/components/ConsoleInsight.test.ts b/front_end/panels/explain/components/ConsoleInsight.test.ts
index 652c8ad..a5e4244 100644
--- a/front_end/panels/explain/components/ConsoleInsight.test.ts
+++ b/front_end/panels/explain/components/ConsoleInsight.test.ts
@@ -2,89 +2,164 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
+import * as Common from '../../../core/common/common.js';
 import * as Host from '../../../core/host/host.js';
 import {dispatchClickEvent, renderElementIntoDOM} from '../../../testing/DOMHelpers.js';
-import {describeWithLocale} from '../../../testing/EnvironmentHelpers.js';
-import type * as Marked from '../../../third_party/marked/marked.js';
+import {describeWithEnvironment} from '../../../testing/EnvironmentHelpers.js';
 import * as Explain from '../explain.js';
 
 const {assert} = chai;
 
-describeWithLocale('ConsoleInsight', () => {
-  describe('Markdown renderer', () => {
-    it('renders link as an x-link', () => {
-      const renderer = new Explain.MarkdownRenderer();
-      const result =
-          renderer.renderToken({type: 'link', text: 'learn more', href: 'exampleLink'} as Marked.Marked.Token);
-      assert((result.values[0] as HTMLElement).tagName === 'X-LINK');
+describeWithEnvironment('ConsoleInsight', () => {
+  function getTestAidaClient() {
+    return {
+      async *
+          fetch() {
+            yield {explanation: 'test', metadata: {}};
+          },
+    };
+  }
+
+  function getTestPromptBuilder() {
+    return {
+      async buildPrompt() {
+        return {
+          prompt: '',
+          sources: [
+            {
+              type: Explain.SourceType.MESSAGE,
+              value: 'error message',
+            },
+          ],
+        };
+      },
+    };
+  }
+
+  async function drainMicroTasks() {
+    await new Promise(resolve => setTimeout(resolve, 0));
+  }
+
+  function skipConsentOnboarding() {
+    beforeEach(() => {
+      Common.Settings.settingForTest('console-insights-onboarding-finished').set(true);
     });
-    it('renders images as an x-link', () => {
-      const renderer = new Explain.MarkdownRenderer();
-      const result =
-          renderer.renderToken({type: 'image', text: 'learn more', href: 'exampleLink'} as Marked.Marked.Token);
-      assert((result.values[0] as HTMLElement).tagName === 'X-LINK');
+
+    afterEach(() => {
+      Common.Settings.settingForTest('console-insights-onboarding-finished').set(false);
     });
-    it('renders headers as a strong element', () => {
-      const renderer = new Explain.MarkdownRenderer();
-      const result = renderer.renderToken({type: 'heading', text: 'learn more'} as Marked.Marked.Token);
-      assert(result.strings.join('').includes('<strong>'));
+  }
+
+  describe('consent onboarding', () => {
+    afterEach(() => {
+      Common.Settings.settingForTest('console-insights-onboarding-finished').set(false);
     });
-    it('renders unsupported tokens', () => {
-      const renderer = new Explain.MarkdownRenderer();
-      const result = renderer.renderToken({type: 'html', raw: '<!DOCTYPE html>'} as Marked.Marked.Token);
-      assert(result.values.join('').includes('<!DOCTYPE html>'));
+
+    it('should show privacy notice first', async () => {
+      const component = new Explain.ConsoleInsight(getTestPromptBuilder(), getTestAidaClient(), '', {
+        isSyncActive: true,
+        accountEmail: 'some-email',
+      });
+      renderElementIntoDOM(component);
+      await drainMicroTasks();
+      assert.strictEqual(component.shadowRoot!.querySelector('h2')?.innerText, 'Console insights Privacy Notice');
+    });
+
+    it('should show legal notice second', async () => {
+      const component = new Explain.ConsoleInsight(getTestPromptBuilder(), getTestAidaClient(), '', {
+        isSyncActive: true,
+        accountEmail: 'some-email',
+      });
+      renderElementIntoDOM(component);
+      await drainMicroTasks();
+      dispatchClickEvent(component.shadowRoot!.querySelector('.next-button')!);
+      await drainMicroTasks();
+      assert.strictEqual(component.shadowRoot!.querySelector('h2')?.innerText, 'Console insights Legal Notice');
+    });
+
+    it('should not confirm legal notice without checkbox', async () => {
+      const component = new Explain.ConsoleInsight(getTestPromptBuilder(), getTestAidaClient(), '', {
+        isSyncActive: true,
+        accountEmail: 'some-email',
+      });
+      renderElementIntoDOM(component);
+      await drainMicroTasks();
+      dispatchClickEvent(component.shadowRoot!.querySelector('.next-button')!);
+      await drainMicroTasks();
+      dispatchClickEvent(component.shadowRoot!.querySelector('.continue-button')!);
+      await drainMicroTasks();
+      assert.strictEqual(component.shadowRoot!.querySelector('h2')?.innerText, 'Console insights Legal Notice');
+    });
+
+    it('should confirm legal notice if checkbox is pressed', async () => {
+      const component =
+          new Explain.ConsoleInsight(getTestPromptBuilder(), getTestAidaClient(), 'Understand this error', {
+            isSyncActive: true,
+            accountEmail: 'some-email',
+          });
+      renderElementIntoDOM(component);
+      await drainMicroTasks();
+      dispatchClickEvent(component.shadowRoot!.querySelector('.next-button')!);
+      await drainMicroTasks();
+      dispatchClickEvent(component.shadowRoot!.querySelector('input')!);
+      await drainMicroTasks();
+      dispatchClickEvent(component.shadowRoot!.querySelector('.continue-button')!);
+      await drainMicroTasks();
+      assert.strictEqual(component.shadowRoot!.querySelector('h2')?.innerText, 'Understand this error');
+      await drainMicroTasks();
+      assert.strictEqual(Common.Settings.settingForTest('console-insights-onboarding-finished').get(), true);
+    });
+
+    it('can cancel the onboarding flow', async () => {
+      const component = new Explain.ConsoleInsight(getTestPromptBuilder(), getTestAidaClient(), '', {
+        isSyncActive: true,
+        accountEmail: 'some-email',
+      });
+      renderElementIntoDOM(component);
+      await drainMicroTasks();
+      dispatchClickEvent(component.shadowRoot!.querySelector('.cancel-button')!);
+      await drainMicroTasks();
+      assert(component.getAttribute('class')?.includes('closing'));
+    });
+
+    it('can disable the feature', async () => {
+      Common.Settings.settingForTest('console-insights-enabled').set(true);
+      const component = new Explain.ConsoleInsight(getTestPromptBuilder(), getTestAidaClient(), '', {
+        isSyncActive: true,
+        accountEmail: 'some-email',
+      });
+      renderElementIntoDOM(component);
+      await drainMicroTasks();
+      dispatchClickEvent(component.shadowRoot!.querySelector('.disable-button')!);
+      await drainMicroTasks();
+      assert.strictEqual(Common.Settings.settingForTest('console-insights-enabled').get(), false);
     });
   });
 
-  describe('ConsoleInsight', () => {
-    function getTestAidaClient() {
-      return {
-        async *
-            fetch() {
-              yield {explanation: 'test', metadata: {}};
-            },
-      };
-    }
+  describe('with consent onboarding finished', () => {
+    skipConsentOnboarding();
 
-    function getTestPromptBuilder() {
-      return {
-        async buildPrompt() {
-          return {
-            prompt: '',
-            sources: [
-              {
-                type: Explain.SourceType.MESSAGE,
-                value: 'error message',
-              },
-            ],
-          };
-        },
-      };
-    }
-
-    async function drainMicroTasks() {
-      await new Promise(resolve => setTimeout(resolve, 0));
-    }
-
-    it('shows the consent flow for signed-in users', async () => {
-      const component = new Explain.ConsoleInsight(getTestPromptBuilder(), getTestAidaClient(), '', {
-        isSyncActive: true,
-        accountEmail: 'some-email',
-      });
+    it('shows the consent reminder flow for signed-in users', async () => {
+      const component =
+          new Explain.ConsoleInsight(getTestPromptBuilder(), getTestAidaClient(), 'Understand this error', {
+            isSyncActive: true,
+            accountEmail: 'some-email',
+          });
       renderElementIntoDOM(component);
       await drainMicroTasks();
-      // Consent button is present.
-      assert(component.shadowRoot!.querySelector('.consent-button'));
+      assert.strictEqual(component.shadowRoot!.querySelector('h2')?.innerText, 'Understand this error');
+      // Continue button is present.
+      assert(component.shadowRoot!.querySelector('.continue-button'));
     });
 
-    it('consent can be accepted', async () => {
+    it('consent reminder can be accepted', async () => {
       const component = new Explain.ConsoleInsight(getTestPromptBuilder(), getTestAidaClient(), '', {
         isSyncActive: true,
         accountEmail: 'some-email',
       });
       renderElementIntoDOM(component);
       await drainMicroTasks();
-      dispatchClickEvent(component.shadowRoot!.querySelector('.consent-button')!, {
+      dispatchClickEvent(component.shadowRoot!.querySelector('.continue-button')!, {
         bubbles: true,
         composed: true,
       });
@@ -106,7 +181,7 @@
       });
       renderElementIntoDOM(component);
       await drainMicroTasks();
-      dispatchClickEvent(component.shadowRoot!.querySelector('.consent-button')!, {
+      dispatchClickEvent(component.shadowRoot!.querySelector('.continue-button')!, {
         bubbles: true,
         composed: true,
       });
@@ -127,18 +202,38 @@
 
     it('reports positive rating', reportsRating(true));
     it('reports negative rating', reportsRating(false));
+  });
 
-    it('report if the user is not logged in', async () => {
-      const component = new Explain.ConsoleInsight(getTestPromptBuilder(), getTestAidaClient(), '', {
-        isSyncActive: false,
-      });
-      renderElementIntoDOM(component);
-      await drainMicroTasks();
-      const content = component.shadowRoot!.querySelector('main')!.innerText.trim();
-      assert.strictEqual(content, 'This feature is only available when you sign into Chrome with your Google account.');
+  it('report if the user is not logged in', async () => {
+    const component = new Explain.ConsoleInsight(getTestPromptBuilder(), getTestAidaClient(), '', {
+      isSyncActive: false,
+    });
+    renderElementIntoDOM(component);
+    await drainMicroTasks();
+    const content = component.shadowRoot!.querySelector('main')!.innerText.trim();
+    assert.strictEqual(content, 'This feature is only available when you sign into Chrome with your Google account.');
+  });
+
+  it('report if the sync is not enabled', async () => {
+    const component = new Explain.ConsoleInsight(getTestPromptBuilder(), getTestAidaClient(), '', {
+      isSyncActive: false,
+      accountEmail: 'some-email',
+    });
+    renderElementIntoDOM(component);
+    await drainMicroTasks();
+    const content = component.shadowRoot!.querySelector('main')!.innerText.trim();
+    assert.strictEqual(content, 'This feature requires you to turn on Chrome sync.');
+  });
+
+  it('report if the navigator is offline', async () => {
+    const navigatorDescriptor = Object.getOwnPropertyDescriptor(globalThis, 'navigator')!;
+    Object.defineProperty(globalThis, 'navigator', {
+      get() {
+        return {onLine: false};
+      },
     });
 
-    it('report if the sync is not enabled', async () => {
+    try {
       const component = new Explain.ConsoleInsight(getTestPromptBuilder(), getTestAidaClient(), '', {
         isSyncActive: false,
         accountEmail: 'some-email',
@@ -146,29 +241,9 @@
       renderElementIntoDOM(component);
       await drainMicroTasks();
       const content = component.shadowRoot!.querySelector('main')!.innerText.trim();
-      assert.strictEqual(content, 'This feature requires you to turn on Chrome sync.');
-    });
-
-    it('report if the navigator is offline', async () => {
-      const navigatorDescriptor = Object.getOwnPropertyDescriptor(globalThis, 'navigator')!;
-      Object.defineProperty(globalThis, 'navigator', {
-        get() {
-          return {onLine: false};
-        },
-      });
-
-      try {
-        const component = new Explain.ConsoleInsight(getTestPromptBuilder(), getTestAidaClient(), '', {
-          isSyncActive: false,
-          accountEmail: 'some-email',
-        });
-        renderElementIntoDOM(component);
-        await drainMicroTasks();
-        const content = component.shadowRoot!.querySelector('main')!.innerText.trim();
-        assert.strictEqual(content, 'Check your internet connection and try again.');
-      } finally {
-        Object.defineProperty(globalThis, 'navigator', navigatorDescriptor);
-      }
-    });
+      assert.strictEqual(content, 'Check your internet connection and try again.');
+    } finally {
+      Object.defineProperty(globalThis, 'navigator', navigatorDescriptor);
+    }
   });
 });
diff --git a/front_end/panels/explain/components/ConsoleInsight.ts b/front_end/panels/explain/components/ConsoleInsight.ts
index bdd6aa7e..7c57ceb 100644
--- a/front_end/panels/explain/components/ConsoleInsight.ts
+++ b/front_end/panels/explain/components/ConsoleInsight.ts
@@ -2,6 +2,7 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
+import * as Common from '../../../core/common/common.js';
 import * as Host from '../../../core/host/host.js';
 import * as i18n from '../../../core/i18n/i18n.js';
 import type * as Platform from '../../../core/platform/platform.js';
@@ -9,6 +10,7 @@
 import * as Marked from '../../../third_party/marked/marked.js';
 import * as Buttons from '../../../ui/components/buttons/buttons.js';
 import * as IconButton from '../../../ui/components/icon_button/icon_button.js';
+import * as Input from '../../../ui/components/input/input.js';
 import * as MarkdownView from '../../../ui/components/markdown_view/markdown_view.js';
 import * as UI from '../../../ui/legacy/legacy.js';
 import * as LitHtml from '../../../ui/lit-html/lit-html.js';
@@ -80,11 +82,6 @@
    */
   opensInNewTab: '(opens in a new tab)',
   /**
-   * @description The title of the button that records the consent of the user
-   * to send the data to the backend.
-   */
-  consentButton: 'Continue',
-  /**
    * @description The title of a link that allows the user to learn more about
    * the feature.
    */
@@ -186,15 +183,22 @@
   INSIGHT = 'insight',
   LOADING = 'loading',
   ERROR = 'error',
-  CONSENT = 'consent',
+  CONSENT_ONBOARDING = 'consent-onboarding',
+  CONSENT_REMINDER = 'consent-reminder',
   NOT_LOGGED_IN = 'not-logged-in',
   SYNC_IS_OFF = 'sync-is-off',
   OFFLINE = 'offline',
 }
 
+const enum ConsentOnboardingPage {
+  PAGE1 = 'private',
+  PAGE2 = 'legal',
+}
+
 type StateData = {
   type: State.LOADING,
-  consentGiven: boolean,
+  consentOnboardingFinished: boolean,
+  consentReminderConfirmed: boolean,
 }|{
   type: State.INSIGHT,
   tokens: MarkdownView.MarkdownView.MarkdownViewData['tokens'],
@@ -204,9 +208,12 @@
   type: State.ERROR,
   error: string,
 }|{
-  type: State.CONSENT,
+  type: State.CONSENT_REMINDER,
   sources: Source[],
 }|{
+  type: State.CONSENT_ONBOARDING,
+  page: ConsentOnboardingPage,
+}|{
   type: State.NOT_LOGGED_IN,
 }|{
   type: State.SYNC_IS_OFF,
@@ -254,7 +261,8 @@
     if (syncInfo?.accountEmail && syncInfo.isSyncActive) {
       this.#state = {
         type: State.LOADING,
-        consentGiven: false,
+        consentReminderConfirmed: false,
+        consentOnboardingFinished: this.#getOnboardingCompletedSetting().get(),
       };
     } else if (!syncInfo?.accountEmail) {
       this.#state = {
@@ -294,8 +302,12 @@
     });
   }
 
+  #getOnboardingCompletedSetting(): Common.Settings.Setting<boolean> {
+    return Common.Settings.Settings.instance().createLocalSetting('console-insights-onboarding-finished', false);
+  }
+
   connectedCallback(): void {
-    this.#shadow.adoptedStyleSheets = [styles];
+    this.#shadow.adoptedStyleSheets = [styles, Input.checkboxStyles];
     this.classList.add('opening');
     void this.#generateInsightIfNeeded();
   }
@@ -313,14 +325,21 @@
     if (this.#state.type !== State.LOADING) {
       return;
     }
-    if (this.#state.consentGiven) {
+    if (!this.#state.consentOnboardingFinished) {
+      this.#transitionTo({
+        type: State.CONSENT_ONBOARDING,
+        page: ConsentOnboardingPage.PAGE1,
+      });
       return;
     }
-    const {sources} = await this.#promptBuilder.buildPrompt();
-    this.#transitionTo({
-      type: State.CONSENT,
-      sources,
-    });
+    if (!this.#state.consentReminderConfirmed) {
+      const {sources} = await this.#promptBuilder.buildPrompt();
+      this.#transitionTo({
+        type: State.CONSENT_REMINDER,
+        sources,
+      });
+      return;
+    }
   }
 
   #onClose(): void {
@@ -365,10 +384,11 @@
     this.#openFeedbackFrom();
   }
 
-  async #onConsent(): Promise<void> {
+  async #onConsentReminderConfirmed(): Promise<void> {
     this.#transitionTo({
       type: State.LOADING,
-      consentGiven: true,
+      consentReminderConfirmed: true,
+      consentOnboardingFinished: this.#getOnboardingCompletedSetting().get(),
     });
     try {
       for await (const {sources, explanation, metadata} of this.#getInsight()) {
@@ -434,6 +454,128 @@
     });
   }
 
+  #onDisableFeature(): void {
+    try {
+      Common.Settings.moduleSetting('console-insights-enabled').set(false);
+    } finally {
+      this.#onClose();
+      UI.InspectorView.InspectorView.instance().displayReloadRequiredWarning('Reload for the change to apply.');
+    }
+  }
+
+  #goToNextPage(): void {
+    this.#transitionTo({
+      type: State.CONSENT_ONBOARDING,
+      page: ConsentOnboardingPage.PAGE2,
+    });
+  }
+
+  #onConsentOnboardingConfirmed(): void {
+    const checkbox = this.#shadow.querySelector('.terms') as HTMLInputElement | undefined;
+    if (!checkbox?.checked) {
+      return;
+    }
+    this.#getOnboardingCompletedSetting().set(true);
+    this.#transitionTo({
+      type: State.LOADING,
+      consentReminderConfirmed: false,
+      consentOnboardingFinished: this.#getOnboardingCompletedSetting().get(),
+    });
+    void this.#generateInsightIfNeeded();
+  }
+
+  #goToPrevPage(): void {
+    this.#transitionTo({
+      type: State.CONSENT_ONBOARDING,
+      page: ConsentOnboardingPage.PAGE1,
+    });
+  }
+
+  #renderCancelButton(): LitHtml.TemplateResult {
+    // clang-format off
+    return html`<${Buttons.Button.Button.litTagName}
+      class="cancel-button"
+      @click=${this.#onClose}
+      .data=${
+        {
+          variant: Buttons.Button.Variant.SECONDARY,
+        } as Buttons.Button.ButtonData
+      }
+    >
+      ${UIStrings.cancel}
+    </${Buttons.Button.Button.litTagName}>`;
+    // clang-format on
+  }
+
+  #renderDisableFeatureButton(): LitHtml.TemplateResult {
+    // clang-format off
+    return html`<${Buttons.Button.Button.litTagName}
+      @click=${this.#onDisableFeature}
+      class="disable-button"
+      .data=${
+        {
+          variant: Buttons.Button.Variant.SECONDARY,
+        } as Buttons.Button.ButtonData
+      }
+    >
+      Disable this feature
+    </${Buttons.Button.Button.litTagName}>`;
+    // clang-format on
+  }
+
+  #renderNextButton(): LitHtml.TemplateResult {
+    // clang-format off
+    return html`<${Buttons.Button.Button.litTagName}
+      class="next-button"
+      @click=${this.#goToNextPage}
+      .data=${
+        {
+          variant: Buttons.Button.Variant.PRIMARY,
+        } as Buttons.Button.ButtonData
+      }
+    >
+      Next
+    </${Buttons.Button.Button.litTagName}>`;
+    // clang-format on
+  }
+
+  #renderBackButton(): LitHtml.TemplateResult {
+    // clang-format off
+    return html`<${Buttons.Button.Button.litTagName}
+      @click=${this.#goToPrevPage}
+      .data=${
+        {
+          variant: Buttons.Button.Variant.SECONDARY,
+        } as Buttons.Button.ButtonData
+      }
+    >
+      Back
+    </${Buttons.Button.Button.litTagName}>`;
+    // clang-format on
+  }
+
+  #renderContinueButton(handler: (event: Event) => void): LitHtml.TemplateResult {
+    // clang-format off
+    return html`<${Buttons.Button.Button.litTagName}
+      @click=${handler}
+      class="continue-button"
+      .data=${
+        {
+          variant: Buttons.Button.Variant.PRIMARY,
+        } as Buttons.Button.ButtonData
+      }
+    >
+      Continue
+    </${Buttons.Button.Button.litTagName}>`;
+    // clang-format on
+  }
+
+  #renderLearnMoreAboutInsights(): LitHtml.TemplateResult {
+    // clang-format off
+    return html`<x-link href=${DOGFOODINFO_URL} class="link">Learn more about Console Insights.</x-link>`;
+    // clang-format on
+  }
+
   #renderMain(): LitHtml.TemplateResult {
     // clang-format off
     switch (this.#state.type) {
@@ -468,7 +610,7 @@
         <main>
           <div class="error">${i18nString(UIStrings.errorBody)}</div>
         </main>`;
-      case State.CONSENT:
+      case State.CONSENT_REMINDER:
         return html`
           <main>
             <p>The following data will be sent to Google to understand the context for the console message.
@@ -479,6 +621,36 @@
             </${ConsoleInsightSourcesList.litTagName}>
           </main>
         `;
+      case State.CONSENT_ONBOARDING:
+        switch (this.#state.page) {
+          case ConsentOnboardingPage.PAGE1:
+            return html`<main>
+              <p>This notice and our <x-link href="https://policies.google.com/privacy" class="link">Privacy Notice</x-link> describe how Console insights in Chrome DevTools handles your data. Please read them carefully.</p>
+
+              <p>Console insights uses the console message, associated stack trace, related source code, and the associated network headers as input data. When you use Console insights, Google collects this input data, generated output, related feature usage information, and your feedback. Google uses this data to provide, improve, and develop Google products and services and machine learning technologies, including Google's enterprise products such as Google Cloud.</p>
+
+              <p>To help with quality and improve our products, human reviewers may read, annotate, and process the above-mentioned input data, generated output, related feature usage information, and your feedback. <strong>Please do not include sensitive (e.g., confidential) or personal information that can be used to identify you or others in your prompts or feedback.</strong> Your data will be stored in a way where Google cannot tell who provided it and can no longer fulfill any deletion requests and will be retained for up to 18 months.</p>
+            </main>`;
+          case ConsentOnboardingPage.PAGE2:
+            return html`<main>
+            <p>As you try Console insights, here are key things to know:
+
+            <ul>
+              <li>Console insights uses console message, associated stack trace, related source code, and the associated network headers to provide answers.</li>
+              <li>Console insights is an experimental technology, and may generate inaccurate or offensive information that doesn't represent Google's views. Voting on the responses will help make Console Insights better.</li>
+              <li>Console insights is an experimental feature and subject to future changes.</li>
+              <li><strong><x-link class="link" href="https://support.google.com/legal/answer/13505487">Use generated code snippets with caution.</x-link></strong></li>
+            </ul>
+            </p>
+
+            <p>
+            <label>
+              <input class="terms" type="checkbox">
+              <span>I accept my use of Console insights is subject to the <x-link href="https://policies.google.com/terms" class="link">Google Terms of Service</x-link> and the <x-link href=${'https://policies.google.com/terms/gener' + 'ative-ai'} class="link">${'Gener' + 'ative'} AI Additional Terms of Service</x-link>.</span>
+            </label>
+            </p>
+            </main>`;
+        }
       case State.NOT_LOGGED_IN:
         return html`
           <main>
@@ -529,38 +701,44 @@
           </${Buttons.Button.Button.litTagName}>
         </div>
       </footer>`;
-      case State.CONSENT:
+      case State.CONSENT_REMINDER:
         return html`<footer>
           <div class="disclaimer">
             ${disclaimer}
           </div>
           <div class="filler"></div>
           <div class="buttons">
-            <${Buttons.Button.Button.litTagName}
-              class="cancel-button"
-              @click=${this.#onClose}
-              .data=${
-                {
-                  variant: Buttons.Button.Variant.SECONDARY,
-                } as Buttons.Button.ButtonData
-              }
-            >
-              ${UIStrings.cancel}
-            </${Buttons.Button.Button.litTagName}>
-            <${Buttons.Button.Button.litTagName}
-              class="consent-button"
-              @click=${this.#onConsent}
-              .data=${
-                {
-                  variant: Buttons.Button.Variant.PRIMARY,
-                  iconName: 'lightbulb-spark',
-                } as Buttons.Button.ButtonData
-              }
-            >
-              ${UIStrings.consentButton}
-            </${Buttons.Button.Button.litTagName}>
+            ${this.#renderCancelButton()}
+            ${this.#renderContinueButton(this.#onConsentReminderConfirmed)}
           </div>
         </footer>`;
+      case State.CONSENT_ONBOARDING:
+        switch (this.#state.page) {
+          case ConsentOnboardingPage.PAGE1:
+            return html`<footer>
+                <div class="disclaimer">
+                  ${this.#renderLearnMoreAboutInsights()}
+                </div>
+                <div class="filler"></div>
+                <div class="buttons">
+                    ${this.#renderCancelButton()}
+                    ${this.#renderDisableFeatureButton()}
+                    ${this.#renderNextButton()}
+                  </div>
+              </footer>`;
+          case ConsentOnboardingPage.PAGE2:
+            return html`<footer>
+            <div class="disclaimer">
+              ${this.#renderLearnMoreAboutInsights()}
+            </div>
+            <div class="filler"></div>
+            <div class="buttons">
+                ${this.#renderBackButton()}
+                ${this.#renderDisableFeatureButton()}
+                ${this.#renderContinueButton(this.#onConsentOnboardingConfirmed)}
+              </div>
+          </footer>`;
+        }
       case State.INSIGHT:
         return html`<footer>
         <div class="disclaimer">
@@ -615,8 +793,15 @@
         return i18nString(UIStrings.insight);
       case State.ERROR:
         return i18nString(UIStrings.error);
-      case State.CONSENT:
+      case State.CONSENT_REMINDER:
         return this.#actionTitle;
+      case State.CONSENT_ONBOARDING:
+        switch (this.#state.page) {
+          case ConsentOnboardingPage.PAGE1:
+            return 'Console insights Privacy Notice';
+          case ConsentOnboardingPage.PAGE2:
+            return 'Console insights Legal Notice';
+        }
     }
   }
 
@@ -662,7 +847,7 @@
 
   constructor() {
     super();
-    this.#shadow.adoptedStyleSheets = [listStyles];
+    this.#shadow.adoptedStyleSheets = [listStyles, Input.checkboxStyles];
   }
 
   #render(): void {
diff --git a/front_end/panels/explain/components/MarkdownRenderer.test.ts b/front_end/panels/explain/components/MarkdownRenderer.test.ts
new file mode 100644
index 0000000..2076453
--- /dev/null
+++ b/front_end/panels/explain/components/MarkdownRenderer.test.ts
@@ -0,0 +1,32 @@
+// Copyright 2023 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import type * as Marked from '../../../third_party/marked/marked.js';
+import * as Explain from '../explain.js';
+
+const {assert} = chai;
+
+describe('Markdown renderer', () => {
+  it('renders link as an x-link', () => {
+    const renderer = new Explain.MarkdownRenderer();
+    const result = renderer.renderToken({type: 'link', text: 'learn more', href: 'exampleLink'} as Marked.Marked.Token);
+    assert((result.values[0] as HTMLElement).tagName === 'X-LINK');
+  });
+  it('renders images as an x-link', () => {
+    const renderer = new Explain.MarkdownRenderer();
+    const result =
+        renderer.renderToken({type: 'image', text: 'learn more', href: 'exampleLink'} as Marked.Marked.Token);
+    assert((result.values[0] as HTMLElement).tagName === 'X-LINK');
+  });
+  it('renders headers as a strong element', () => {
+    const renderer = new Explain.MarkdownRenderer();
+    const result = renderer.renderToken({type: 'heading', text: 'learn more'} as Marked.Marked.Token);
+    assert(result.strings.join('').includes('<strong>'));
+  });
+  it('renders unsupported tokens', () => {
+    const renderer = new Explain.MarkdownRenderer();
+    const result = renderer.renderToken({type: 'html', raw: '<!DOCTYPE html>'} as Marked.Marked.Token);
+    assert(result.values.join('').includes('<!DOCTYPE html>'));
+  });
+});
diff --git a/front_end/panels/explain/components/consoleInsight.css b/front_end/panels/explain/components/consoleInsight.css
index c817f4b..59728dc 100644
--- a/front_end/panels/explain/components/consoleInsight.css
+++ b/front_end/panels/explain/components/consoleInsight.css
@@ -107,6 +107,24 @@
   font-style: normal;
   font-weight: 400;
   line-height: 20px;
+
+  p {
+    margin-block-start: 1em;
+    margin-block-end: 1em;
+  }
+
+  ul {
+    padding-left: 1em;
+  }
+
+  label {
+    display: inline-block;
+
+    input,
+    span {
+      vertical-align: middle;
+    }
+  }
 }
 
 devtools-markdown-view {
diff --git a/front_end/testing/EnvironmentHelpers.ts b/front_end/testing/EnvironmentHelpers.ts
index 959d20d..2f22d61 100644
--- a/front_end/testing/EnvironmentHelpers.ts
+++ b/front_end/testing/EnvironmentHelpers.ts
@@ -250,6 +250,12 @@
         Common.Settings.SettingCategory.CONSOLE, 'console-timestamps-enabled', false,
         Common.Settings.SettingType.BOOLEAN),
     createSettingValue(
+        Common.Settings.SettingCategory.CONSOLE, 'console-insights-enabled', false,
+        Common.Settings.SettingType.BOOLEAN),
+    createSettingValue(
+        Common.Settings.SettingCategory.CONSOLE, 'console-insights-onboarding-finished', false,
+        Common.Settings.SettingType.BOOLEAN),
+    createSettingValue(
         Common.Settings.SettingCategory.CONSOLE, 'console-history-autocomplete', false,
         Common.Settings.SettingType.BOOLEAN),
     createSettingValue(
diff --git a/test/interactions/goldens/linux/explain/console_insight.png b/test/interactions/goldens/linux/explain/console_insight.png
index 4991a0f..d00b4db 100644
--- a/test/interactions/goldens/linux/explain/console_insight.png
+++ b/test/interactions/goldens/linux/explain/console_insight.png
Binary files differ
diff --git a/test/interactions/goldens/linux/explain/console_insight_after_consent.png b/test/interactions/goldens/linux/explain/console_insight_after_consent.png
deleted file mode 100644
index ff5c629..0000000
--- a/test/interactions/goldens/linux/explain/console_insight_after_consent.png
+++ /dev/null
Binary files differ
diff --git a/test/interactions/goldens/linux/explain/console_insight_disclaimer1.png b/test/interactions/goldens/linux/explain/console_insight_disclaimer1.png
new file mode 100644
index 0000000..12ffddf
--- /dev/null
+++ b/test/interactions/goldens/linux/explain/console_insight_disclaimer1.png
Binary files differ
diff --git a/test/interactions/goldens/linux/explain/console_insight_disclaimer2.png b/test/interactions/goldens/linux/explain/console_insight_disclaimer2.png
new file mode 100644
index 0000000..17a5404
--- /dev/null
+++ b/test/interactions/goldens/linux/explain/console_insight_disclaimer2.png
Binary files differ
diff --git a/test/interactions/panels/explain/explain_test.ts b/test/interactions/panels/explain/explain_test.ts
index ef66a81..6abd0c3 100644
--- a/test/interactions/panels/explain/explain_test.ts
+++ b/test/interactions/panels/explain/explain_test.ts
@@ -11,17 +11,25 @@
   preloadForCodeCoverage('console_insight/static.html');
 
   // eslint-disable-next-line rulesdir/ban_screenshot_test_outside_perf_panel
-  itScreenshot('renders initial state', async () => {
+  itScreenshot('renders the initial disclaimer', async () => {
     await loadComponentDocExample('console_insight/static.html');
-    await waitFor('.consent-button');
-    await assertElementScreenshotUnchanged(await waitFor('devtools-console-insight'), 'explain/console_insight.png', 3);
+    await assertElementScreenshotUnchanged(
+        await waitFor('devtools-console-insight'), 'explain/console_insight_disclaimer1.png', 3);
   });
 
   // eslint-disable-next-line rulesdir/ban_screenshot_test_outside_perf_panel
-  itScreenshot('renders the state after consent', async () => {
+  itScreenshot('renders the second disclaimer', async () => {
     await loadComponentDocExample('console_insight/static.html');
-    await click('.consent-button');
+    await click('.next-button');
     await assertElementScreenshotUnchanged(
-        await waitFor('devtools-console-insight'), 'explain/console_insight_after_consent.png', 3);
+        await waitFor('devtools-console-insight'), 'explain/console_insight_disclaimer2.png', 3);
+  });
+
+  // eslint-disable-next-line rulesdir/ban_screenshot_test_outside_perf_panel
+  itScreenshot('renders the insight', async () => {
+    await loadComponentDocExample('console_insight/static.html');
+    await click('.next-button');
+    await click('.continue-button');
+    await assertElementScreenshotUnchanged(await waitFor('devtools-console-insight'), 'explain/console_insight.png', 3);
   });
 });