> ## Documentation Index
> Fetch the complete documentation index at: https://docs.open.cx/llms.txt
> Use this file to discover all available pages before exploring further.

# Playground

> Tweak widget options in a live form and copy the generated code. No install required.

Tweak the options in the right-hand panel and watch the snippet update. Hit **Launch preview** to load the widget live with your current settings.

export const PLAYGROUND_HTML = `<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width,initial-scale=1" />
    <title>OpenCX Widget Playground</title>
    <style>
      :root {
        --bg: #ffffff;
        --surface: #f8fafc;
        --surface-2: #f1f5f9;
        --border: #e5e7eb;
        --border-strong: #cbd5e1;
        --text: #0f172a;
        --muted: #64748b;
        --primary: #15803d;
        --primary-hover: #166534;
        --primary-fg: #ffffff;
        --danger: #dc2626;
        --stage-from: #f8fafc;
        --stage-to: #eef2ff;
        --code-bg: #0b1220;
        --code-text: #e5e7eb;
        --ring: rgba(21, 128, 61, 0.25);
        --shadow: 0 1px 2px rgba(15, 23, 42, 0.04);
        --scroll-thumb: rgba(15, 23, 42, 0.35);
        --scroll-thumb-hover: rgba(15, 23, 42, 0.55);
        font-family:
          'Inter', ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto,
          'Helvetica Neue', Arial, sans-serif;
      }
      html[data-theme='dark'] {
        --bg: #0a0a0a;
        --surface: #111113;
        --surface-2: #141417;
        --border: rgba(255, 255, 255, 0.06);
        --border-strong: rgba(255, 255, 255, 0.12);
        --text: #fafafa;
        --muted: #8a8a93;
        --primary: #54eb64;
        --primary-hover: #34c94a;
        --primary-fg: #052e12;
        --danger: #f87171;
        --stage-from: #0c0c0f;
        --stage-to: #141417;
        --code-bg: #060608;
        --code-text: #e4e4e7;
        --ring: rgba(84, 235, 100, 0.3);
        --shadow: 0 1px 2px rgba(0, 0, 0, 0.4);
        --scroll-thumb: rgba(255, 255, 255, 0.3);
        --scroll-thumb-hover: rgba(255, 255, 255, 0.5);
      }
      * {
        box-sizing: border-box;
      }
      html,
      body {
        margin: 0;
        padding: 0;
        height: 100%;
        background: var(--bg);
        color: var(--text);
        font-size: 14px;
      }
      body {
        min-height: 100%;
      }

      /* View: "stage" — just the fake browser + widget canvas */
      html[data-view='stage'] body {
        display: block;
        height: 100%;
      }
      html[data-view='stage'] main {
        height: 100%;
        padding: 0;
      }
      html[data-view='stage'] .stage {
        border: 0;
        border-radius: 0;
      }
      html[data-view='stage'] aside,
      html[data-view='stage'] section.snippet {
        display: none;
      }

      /* View: "snippet" — just the generated code block */
      html[data-view='snippet'] body {
        display: block;
        height: 100%;
      }
      html[data-view='snippet'] section.snippet {
        margin: 0;
        height: 100%;
        max-height: none;
        border: 0;
        border-radius: 0;
      }
      html[data-view='snippet'] aside,
      html[data-view='snippet'] main {
        display: none;
      }

      /* View: "form" — the options aside */
      html[data-view='form'] body,
      html[data-view='form'] {
        display: block;
        height: 100%;
        background: transparent;
      }
      html[data-view='form'] aside {
        height: 100%;
        width: 100%;
        max-width: none;
        border: 0;
        border-radius: 0;
        background: var(--bg);
        /* Stronger bottom fade to clearly hint more content below. */
        --fade: 40px;
        -webkit-mask-image: linear-gradient(
          to bottom,
          #000 0,
          #000 calc(100% - var(--fade)),
          transparent 100%
        );
        mask-image: linear-gradient(
          to bottom,
          #000 0,
          #000 calc(100% - var(--fade)),
          transparent 100%
        );
      }
      html[data-view='form'] main,
      html[data-view='form'] section.snippet {
        display: none;
      }

      aside {
        overflow-y: scroll;
        overflow-x: hidden;
        padding: 14px 12px 20px;
        background: var(--bg);
        scrollbar-gutter: stable;
      }
      aside::-webkit-scrollbar {
        width: 10px;
      }
      aside::-webkit-scrollbar-track {
        background: transparent;
      }
      aside::-webkit-scrollbar-thumb {
        background: var(--scroll-thumb);
        border-radius: 999px;
        border: 2px solid transparent;
        background-clip: padding-box;
        min-height: 40px;
      }
      aside::-webkit-scrollbar-thumb:hover {
        background: var(--scroll-thumb-hover);
        background-clip: padding-box;
      }
      aside {
        scrollbar-width: thin;
        scrollbar-color: var(--scroll-thumb) transparent;
      }

      main {
        position: relative;
        padding: 12px;
        overflow: hidden;
      }

      .stage {
        position: relative;
        height: 100%;
        min-height: 360px;
        border: 1px solid var(--border);
        border-radius: 6px;
        background: linear-gradient(135deg, var(--stage-from) 0%, var(--stage-to) 100%);
        overflow: hidden;
      }
      .chrome {
        height: 34px;
        display: flex;
        align-items: center;
        gap: 6px;
        padding: 0 12px;
        background: transparent;
        border-bottom: 1px solid var(--border);
      }
      .chrome .dot {
        width: 10px;
        height: 10px;
        border-radius: 50%;
        opacity: 0.85;
      }
      .chrome .url {
        flex: 1;
        margin-left: 8px;
        background: var(--surface);
        font-size: 11px;
        color: var(--muted);
        padding: 4px 10px;
        border-radius: 6px;
      }
      .stage-empty {
        position: absolute;
        inset: 34px 0 0 0;
        display: flex;
        align-items: center;
        justify-content: center;
        color: var(--muted);
        text-align: center;
        padding: 24px;
        font-size: 13px;
      }

      section.snippet {
        margin: 0 12px 12px;
        border: 1px solid var(--border);
        border-radius: 6px;
        background: var(--bg);
        display: flex;
        flex-direction: column;
        min-height: 220px;
        max-height: 40vh;
        overflow: hidden;
      }
      html[data-view='stage'] main {
        padding-bottom: 0;
      }
      .snippet-head {
        display: flex;
        align-items: center;
        justify-content: space-between;
        padding: 10px 14px;
        border-bottom: 1px solid var(--border);
      }
      .tabs {
        display: inline-flex;
        background: var(--surface);
        border-radius: 8px;
        padding: 3px;
        border: 1px solid var(--border);
      }
      .tab {
        padding: 4px 12px;
        font-size: 12px;
        color: var(--muted);
        cursor: pointer;
        border-radius: 6px;
        background: transparent;
        border: 0;
        font-family: inherit;
        transition:
          color 0.15s,
          background 0.15s;
      }
      .tab[aria-selected='true'] {
        background: var(--bg);
        color: var(--text);
        box-shadow: var(--shadow);
      }
      pre {
        flex: 1;
        margin: 0;
        padding: 16px;
        overflow: auto;
        background: var(--code-bg);
        color: var(--code-text);
        font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
        font-size: 12px;
        line-height: 1.6;
      }

      h2 {
        font-size: 13px;
        font-weight: 600;
        margin: 0;
        letter-spacing: -0.01em;
      }
      .hint {
        color: var(--muted);
        font-size: 11px;
      }
      .head-actions {
        position: sticky;
        top: -14px;
        margin: -14px -12px 8px;
        padding: 14px 12px 8px;
        display: flex;
        justify-content: space-between;
        align-items: center;
        gap: 8px;
        background: linear-gradient(to bottom, var(--bg) 70%, transparent);
        z-index: 2;
      }

      details {
        border-top: 1px solid var(--border);
        padding: 14px 0;
      }
      details:first-of-type {
        border-top: 0;
        padding-top: 6px;
      }
      summary {
        cursor: pointer;
        font-size: 11px;
        font-weight: 600;
        letter-spacing: 0.04em;
        text-transform: uppercase;
        color: var(--muted);
        list-style: none;
        display: flex;
        justify-content: space-between;
        align-items: center;
      }
      summary:hover {
        color: var(--text);
      }
      summary::-webkit-details-marker {
        display: none;
      }
      summary::after {
        content: '+';
        color: var(--muted);
        font-size: 16px;
        font-weight: 400;
        line-height: 1;
      }
      details[open] summary::after {
        content: '−';
      }
      .fields {
        display: grid;
        gap: 12px;
        padding: 12px 0 0;
      }

      label.field {
        display: grid;
        gap: 6px;
      }
      .label {
        font-size: 11px;
        font-weight: 600;
        color: var(--text);
        letter-spacing: 0.01em;
      }
      .label-muted {
        color: var(--muted);
        font-weight: 500;
      }

      input[type='text'],
      input[type='url'],
      input[type='email'],
      select,
      textarea {
        width: 100%;
        height: 32px;
        padding: 0 10px;
        border: 1px solid var(--border);
        background: var(--bg);
        color: var(--text);
        border-radius: 8px;
        font: inherit;
        font-size: 12px;
        transition:
          border-color 0.15s,
          box-shadow 0.15s;
      }
      textarea {
        height: auto;
        padding: 8px 10px;
        min-height: 60px;
        resize: vertical;
      }
      input:focus,
      select:focus,
      textarea:focus {
        outline: none;
        border-color: var(--primary);
        box-shadow: 0 0 0 3px var(--ring);
      }
      /* Radix wraps the widget's chat panel in a tabindex'd div for a11y;
       * focused, Chrome draws its default auto 1px outline using the
       * system focus-ring color (blue on macOS). Kill it on the Radix
       * wrappers only — interactive elements inside the widget's own
       * iframes live in separate documents and aren't affected. */
      #opencx-root [id^='radix-'] {
        outline: none;
      }
      input[aria-invalid='true'],
      textarea[aria-invalid='true'] {
        border-color: var(--danger);
      }
      input[aria-invalid='true']:focus,
      textarea[aria-invalid='true']:focus {
        box-shadow: 0 0 0 3px rgba(220, 38, 38, 0.2);
      }
      .field-error {
        display: none;
        color: var(--danger);
        font-size: 11px;
      }
      label.field[data-error='true'] .field-error {
        display: block;
      }
      input[type='color'] {
        width: 32px;
        height: 32px;
        padding: 2px;
        border: 1px solid var(--border);
        border-radius: 8px;
        background: var(--bg);
        cursor: pointer;
      }
      .row {
        display: flex;
        align-items: center;
        gap: 8px;
      }
      .row > input[type='text'] {
        flex: 1;
        font-family: ui-monospace, Menlo, monospace;
        text-transform: uppercase;
      }
      .slider-row {
        display: grid;
        gap: 6px;
      }
      .slider-head {
        display: flex;
        justify-content: space-between;
        align-items: baseline;
      }
      .slider-val {
        font-family: ui-monospace, Menlo, monospace;
        font-size: 11px;
        color: var(--muted);
      }
      input[type='range'] {
        width: 100%;
        accent-color: var(--primary);
      }
      .two-col {
        display: grid;
        grid-template-columns: 1fr 1fr;
        gap: 10px;
      }
      .switch-row {
        display: flex;
        justify-content: space-between;
        align-items: start;
        gap: 12px;
      }
      .switch-copy {
        display: grid;
        gap: 2px;
      }
      .switch-copy small {
        color: var(--muted);
        font-size: 11px;
      }
      input[type='checkbox'].switch {
        appearance: none;
        width: 32px;
        height: 18px;
        border-radius: 999px;
        background: var(--border-strong);
        position: relative;
        cursor: pointer;
        transition: background 0.15s;
        flex-shrink: 0;
        margin-top: 2px;
      }
      input[type='checkbox'].switch:checked {
        background: var(--primary);
      }
      input[type='checkbox'].switch::after {
        content: '';
        position: absolute;
        top: 2px;
        left: 2px;
        width: 14px;
        height: 14px;
        border-radius: 50%;
        background: #ffffff;
        transition: transform 0.15s;
        box-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
      }
      input[type='checkbox'].switch:checked::after {
        transform: translateX(14px);
      }

      .list-field {
        display: grid;
        gap: 6px;
      }
      .list-field .items {
        display: grid;
        gap: 6px;
      }
      .list-field .item {
        display: flex;
        gap: 6px;
      }
      .list-field .item input {
        flex: 1;
      }

      button {
        font: inherit;
      }
      .btn {
        appearance: none;
        border: 1px solid var(--border);
        background: var(--bg);
        color: var(--text);
        padding: 6px 10px;
        font-size: 12px;
        border-radius: 8px;
        cursor: pointer;
        display: inline-flex;
        gap: 6px;
        align-items: center;
        justify-content: center;
        transition:
          background 0.15s,
          border-color 0.15s,
          color 0.15s;
      }
      .btn:hover {
        background: var(--surface);
        border-color: var(--border-strong);
      }
      .btn.primary {
        background: var(--primary);
        color: var(--primary-fg);
        border-color: var(--primary);
      }
      .btn.primary:hover {
        background: var(--primary-hover);
        border-color: var(--primary-hover);
      }
      .btn.ghost {
        border-color: transparent;
      }
      .btn.icon-btn {
        padding: 6px 8px;
      }
      .spinner {
        display: inline-block;
        width: 12px;
        height: 12px;
        border: 2px solid var(--border);
        border-top-color: var(--primary);
        border-radius: 50%;
        animation: spin 0.7s linear infinite;
      }
      @keyframes spin {
        to {
          transform: rotate(360deg);
        }
      }

      .token-blur {
        filter: blur(4px);
        border-radius: 3px;
        user-select: none;
        pointer-events: none;
      }

      @keyframes modal-backdrop-in {
        from { opacity: 0; }
        to { opacity: 1; }
      }
      @keyframes modal-in {
        from { opacity: 0; transform: translateY(10px) scale(0.96); }
        to { opacity: 1; transform: translateY(0) scale(1); }
      }
      .modal-backdrop {
        position: fixed;
        inset: 0;
        background: rgba(0, 0, 0, 0.45);
        display: none;
        align-items: center;
        justify-content: center;
        z-index: 100;
        padding: 20px;
      }
      .modal-backdrop.open {
        display: flex;
        animation: modal-backdrop-in 0.18s ease both;
      }
      .modal {
        background: var(--bg);
        border: 1px solid var(--border);
        border-radius: 10px;
        box-shadow: 0 20px 50px rgba(0, 0, 0, 0.3);
        width: 100%;
        max-width: 420px;
        padding: 20px;
        display: grid;
        gap: 12px;
      }
      .modal-backdrop.open .modal {
        animation: modal-in 0.24s cubic-bezier(0.2, 0.9, 0.3, 1.2) both;
      }
      @media (prefers-reduced-motion: reduce) {
        .modal-backdrop.open,
        .modal-backdrop.open .modal {
          animation: none;
        }
      }
      .modal h3 {
        margin: 0;
        font-size: 14px;
        font-weight: 600;
      }
      .modal p {
        margin: 0;
        font-size: 12px;
        color: var(--muted);
        line-height: 1.5;
      }
      .modal p a {
        color: var(--primary);
        text-decoration: none;
      }
      .modal p a:hover {
        text-decoration: underline;
      }
      .modal input[type='text'] {
        width: 100%;
        padding: 8px 10px;
        font-size: 12px;
        font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
        border: 1px solid var(--border);
        border-radius: 6px;
        background: var(--surface);
        color: var(--text);
        box-sizing: border-box;
      }
      .modal input[type='text']:focus {
        outline: none;
        border-color: var(--primary);
      }
      .modal-actions {
        display: flex;
        justify-content: flex-end;
        gap: 8px;
      }
    </style>
  </head>
  <body>
    <aside aria-label="Widget options">
      <div class="head-actions">
        <h2>Widget options</h2>
        <button class="btn ghost" id="reset" type="button" aria-label="Reset to defaults">
          Reset
        </button>
      </div>

      <!-- populated dynamically -->
      <div id="form-root"></div>
    </aside>

    <main>
      <div class="stage">
        <div class="chrome" aria-hidden="true">
          <span class="dot" style="background: #f87171"></span>
          <span class="dot" style="background: #fbbf24"></span>
          <span class="dot" style="background: #34d399"></span>
          <span class="url">your-site.com</span>
        </div>
        <div class="stage-empty" id="stage-empty">
          <div id="stage-empty-inner">
            <div>
              Click <strong>Launch preview</strong> to load the widget with your options.
            </div>
            <button class="btn primary" id="launch" type="button" style="margin-top: 12px">
              Launch preview
            </button>
          </div>
        </div>
      </div>
    </main>

    <section class="snippet" aria-label="Generated snippet">
      <div class="snippet-head">
        <div class="tabs" role="tablist" aria-label="Snippet format">
          <button class="tab" role="tab" data-tab="react" aria-selected="true">React</button>
          <button class="tab" role="tab" data-tab="html" aria-selected="false">HTML script</button>
        </div>
        <button class="btn" id="copy" type="button" aria-label="Copy snippet">Copy</button>
      </div>
      <pre><code id="snippet"></code></pre>
    </section>

    <div class="modal-backdrop" id="token-modal" role="dialog" aria-modal="true" aria-labelledby="token-modal-title">
      <div class="modal">
        <h3 id="token-modal-title">Use your own widget token</h3>
        <p>
          Paste your widget token below to copy the snippet with it filled in. Find it in your
          OpenCX dashboard under <strong>Channels → Chat Widget</strong>. Leave blank to copy the
          snippet with an empty token.
        </p>
        <input
          type="text"
          id="token-input"
          placeholder="Your widget token"
          autocomplete="off"
          spellcheck="false"
        />
        <div class="modal-actions">
          <button class="btn ghost" id="token-cancel" type="button">Cancel</button>
          <button class="btn primary" id="token-confirm" type="button">Copy snippet</button>
        </div>
      </div>
    </div>

    <script>
      (function () {
        'use strict';

        // ---------------------------------------------------------------------
        // Theme sync with Mintlify parent (same-origin iframe).
        // ---------------------------------------------------------------------
        var embedded = window.parent && window.parent !== window;
        function detectTheme() {
          try {
            var parentDoc = window.parent && window.parent.document;
            if (parentDoc) {
              var root = parentDoc.documentElement;
              if (root.classList.contains('dark')) return 'dark';
              if (root.getAttribute('data-theme') === 'dark') return 'dark';
              if (root.classList.contains('light')) return 'light';
              return 'light';
            }
          } catch (_) {}
          if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
            return 'dark';
          }
          return 'light';
        }
        function applyTheme() {
          document.documentElement.setAttribute('data-theme', detectTheme());
        }
        applyTheme();
        try {
          var parentDoc = window.parent && window.parent.document;
          if (parentDoc) {
            new MutationObserver(applyTheme).observe(parentDoc.documentElement, {
              attributes: true,
              attributeFilter: ['class', 'data-theme'],
            });
          }
        } catch (_) {}
        if (!embedded && window.matchMedia) {
          window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', applyTheme);
        }

        // ---------------------------------------------------------------------
        // View mode: ?view=form | ?view=stage | ?view=snippet
        // Also readable from window.__PLAYGROUND_VIEW (for srcdoc embedding).
        // ---------------------------------------------------------------------
        var viewParam =
          window.__PLAYGROUND_VIEW || new URLSearchParams(location.search).get('view');
        if (viewParam === 'form' || viewParam === 'stage' || viewParam === 'snippet') {
          document.documentElement.setAttribute('data-view', viewParam);
        }

        // ---------------------------------------------------------------------
        // Configuration schema
        // ---------------------------------------------------------------------
        var DEMO_TOKEN = '34d6a5329e88c18cd920f510d6ee4c19';
        var STORAGE_KEY = 'opencx-widget-playground-state-v2';
        var LAUNCH_FLAG_KEY = 'opencx-widget-playground-launched';

        var LANGUAGES = [
          { value: 'en', label: 'English' },
          { value: 'es', label: 'Spanish' },
          { value: 'fr', label: 'French' },
          { value: 'de', label: 'German' },
          { value: 'pt', label: 'Portuguese' },
          { value: 'ar', label: 'Arabic' },
        ];
        var PALETTES = [
          { value: 'neutral', label: 'Neutral' },
          { value: 'stone', label: 'Stone' },
          { value: 'zinc', label: 'Zinc' },
          { value: 'slate', label: 'Slate' },
        ];
        var QUESTION_POSITIONS = [
          { value: 'above-chat-input', label: 'Above chat input' },
          { value: 'below-initial-messages', label: 'Below initial messages' },
        ];
        var ANCHOR_TARGETS = [
          { value: '_blank', label: 'New tab (_blank)' },
          { value: '_top', label: 'Same tab (_top)' },
        ];
        var BOX_SHADOWS = [
          { value: 'none', label: 'None', css: undefined },
          { value: 'soft', label: 'Soft', css: '0 10px 25px rgba(0,0,0,0.06)' },
          { value: 'medium', label: 'Medium', css: '0 20px 40px rgba(0,0,0,0.10)' },
          { value: 'strong', label: 'Strong', css: '0 30px 60px rgba(0,0,0,0.18)' },
        ];

        var DEFAULTS = {
          // Identity
          botName: 'Aria',
          botAvatarUrl: '',
          humanAgentName: '',
          humanAgentAvatarUrl: '',
          organizationLogo: '',
          language: 'en',

          // Theme — colors & surface
          primaryColor: '#15803d',
          palette: 'neutral',
          borderRadius: 16,
          containerOutline: false,
          containerOutlineColor: '#e5e7eb',
          containerBoxShadow: 'none',

          // Theme — launcher
          triggerSize: 56,
          triggerOffsetBottom: 20,
          triggerOffsetRight: 20,
          triggerOffsetLeft: 0,
          openIconUrl: '',
          closeIconUrl: '',

          // First visit
          initialMessages: ['Hey! Happy to help.'],
          initialQuestions: [],
          initialQuestionsPosition: 'below-initial-messages',
          chatBannerMessage: '',
          chatFooterMessage: '',

          // Visitor
          collectUserData: false,
          prefillName: '',
          prefillEmail: '',

          // Behavior
          isOpen: true,
          openAfterNSeconds: 0,
          oneOpenSessionAllowed: false,
          anchorTarget: '_blank',
          routerGoToChatIfNoSessions: false,
          routerChatScreenOnly: false,

          // Features
          disableTooltips: false,
          thisWasHelpfulOrNot: false,
          timestampsPerMessageGroup: false,
        };

        // Mutable state (deep clone to avoid aliasing default arrays).
        var state = loadState();
        var activeTab = 'react';
        var widgetLoaded = false;
        var livePreviewTimer = null;

        function deepClone(value) {
          return JSON.parse(JSON.stringify(value));
        }

        function loadState() {
          try {
            var raw = localStorage.getItem(STORAGE_KEY);
            if (!raw) return deepClone(DEFAULTS);
            var parsed = JSON.parse(raw);
            if (!parsed || typeof parsed !== 'object') return deepClone(DEFAULTS);
            // Merge with defaults so new fields added since the state was saved still resolve.
            return Object.assign(deepClone(DEFAULTS), parsed);
          } catch (_) {
            return deepClone(DEFAULTS);
          }
        }

        function saveState() {
          try {
            localStorage.setItem(STORAGE_KEY, JSON.stringify(state));
          } catch (_) {
            // quota full or disabled — fail silently, playground still works for this tab
          }
        }

        // ---------------------------------------------------------------------
        // Validation helpers
        // ---------------------------------------------------------------------
        function isValidOptionalUrl(value) {
          if (!value) return true;
          try {
            var u = new URL(value);
            return u.protocol === 'http:' || u.protocol === 'https:';
          } catch (_) {
            return false;
          }
        }

        function isValidOptionalEmail(value) {
          if (!value) return true;
          return /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/.test(value);
        }

        function isValidHex(value) {
          return /^#([0-9a-f]{3}|[0-9a-f]{6})$/i.test(value);
        }

        // ---------------------------------------------------------------------
        // Build the actual widget config that readers copy into their app.
        // Key: if a value is at the default, we omit it to keep the snippet clean.
        // ---------------------------------------------------------------------
        function buildOptions() {
          var opts = {
            token: DEMO_TOKEN,
            language: state.language,
          };

          // Identity
          var bot = { name: state.botName };
          if (state.botAvatarUrl.trim()) bot.avatarUrl = state.botAvatarUrl.trim();
          opts.bot = bot;

          var humanAgent = {};
          if (state.humanAgentName.trim()) humanAgent.name = state.humanAgentName.trim();
          if (state.humanAgentAvatarUrl.trim())
            humanAgent.avatarUrl = state.humanAgentAvatarUrl.trim();
          if (Object.keys(humanAgent).length) opts.humanAgent = humanAgent;

          // Initial messages + questions
          var initialMessages = state.initialMessages.filter(Boolean);
          if (initialMessages.length) opts.initialMessages = initialMessages;
          var initialQuestions = state.initialQuestions.filter(Boolean);
          if (initialQuestions.length) {
            opts.initialQuestions = initialQuestions;
            if (state.initialQuestionsPosition !== DEFAULTS.initialQuestionsPosition) {
              opts.initialQuestionsPosition = state.initialQuestionsPosition;
            }
          }

          // Banner / footer
          if (state.chatBannerMessage.trim()) {
            opts.chatBannerItems = [
              { message: state.chatBannerMessage.trim(), persistent: true },
            ];
          }
          if (state.chatFooterMessage.trim()) {
            opts.chatFooterItems = [{ message: state.chatFooterMessage.trim() }];
          }

          // Visitor
          if (state.collectUserData) opts.collectUserData = true;
          var prefill = {};
          if (state.prefillName.trim()) prefill.name = state.prefillName.trim();
          if (state.prefillEmail.trim()) prefill.email = state.prefillEmail.trim();
          if (Object.keys(prefill).length) opts.prefillUserData = prefill;

          // Behavior
          if (state.isOpen) opts.isOpen = true;
          if (state.openAfterNSeconds > 0) opts.openAfterNSeconds = state.openAfterNSeconds;
          if (state.oneOpenSessionAllowed) opts.oneOpenSessionAllowed = true;
          if (state.anchorTarget !== DEFAULTS.anchorTarget) opts.anchorTarget = state.anchorTarget;

          var router = {};
          if (state.routerGoToChatIfNoSessions) router.goToChatIfNoSessions = true;
          if (state.routerChatScreenOnly) router.chatScreenOnly = true;
          if (Object.keys(router).length) opts.router = router;

          // Features
          if (state.disableTooltips) opts.disableTooltips = true;
          if (state.thisWasHelpfulOrNot) opts.thisWasHelpfulOrNot = { enabled: true };
          if (state.timestampsPerMessageGroup)
            opts.timestamps = { perMessageGroup: { enabled: true } };

          // Assets
          var assets = {};
          if (state.organizationLogo.trim()) assets.organizationLogo = state.organizationLogo.trim();
          var trigger = {};
          if (state.openIconUrl.trim()) trigger.openIcon = state.openIconUrl.trim();
          if (state.closeIconUrl.trim()) trigger.closeIcon = state.closeIconUrl.trim();
          if (Object.keys(trigger).length) assets.widgetTrigger = trigger;
          if (Object.keys(assets).length) opts.assets = assets;

          // Theme
          var triggerOffset = {
            bottom: state.triggerOffsetBottom,
            right: state.triggerOffsetRight,
          };
          if (state.triggerOffsetLeft > 0) triggerOffset.left = state.triggerOffsetLeft;

          var container = { borderRadius: state.borderRadius + 'px' };
          if (state.containerOutline) {
            container.outline = '1px solid';
            container.outlineColor = state.containerOutlineColor;
          }
          var shadow = BOX_SHADOWS.find(function (s) {
            return s.value === state.containerBoxShadow;
          });
          if (shadow && shadow.css) container.boxShadow = shadow.css;

          opts.theme = {
            palette: state.palette,
            primaryColor: state.primaryColor,
            widgetTrigger: {
              size: {
                button: state.triggerSize,
                icon: Math.round(state.triggerSize * 0.45),
              },
              offset: triggerOffset,
            },
            widgetContentContainer: container,
          };

          return opts;
        }

        // Preview-only overrides: keep the widget large in the stage, but
        // leave a margin so it reads as a floating panel, not a full takeover.
        // Always render open so tweaking options doesn't close the preview.
        function buildPreviewOptions() {
          var opts = buildOptions();
          opts.isOpen = true;
          opts.theme.screens = {
            welcome: { minHeight: 'calc(100vh - 220px)', width: 'calc(100vw - 32px)' },
            sessions: { height: 'calc(100vh - 220px)', width: 'calc(100vw - 32px)' },
            chat: { height: 'calc(100vh - 220px)', width: 'calc(100vw - 32px)' },
          };
          opts.theme.widgetTrigger.size = {
            button: Math.min(state.triggerSize, 48),
            icon: Math.min(Math.round(state.triggerSize * 0.45), 20),
          };
          return opts;
        }

        // ---------------------------------------------------------------------
        // Snippet generation
        // ---------------------------------------------------------------------
        function indent(json, spaces) {
          return json.split('\\n').join('\\n' + ' '.repeat(spaces));
        }
        function reactSnippet() {
          var json = JSON.stringify(buildOptions(), null, 2);
          return (
            "import { Widget } from '@opencx/widget-react';\\n\\n" +
            'export function Chat() {\\n' +
            '  return (\\n' +
            '    <Widget\\n' +
            '      options={' +
            indent(json, 8) +
            '}\\n' +
            '    />\\n' +
            '  );\\n' +
            '}'
          );
        }
        function htmlSnippet() {
          var json = JSON.stringify(buildOptions(), null, 2);
          return (
            '<script defer src="https://unpkg.com/@opencx/widget@latest/dist-embed/script.js"></' +
            'script>\\n' +
            '<script>\\n' +
            "  window.addEventListener('DOMContentLoaded', () => {\\n" +
            '    initOpenScript(' +
            indent(json, 4) +
            ');\\n' +
            '  });\\n' +
            '</' +
            'script>'
          );
        }
        function escapeHtml(s) {
          return s
            .replace(/&/g, '&amp;')
            .replace(/</g, '&lt;')
            .replace(/>/g, '&gt;')
            .replace(/"/g, '&quot;')
            .replace(/'/g, '&#39;');
        }
        function renderSnippet() {
          var el = document.getElementById('snippet');
          if (!el) return;
          var text = activeTab === 'react' ? reactSnippet() : htmlSnippet();
          var parts = text.split(DEMO_TOKEN);
          var html = parts
            .map(escapeHtml)
            .join('<span class="token-blur">' + escapeHtml(DEMO_TOKEN) + '</span>');
          el.innerHTML = html;
        }

        // ---------------------------------------------------------------------
        // Form rendering (data-driven)
        // ---------------------------------------------------------------------
        var FIELD_DEFS = [
          {
            id: 'identity',
            title: 'Identity',
            fields: [
              { kind: 'text', key: 'botName', label: 'Bot name' },
              { kind: 'url', key: 'botAvatarUrl', label: 'Bot avatar URL', optional: true },
              { kind: 'text', key: 'humanAgentName', label: 'Human agent name', optional: true },
              {
                kind: 'url',
                key: 'humanAgentAvatarUrl',
                label: 'Human agent avatar URL',
                optional: true,
              },
              {
                kind: 'url',
                key: 'organizationLogo',
                label: 'Organization logo URL',
                optional: true,
              },
              { kind: 'select', key: 'language', label: 'Language', options: LANGUAGES },
            ],
          },
          {
            id: 'theme',
            title: 'Theme',
            fields: [
              { kind: 'color', key: 'primaryColor', label: 'Primary color' },
              { kind: 'select', key: 'palette', label: 'Palette', options: PALETTES },
              {
                kind: 'slider',
                key: 'borderRadius',
                label: 'Window border radius',
                min: 0,
                max: 32,
                unit: 'px',
              },
              {
                kind: 'switch',
                key: 'containerOutline',
                label: 'Window outline',
                description: 'Adds a 1px outline around the chat window.',
              },
              {
                kind: 'color',
                key: 'containerOutlineColor',
                label: 'Outline color',
                dependsOn: 'containerOutline',
              },
              {
                kind: 'select',
                key: 'containerBoxShadow',
                label: 'Window shadow',
                options: BOX_SHADOWS,
              },
              {
                kind: 'slider',
                key: 'triggerSize',
                label: 'Launcher size',
                min: 40,
                max: 96,
                unit: 'px',
              },
              {
                kind: 'sliders2',
                label: 'Launcher offset',
                fields: [
                  { key: 'triggerOffsetBottom', label: 'Bottom', min: 0, max: 200, unit: 'px' },
                  { key: 'triggerOffsetRight', label: 'Right', min: 0, max: 200, unit: 'px' },
                ],
              },
              {
                kind: 'slider',
                key: 'triggerOffsetLeft',
                label: 'Launcher left offset',
                min: 0,
                max: 200,
                unit: 'px',
                hint: 'Set to 0 to pin to the right instead.',
              },
              {
                kind: 'url',
                key: 'openIconUrl',
                label: 'Custom open icon URL',
                optional: true,
              },
              {
                kind: 'url',
                key: 'closeIconUrl',
                label: 'Custom close icon URL',
                optional: true,
              },
            ],
          },
          {
            id: 'first-visit',
            title: 'First visit',
            fields: [
              {
                kind: 'list',
                key: 'initialMessages',
                label: 'Initial messages',
                placeholder: 'Hi, how can I help?',
                max: 5,
              },
              {
                kind: 'list',
                key: 'initialQuestions',
                label: 'Initial questions',
                placeholder: 'How do I reset my password?',
                max: 5,
              },
              {
                kind: 'select',
                key: 'initialQuestionsPosition',
                label: 'Questions position',
                options: QUESTION_POSITIONS,
              },
              {
                kind: 'text',
                key: 'chatBannerMessage',
                label: 'Chat banner message',
                optional: true,
                hint: 'Persistent banner shown at the top of the chat.',
              },
              {
                kind: 'text',
                key: 'chatFooterMessage',
                label: 'Chat footer message',
                optional: true,
                hint: 'Small note shown under the composer.',
              },
            ],
          },
          {
            id: 'visitor',
            title: 'Visitor',
            fields: [
              {
                kind: 'switch',
                key: 'collectUserData',
                label: 'Collect visitor data',
                description: 'Ask for name and email before the first message.',
              },
              {
                kind: 'text',
                key: 'prefillName',
                label: 'Prefill visitor name',
                optional: true,
              },
              {
                kind: 'email',
                key: 'prefillEmail',
                label: 'Prefill visitor email',
                optional: true,
              },
            ],
          },
          {
            id: 'behavior',
            title: 'Behavior',
            fields: [
              {
                kind: 'switch',
                key: 'isOpen',
                label: 'Open by default',
                description: 'Show the chat window on page load.',
              },
              {
                kind: 'slider',
                key: 'openAfterNSeconds',
                label: 'Auto-open after',
                min: 0,
                max: 60,
                unit: 's',
                zeroLabel: 'Disabled',
              },
              {
                kind: 'switch',
                key: 'oneOpenSessionAllowed',
                label: 'One open session at a time',
                description: 'Force visitors back to their active conversation.',
              },
              {
                kind: 'select',
                key: 'anchorTarget',
                label: 'Link open behavior',
                options: ANCHOR_TARGETS,
              },
              {
                kind: 'switch',
                key: 'routerGoToChatIfNoSessions',
                label: 'Skip welcome when empty',
                description: 'Go straight to the chat screen if no sessions exist.',
              },
              {
                kind: 'switch',
                key: 'routerChatScreenOnly',
                label: 'Chat screen only',
                description: 'Hide welcome and sessions screens entirely.',
              },
            ],
          },
          {
            id: 'features',
            title: 'Features',
            fields: [
              {
                kind: 'switch',
                key: 'disableTooltips',
                label: 'Disable tooltips',
                description: 'Hide hover tooltips inside the widget.',
              },
              {
                kind: 'switch',
                key: 'thisWasHelpfulOrNot',
                label: 'Feedback prompt',
                description: 'Show a "was this helpful?" prompt after AI messages.',
              },
              {
                kind: 'switch',
                key: 'timestampsPerMessageGroup',
                label: 'Message group timestamps',
                description: 'Show a timestamp for each group of messages.',
              },
            ],
          },
        ];

        function renderForm() {
          var root = document.getElementById('form-root');
          if (!root) return;
          root.innerHTML = '';
          FIELD_DEFS.forEach(function (section, i) {
            var details = document.createElement('details');
            details.open = true;
            var summary = document.createElement('summary');
            summary.textContent = section.title;
            details.appendChild(summary);
            var fields = document.createElement('div');
            fields.className = 'fields';
            section.fields.forEach(function (field) {
              fields.appendChild(renderField(field));
            });
            details.appendChild(fields);
            root.appendChild(details);
          });
        }

        function renderField(field) {
          switch (field.kind) {
            case 'text':
            case 'url':
            case 'email':
              return renderInput(field);
            case 'select':
              return renderSelect(field);
            case 'color':
              return renderColor(field);
            case 'slider':
              return renderSlider(field);
            case 'sliders2':
              return renderSliders2(field);
            case 'switch':
              return renderSwitch(field);
            case 'list':
              return renderList(field);
          }
          return document.createComment('unknown field kind');
        }

        function hintOrError(field) {
          var holder = document.createElement('div');
          if (field.hint) {
            var hint = document.createElement('div');
            hint.className = 'hint';
            hint.textContent = field.hint;
            holder.appendChild(hint);
          }
          var err = document.createElement('div');
          err.className = 'field-error';
          err.textContent = field.errorMessage || 'Invalid value';
          holder.appendChild(err);
          return holder;
        }

        function renderInput(field) {
          var wrap = document.createElement('label');
          wrap.className = 'field';
          var span = document.createElement('span');
          span.className = 'label';
          span.textContent = field.label;
          if (field.optional) {
            var opt = document.createElement('span');
            opt.className = 'label-muted';
            opt.textContent = ' (optional)';
            span.appendChild(opt);
          }
          wrap.appendChild(span);
          var input = document.createElement('input');
          input.type = field.kind;
          input.value = state[field.key] || '';
          input.placeholder = field.placeholder || '';
          input.addEventListener('input', function () {
            var value = input.value;
            var valid = true;
            if (field.kind === 'url' && !isValidOptionalUrl(value)) valid = false;
            if (field.kind === 'email' && !isValidOptionalEmail(value)) valid = false;
            input.setAttribute('aria-invalid', valid ? 'false' : 'true');
            wrap.setAttribute('data-error', valid ? 'false' : 'true');
            if (valid) {
              state[field.key] = value;
              saveState();
              broadcast();
            }
          });
          wrap.appendChild(input);
          wrap.appendChild(hintOrError(field));
          return wrap;
        }

        function renderSelect(field) {
          var wrap = document.createElement('label');
          wrap.className = 'field';
          var span = document.createElement('span');
          span.className = 'label';
          span.textContent = field.label;
          wrap.appendChild(span);
          var select = document.createElement('select');
          field.options.forEach(function (opt) {
            var o = document.createElement('option');
            o.value = opt.value;
            o.textContent = opt.label;
            select.appendChild(o);
          });
          select.value = state[field.key];
          var handler = function () {
            state[field.key] = select.value;
            saveState();
            broadcast();
          };
          select.addEventListener('input', handler);
          select.addEventListener('change', handler);
          wrap.appendChild(select);
          if (field.hint) wrap.appendChild(hintOrError(field));
          return wrap;
        }

        function renderColor(field) {
          var wrap = document.createElement('label');
          wrap.className = 'field';
          var span = document.createElement('span');
          span.className = 'label';
          span.textContent = field.label;
          wrap.appendChild(span);
          var row = document.createElement('div');
          row.className = 'row';
          var picker = document.createElement('input');
          picker.type = 'color';
          picker.value = state[field.key];
          picker.setAttribute('aria-label', field.label + ' color picker');
          var hex = document.createElement('input');
          hex.type = 'text';
          hex.value = state[field.key].toUpperCase();
          hex.spellcheck = false;
          hex.setAttribute('aria-label', field.label + ' hex value');
          picker.addEventListener('input', function () {
            state[field.key] = picker.value;
            hex.value = picker.value.toUpperCase();
            hex.setAttribute('aria-invalid', 'false');
            saveState();
            broadcast();
          });
          hex.addEventListener('input', function () {
            var v = hex.value.trim();
            if (!isValidHex(v)) {
              hex.setAttribute('aria-invalid', 'true');
              return;
            }
            hex.setAttribute('aria-invalid', 'false');
            state[field.key] = v.toLowerCase();
            picker.value = v.toLowerCase();
            saveState();
            broadcast();
          });
          row.appendChild(picker);
          row.appendChild(hex);
          wrap.appendChild(row);
          return wrap;
        }

        function renderSlider(field) {
          var wrap = document.createElement('div');
          wrap.className = 'slider-row';
          var head = document.createElement('div');
          head.className = 'slider-head';
          var lbl = document.createElement('span');
          lbl.className = 'label';
          lbl.textContent = field.label;
          var val = document.createElement('span');
          val.className = 'slider-val';
          var render = function () {
            if (field.zeroLabel && state[field.key] === 0) val.textContent = field.zeroLabel;
            else val.textContent = state[field.key] + (field.unit || '');
          };
          render();
          head.appendChild(lbl);
          head.appendChild(val);
          wrap.appendChild(head);
          var input = document.createElement('input');
          input.type = 'range';
          input.min = String(field.min);
          input.max = String(field.max);
          input.value = String(state[field.key]);
          input.setAttribute('aria-label', field.label);
          input.addEventListener('input', function () {
            var n = Number(input.value);
            if (!Number.isFinite(n)) return;
            state[field.key] = n;
            render();
            saveState();
            broadcast();
          });
          wrap.appendChild(input);
          if (field.hint) {
            var hint = document.createElement('div');
            hint.className = 'hint';
            hint.textContent = field.hint;
            wrap.appendChild(hint);
          }
          return wrap;
        }

        function renderSliders2(field) {
          var wrap = document.createElement('div');
          var outerLbl = document.createElement('span');
          outerLbl.className = 'label';
          outerLbl.textContent = field.label;
          outerLbl.style.marginBottom = '6px';
          outerLbl.style.display = 'block';
          wrap.appendChild(outerLbl);
          var grid = document.createElement('div');
          grid.className = 'two-col';
          field.fields.forEach(function (sub) {
            grid.appendChild(renderSlider(sub));
          });
          wrap.appendChild(grid);
          return wrap;
        }

        function renderSwitch(field) {
          var wrap = document.createElement('div');
          wrap.className = 'switch-row';
          var copy = document.createElement('div');
          copy.className = 'switch-copy';
          var lbl = document.createElement('span');
          lbl.className = 'label';
          lbl.textContent = field.label;
          copy.appendChild(lbl);
          if (field.description) {
            var d = document.createElement('small');
            d.textContent = field.description;
            copy.appendChild(d);
          }
          wrap.appendChild(copy);
          var input = document.createElement('input');
          input.type = 'checkbox';
          input.className = 'switch';
          input.checked = Boolean(state[field.key]);
          input.setAttribute('aria-label', field.label);
          input.addEventListener('input', function () {
            state[field.key] = input.checked;
            saveState();
            broadcast();
          });
          wrap.appendChild(input);
          return wrap;
        }

        function renderList(field) {
          var wrap = document.createElement('div');
          wrap.className = 'list-field';
          var head = document.createElement('div');
          head.style.display = 'flex';
          head.style.justifyContent = 'space-between';
          head.style.alignItems = 'baseline';
          var lbl = document.createElement('span');
          lbl.className = 'label';
          lbl.textContent = field.label;
          var count = document.createElement('span');
          count.className = 'hint';
          count.textContent = state[field.key].length + '/' + field.max;
          head.appendChild(lbl);
          head.appendChild(count);
          wrap.appendChild(head);

          var items = document.createElement('div');
          items.className = 'items';
          wrap.appendChild(items);

          var addBtn = document.createElement('button');
          addBtn.type = 'button';
          addBtn.className = 'btn ghost';
          addBtn.style.width = '100%';
          addBtn.style.justifyContent = 'flex-start';
          addBtn.textContent = '+ Add';
          addBtn.addEventListener('click', function () {
            if (state[field.key].length >= field.max) return;
            state[field.key].push('');
            saveState();
            renderItems();
            broadcast();
          });

          function renderItems() {
            items.innerHTML = '';
            state[field.key].forEach(function (val, i) {
              var row = document.createElement('div');
              row.className = 'item';
              var input = document.createElement('input');
              input.type = 'text';
              input.value = val;
              input.placeholder = field.placeholder || '';
              input.setAttribute('aria-label', field.label + ' ' + (i + 1));
              input.addEventListener('input', function () {
                state[field.key][i] = input.value;
                saveState();
                renderSnippet();
                triggerLivePreview();
              });
              var rm = document.createElement('button');
              rm.type = 'button';
              rm.className = 'btn icon-btn';
              rm.setAttribute('aria-label', 'Remove ' + field.label.toLowerCase() + ' ' + (i + 1));
              rm.textContent = '×';
              rm.addEventListener('click', function () {
                state[field.key].splice(i, 1);
                saveState();
                renderItems();
                broadcast();
              });
              row.appendChild(input);
              row.appendChild(rm);
              items.appendChild(row);
            });
            count.textContent = state[field.key].length + '/' + field.max;
            if (state[field.key].length >= field.max) addBtn.setAttribute('disabled', 'true');
            else addBtn.removeAttribute('disabled');
          }

          renderItems();
          wrap.appendChild(addBtn);
          return wrap;
        }

        function broadcast() {
          renderSnippet();
          triggerLivePreview();
        }

        // True if an input/textarea/select is focused in any *other* iframe on
        // the page (i.e. the options form). Re-mounting the widget while the
        // user is mid-keystroke would re-arm Radix's focus trap and yank focus
        // out of the form, so we defer until the field is blurred.
        function isSiblingInputFocused() {
          try {
            var parent = window.parent;
            if (!parent || parent === window) return false;
            var iframes = parent.document.querySelectorAll('iframe');
            for (var i = 0; i < iframes.length; i++) {
              var f = iframes[i];
              if (f.contentWindow === window) continue;
              var doc = f.contentDocument;
              if (!doc) continue;
              var ae = doc.activeElement;
              if (!ae || ae === doc.body) continue;
              var tag = ae.tagName;
              if (tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT') return true;
            }
          } catch (_) {}
          return false;
        }

        function runLivePreview() {
          livePreviewTimer = null;
          if (!widgetLoaded || typeof window.initOpenScript !== 'function') return;
          if (isSiblingInputFocused()) {
            livePreviewTimer = setTimeout(runLivePreview, 300);
            return;
          }
          try {
            window.initOpenScript(buildPreviewOptions());
          } catch (err) {
            console.error('Widget re-init failed', err);
          }
        }

        function triggerLivePreview() {
          if (!widgetLoaded || typeof window.initOpenScript !== 'function') return;
          if (livePreviewTimer) clearTimeout(livePreviewTimer);
          livePreviewTimer = setTimeout(runLivePreview, 200);
        }

        // ---------------------------------------------------------------------
        // Cross-iframe state sync (same origin via 'storage' event)
        // ---------------------------------------------------------------------
        window.addEventListener('storage', function (e) {
          if (e.key !== STORAGE_KEY || e.newValue === null) return;
          try {
            var incoming = JSON.parse(e.newValue);
            if (!incoming || typeof incoming !== 'object') return;
            state = Object.assign(deepClone(DEFAULTS), incoming);
          } catch (_) {
            return;
          }
          // Form view: re-render so inputs show new values.
          if (viewParam === 'form') renderForm();
          // Snippet view: regenerate code.
          if (viewParam === 'snippet') renderSnippet();
          // Stage view: re-init the live widget if mounted.
          if (viewParam === 'stage') triggerLivePreview();
        });

        // ---------------------------------------------------------------------
        // Snippet tabs + copy
        // ---------------------------------------------------------------------
        Array.prototype.forEach.call(document.querySelectorAll('.tab'), function (tab) {
          tab.addEventListener('click', function () {
            activeTab = tab.getAttribute('data-tab');
            Array.prototype.forEach.call(document.querySelectorAll('.tab'), function (t) {
              t.setAttribute('aria-selected', t === tab ? 'true' : 'false');
            });
            renderSnippet();
          });
        });

        var copyBtn = document.getElementById('copy');
        var tokenModal = document.getElementById('token-modal');
        var tokenInput = document.getElementById('token-input');
        var tokenCancel = document.getElementById('token-cancel');
        var tokenConfirm = document.getElementById('token-confirm');

        function openTokenModal() {
          if (!tokenModal) return;
          tokenModal.classList.add('open');
          setTimeout(function () {
            if (tokenInput) tokenInput.focus();
          }, 0);
        }
        function closeTokenModal() {
          if (!tokenModal) return;
          tokenModal.classList.remove('open');
        }
        function copyWithToken(userToken) {
          var raw = activeTab === 'react' ? reactSnippet() : htmlSnippet();
          var token = (userToken || '').trim();
          var text = raw.split(DEMO_TOKEN).join(token);
          if (!navigator.clipboard) return;
          navigator.clipboard
            .writeText(text)
            .then(function () {
              if (!copyBtn) return;
              var prev = copyBtn.getAttribute('data-label') || 'Copy';
              copyBtn.textContent = 'Copied';
              setTimeout(function () {
                copyBtn.textContent = prev;
              }, 1200);
            })
            .catch(function (err) {
              console.error('Copy failed', err);
            });
        }

        if (copyBtn) {
          copyBtn.setAttribute('data-label', copyBtn.textContent);
          copyBtn.addEventListener('click', openTokenModal);
        }
        if (tokenCancel) tokenCancel.addEventListener('click', closeTokenModal);
        if (tokenModal) {
          tokenModal.addEventListener('click', function (e) {
            if (e.target === tokenModal) closeTokenModal();
          });
        }
        if (tokenConfirm) {
          tokenConfirm.addEventListener('click', function () {
            copyWithToken(tokenInput ? tokenInput.value : '');
            closeTokenModal();
          });
        }
        if (tokenInput) {
          tokenInput.addEventListener('keydown', function (e) {
            if (e.key === 'Enter') {
              copyWithToken(tokenInput.value);
              closeTokenModal();
            } else if (e.key === 'Escape') {
              closeTokenModal();
            }
          });
        }

        var resetBtn = document.getElementById('reset');
        if (resetBtn) {
          resetBtn.addEventListener('click', function () {
            state = deepClone(DEFAULTS);
            saveState();
            renderForm();
            broadcast();
          });
        }

        // ---------------------------------------------------------------------
        // Widget launch (stage view)
        // ---------------------------------------------------------------------
        function setStageMessage(html) {
          var inner = document.getElementById('stage-empty-inner');
          if (inner) inner.innerHTML = html;
        }
        function setStageUnavailable() {
          setStageMessage(
            '<div style="max-width:360px;text-align:center;line-height:1.5">' +
              '<strong style="display:block;margin-bottom:6px">Preview unavailable</strong>' +
              '<span style="opacity:.75">The shared demo workspace is not reachable right now. ' +
              'Copy the snippet and swap the <code style="background:var(--surface);padding:1px 4px;border-radius:3px">token</code> ' +
              'for your own (dashboard → Channels → Chat Widget) to try it in your app.</span>' +
              '</div>',
          );
        }
        function waitForWidgetRender(timeoutMs) {
          return new Promise(function (resolve) {
            var start = Date.now();
            var tick = function () {
              var root = document.getElementById('opencx-root');
              if (root && root.children.length > 0) return resolve(true);
              if (Date.now() - start > timeoutMs) return resolve(false);
              setTimeout(tick, 200);
            };
            tick();
          });
        }
        function markLaunched(value) {
          try {
            if (value) sessionStorage.setItem(LAUNCH_FLAG_KEY, '1');
            else sessionStorage.removeItem(LAUNCH_FLAG_KEY);
          } catch (_) {}
        }
        function wasLaunchedBefore() {
          try {
            return sessionStorage.getItem(LAUNCH_FLAG_KEY) === '1';
          } catch (_) {
            return false;
          }
        }
        function launchWidget() {
          if (widgetLoaded) {
            try {
              window.initOpenScript(buildPreviewOptions());
              setStageMessage(
                '<div style="opacity:.8">Preview updated. Look at the bottom-right corner of this panel.</div>',
              );
            } catch (e) {
              console.error('Widget re-init failed', e);
            }
            return;
          }
          setStageMessage(
            '<div style="display:flex;gap:8px;align-items:center;justify-content:center;opacity:.8">' +
              '<span class="spinner"></span>Loading widget&hellip;</div>',
          );
          var s = document.createElement('script');
          s.src = 'https://unpkg.com/@opencx/widget@latest/dist-embed/script.js';
          s.crossOrigin = 'anonymous';
          s.onload = function () {
            try {
              window.initOpenScript(buildPreviewOptions());
            } catch (e) {
              console.error('Widget init failed', e);
              setStageUnavailable();
              markLaunched(false);
              return;
            }
            waitForWidgetRender(6000).then(function (rendered) {
              if (rendered) {
                widgetLoaded = true;
                markLaunched(true);
                setStageMessage(
                  '<div style="opacity:.8">Widget loaded. Look at the bottom-right corner of this panel \\u2192</div>',
                );
              } else {
                setStageUnavailable();
                markLaunched(false);
              }
            });
          };
          s.onerror = function () {
            setStageUnavailable();
            markLaunched(false);
          };
          document.body.appendChild(s);
        }
        var launchBtn = document.getElementById('launch');
        if (launchBtn) launchBtn.addEventListener('click', launchWidget);

        // If the user already launched the widget in this tab, auto-restart on
        // iframe reload (e.g. Mintlify remount when the site theme toggles) so
        // they don't have to click Launch preview again.
        if (viewParam === 'stage' && wasLaunchedBefore()) {
          launchWidget();
        }

        // ---------------------------------------------------------------------
        // Initial render
        // ---------------------------------------------------------------------
        renderForm();
        renderSnippet();
      })();
    </script>
  </body>
</html>
`;

export const withView = view => PLAYGROUND_HTML.replace('<head>', '<head><script>window.__PLAYGROUND_VIEW=' + JSON.stringify(view) + ';</' + 'script>');

<div style={{ display: 'grid', gridTemplateColumns: 'minmax(0, 1fr) 280px', gap: 12, alignItems: 'start' }}>
  <div style={{ display: 'grid', gap: 12, minWidth: 0 }}>
    <iframe
      srcDoc={withView('stage')}
      loading="lazy"
      style={{
    width: '100%',
    height: 620,
    aspectRatio: 'auto',
    border: '1px solid rgba(128,128,128,0.18)',
    borderRadius: 10,
    background: 'transparent',
    display: 'block',
    overflow: 'hidden',
    outline: 'none',
    colorScheme: 'normal',
  }}
      title="Widget preview"
    />

    <iframe
      srcDoc={withView('snippet')}
      loading="lazy"
      style={{
    width: '100%',
    height: 260,
    aspectRatio: 'auto',
    border: '1px solid rgba(128,128,128,0.18)',
    borderRadius: 10,
    background: 'transparent',
    display: 'block',
    overflow: 'hidden',
    outline: 'none',
    colorScheme: 'normal',
  }}
      title="Widget snippet"
    />
  </div>

  <iframe
    srcDoc={withView('form')}
    loading="lazy"
    style={{
  width: '100%',
  height: 892,
  aspectRatio: 'auto',
  border: '1px solid rgba(128,128,128,0.18)',
  borderRadius: 10,
  background: 'transparent',
  display: 'block',
  overflow: 'hidden',
  position: 'sticky',
  top: 96,
  outline: 'none',
  colorScheme: 'normal',
}}
    title="Widget options"
  />
</div>

<Tip>
  The preview uses a shared demo workspace so you can try everything without signing up. Replace the `token` in the generated snippet with your own (dashboard → **Channels → Chat Widget**) before shipping.
</Tip>

## What You Can Tweak Here

<CardGroup cols={2}>
  <Card title="Identity" icon="id-card">
    Bot name, avatar, and language.
  </Card>

  <Card title="Theme" icon="paintbrush">
    Primary color, palette, border radius, launcher size and position.
  </Card>

  <Card title="First visit" icon="hand-wave">
    Initial messages and suggested questions.
  </Card>

  <Card title="Behavior" icon="sliders">
    Open-by-default, auto-open delay, visitor data collection, single-session mode.
  </Card>
</CardGroup>

## Need Something More Advanced?

The playground covers the options most sites need. For deeper control — custom components, CSS overrides, header buttons, or programmatic hooks — head to the code-based references:

<CardGroup cols={2}>
  <Card title="Configuration" icon="gear" href="/widget/configuration">
    Full option reference with every field and its defaults.
  </Card>

  <Card title="Custom React Components" icon="react" href="/widget/custom-components-react">
    Render AI action results as typed React components.
  </Card>

  <Card title="Headless" icon="code" href="/widget/custom-components-headless">
    Build fully custom chat UI with the headless package.
  </Card>

  <Card title="Install the widget" icon="download" href="/widget/install-widget">
    Script tag, React package, or headless — pick your integration.
  </Card>
</CardGroup>
