# Chat widget verification and troubleshooting

Read this reference before styling chat in any site build or restyle, and when a user asks to
add, verify, or debug the answer.cloud chat/search widget.
The widget and its API are platform-owned; site authors supply answerable static content.

## Verification flow

1. Run `list_sites` and identify the exact site, kind, live URL, current deploy, and page usage.
2. Run `list_deploys`; confirm the intended deploy is `live`.
3. Open the live URL. Do not use the local Astro preview to judge widget injection.
4. Confirm the page contains substantive HTML and the injected widget launcher.
5. Open the widget and submit one short, non-sensitive question grounded in the page
   whenever live chat verification is in scope, including every first launch.
6. Confirm the response renders, relevant references use the site's canonical host, and the
   browser reports no failed chat request.

Do not add `https://app.answer.cloud/embed.js` to Astro source. The platform injects it before
`</body>` during publishing.

## Brand-matched styling workflow

For a site build or visual restyle, create a chat theme by default unless the user explicitly
requests the stock theme or disables chat. For a new unbranded site, begin with the official
answer.cloud style template's `public/assets/answercloud-chat.css`. Treat it as a starting point:
older copies may style the launcher wrapper without its label, or put borders and backgrounds
around legacy message spacing. Apply the quality requirements below before shipping it.

1. Read the site's global style sources and inspect representative rendered components. Extract
   the fonts, surfaces, text colors, accent and on-accent colors, border, focus ring, radii,
   shadow, and motion character actually in use. Do not invent a second brand system.
2. Write the theme to `public/assets/answercloud-chat.css`. Astro must emit it unchanged at
   `dist/assets/answercloud-chat.css`.
3. Scope every selector under `#llmo-chat-button`, `#llmo-chat-iframe`, or
   `#llmo-chat-widget`. The same URL loads into the customer's page and the iframe, so unscoped
   rules such as `:root`, `html`, `body`, `*`, `button`, `a`, or generic utility classes can
   damage the host site.
4. Exercise all meaningful states: launcher default/hover/focus, desktop/mobile frame, empty
   chat, user and AI messages, long Markdown, links and references, composer focus, send/close/
   clear controls, loader, errors, and reduced motion.
5. Build first and verify the asset exists. For a `byo_cms` artifact, update the package script
   so `assets/answercloud-chat.css` ships next to `assets/aeo-cms.css`.
6. A new hosting site’s first production deploy automatically attaches this asset when no
   CSS URL is configured. Existing deployed sites and explicit themes are preserved. Once an
   authorized deploy is live, inspect `list_sites`, verify the canonical HTTPS asset, and use
   `configure_widget` only when needed for theme or copy. Never point live configuration at
   localhost, a preview server, or a path that has not deployed yet.

The supported stable hooks are:

- Host page: `#llmo-chat-button` is a wrapper containing `.llmo-chat-button-icon` and a
  read-only input, `.llmo-chat-button-input`. The visible label is that input's `::placeholder`,
  not text on the wrapper. `#llmo-chat-iframe` is the outer panel frame.
- Chat iframe: `#llmo-chat-widget`, `.llmo-chat-popup`, `.llmo-messages`,
  `.llmo-message.llmo-user`, `.llmo-message.llmo-ai`, `.llmo-markdown`,
  `.llmo-message-references`, `.composer-wrap`, `.composer`, `.chat_textarea`,
  `.chat_send`, `.llmo-close`, `.llmo-clear-chat`, `.llmo-loader`, and `.ai-icon`.
- Inside the iframe, `.llmo-messages` is the outer scroll region; `#llmo-messages` is the
  inner list of messages. They are different elements. The composer `.chat_textarea` is a
  `contenteditable` div with a `:empty::before` placeholder, not a native textarea. Target
  these real hooks; guessed selectors such as `header` or `.assistant-message` may match nothing.
- Legacy theme variables on `#llmo-chat-widget` remain supported: `--bg`, `--ink`, `--ink-mid`, `--muted`,
  `--panel`, `--ring`, `--ring-soft`, `--brand`, `--accent`, `--accent-deep`,
  `--accent-soft`, `--accent-tint`, `--accent-tint-2`, `--accent-line`, `--radius`,
  `--ease-brand`, `--font-sans`, and `--font-mono`.

Use the shared properties below on all three roots. The launcher and iframe are separate
HTML documents, so declaring tokens only on the panel does not style the launcher. Host-page
variables and font declarations do not cross into the iframe: provide resolved values or define
every referenced variable in this standalone theme. Replace these neutral example values with
the site's actual palette; any color is supported.

```css
#llmo-chat-button,
#llmo-chat-iframe,
#llmo-chat-widget {
  --ac-chat-font-family: "Site Sans", system-ui, sans-serif;
  --ac-chat-font-size: 16px;
  --ac-chat-surface: #ffffff;
  --ac-chat-text: #1a1b1c;
  --ac-chat-secondary-text: #3a3d40;
  --ac-chat-muted: #667085;
  --ac-chat-panel: #f6f5f4;
  --ac-chat-border: #d9d9d9;
  --ac-chat-accent: #1a1b1c;
  --ac-chat-on-accent: #ffffff;
  --ac-chat-focus-ring: #1a1b1c;
  --ac-chat-launcher-text: #1a1b1c;
  --ac-chat-icon-color: #1a1b1c;
  --ac-chat-user-surface: #f0f1f2;
  --ac-chat-assistant-surface: transparent;
  --ac-chat-input-surface: #ffffff;
  --ac-chat-radius: 8px;
}

#llmo-chat-iframe {
  --ac-chat-frame-width: min(420px, calc(100vw - 48px));
  --ac-chat-frame-height: min(640px, calc(100dvh - 48px));
  --ac-chat-mobile-width: calc(100vw - 16px);
  --ac-chat-mobile-height: min(640px, calc(100dvh - 16px));
  --ac-chat-frame-padding: 0px;
  --ac-chat-frame-shadow: 0 16px 48px rgb(26 27 28 / 16%);
}
```

Use `--ac-chat-icon-color` for the supplied Answer Cloud spark, `--ac-chat-link-color` for
answer links, `--ac-chat-user-surface` / `--ac-chat-assistant-surface` for message backgrounds,
and `--ac-chat-send-background` / `--ac-chat-send-color` for the send button. Other optional
properties control `launcher-text`, `launcher-radius`, `launcher-width`, `launcher-shadow`,
`mobile-width`, `mobile-height`, `offset-bottom`, `offset-right`, `message-radius`, `code-surface`,
`input-surface`, `control-surface`, `send-shadow`, and `loader-shine` (all prefixed `--ac-chat-`).
Frame geometry uses these properties directly, so token changes do not need `!important`.

Keep custom fonts available inside the iframe as well as the host page. Theme loading is
nonblocking, with a brief bounded first-paint hold for fast stylesheets. Slow or failed themes
fall back to usable stock chat; a late stylesheet can apply without restarting the conversation.
Existing selectors and legacy variables remain valid; migrate a working older theme only when
requested. Check stock chat and an existing custom theme alongside the new palette.

## Chat design requirements

Use a calm, readable conversation surface with compact controls and restrained brand accents.
Carry the site's font, palette, border treatment, and radii into chat without copying decorative
hero or card styles into every message. A themed asset that loads successfully is not sufficient.

### Launcher: one surface with a visible invitation

- Use a short, explicit label such as "Ask Example" or "Ask a question". Keep it visible beside
  the spark in default, hover, and focus states. Do not ship a blank field or an unexplained icon
  in place of the label unless the user explicitly asks for an icon-only launcher.
- Style `.llmo-chat-button-input`, its `::placeholder`, and the icon as a unit. Set
  `--ac-chat-launcher-text` and `button_text_color` against the **input's actual background**;
  white text on a white input disappears even when the wrapper is brightly colored. Keep
  placeholder opacity at 1. Verify the computed colors, not just the configured hex values.
- Prefer a neutral input surface, subtle border, and one soft floating shadow. Keep the wrapper
  transparent or the same surface; remove old accent fills, thick borders, and redundant shadows
  that create a heavy frame around the input. Reserve a strong outline for keyboard focus.
- Keep the control at least 44px tall and within the viewport, including its padding and page
  offset. Preserve room for the icon and label at 320px width. Do not override the platform's
  open/close `display`, `opacity`, `transform`, or pointer-event state with `!important`.

### Panel: compact conversation layout

- Use one continuous panel. Default to left-aligned assistant text on its surface and compact,
  right-aligned user bubbles. Avoid full-width bordered cards around each answer or greeting,
  repeated dividers, oversized avatars, and decorative empty space.
- Use roughly 16px body text, 1.5–1.65 line height, 16–20px content insets, and 12–16px between
  messages. Keep the greeting to one or two useful sentences. A short reply should occupy only
  the height its content needs; never set a minimum message height.
- Normalize legacy layout when restyling messages: stock assistant rules include
  `margin-top: -92px` and `padding-top: 112px`, and user messages float with a `54px` top margin.
  Adding a background or border without resetting those rules produces tall, overlapping cards.
  Use a normal flow or column flex list and reset **both** roles together. For example:

  ```css
  #llmo-chat-widget #llmo-messages {
    display: flex;
    flex-direction: column;
    gap: 16px;
    min-width: 0;
  }
  #llmo-chat-widget .llmo-message.llmo-user,
  #llmo-chat-widget .llmo-message.llmo-ai {
    float: none;
    position: static;
    margin: 0;
    min-width: 0;
    max-width: 100%;
    padding: 0;
  }
  #llmo-chat-widget .llmo-message.llmo-user {
    align-self: flex-end;
    max-width: 85%;
    padding: 8px 12px;
  }
  #llmo-chat-widget .llmo-message.llmo-ai {
    align-self: stretch;
    background: transparent;
    border: 0;
    box-shadow: none;
  }
  ```

  Replace conflicting inherited theme rules, including old `!important` declarations, rather
  than layering this example under them. Prefer tokens for colors and scoped rules for layout.
- Keep clear/close controls in a reserved top area, with at least 44px hit areas. Messages must
  not run behind them. Keep the Answer Cloud attribution visible, but do not apply answer-link
  underlines or card decoration to `.ai-icon`; scope prose links to `.llmo-markdown` and references.
- Wrap long words and URLs; constrain images to the message width. Wide tables and code blocks
  may scroll within the answer, but must not widen the panel or create page-level scrolling.

### Composer: always available

- Keep the composer visible at the panel bottom while **only the transcript scrolls**. Prefer
  `#llmo-chat-widget .llmo-chat-popup { grid-template-rows: minmax(0, 1fr) auto; }`, a scroll region with
  `min-height: 0`, and a composer in normal flow (`position: relative` instead of the stock
  absolute positioning). Reserve top space for controls and bottom safe-area padding. If keeping
  an overlay composer, reserve its actual expanded height so it cannot cover the last answer.
- Show the "Ask a question…" placeholder, typed text, caret, focus ring, and send icon clearly.
  Style `.chat_textarea` and its `:empty::before` placeholder; native textarea rules alone do
  nothing here. Use at least 16px input text, a comfortable initial height around 52px, and a
  bounded multiline height with internal scrolling. Leave room for a 44px send target.
- Keep send, close, and clear controls visible and distinct in default, focus, disabled, and
  loading states. Preserve keyboard behavior: Enter sends, Shift+Enter inserts a newline, and
  focus reaches the input and controls. Do not hide failure text or loading feedback with CSS.
- Check text and placeholder contrast against their own surfaces (at least 4.5:1 for normal
  text), and control icons/focus indicators (at least 3:1). Check dark mode only if supported by
  the site, and honor reduced motion. Do not use a faint translucent accent as the only focus cue.

### Required visual and interaction check

After an authorized deployment, inspect the **actual injected launcher and iframe** at a desktop
viewport and at 320px and 390px mobile widths, including a short viewport or open software keyboard.
Local Astro pages and HTTP success alone cannot verify chat. Capture and inspect the closed
launcher, empty open chat, a completed exchange, and a long/scrolling exchange; do not send private
content. Exercise loading, failure, and long Markdown with controlled local fixtures if needed,
without generating excessive live requests. Check that:

- The launcher invitation is readable and has no unintended colored frame.
- Short messages have compact spacing; controls, attribution, references, and answers do not overlap.
- The composer and send control remain reachable before and after sending, with long content,
  multiline input, and a reduced viewport. The latest answer can scroll fully above the composer.
- Keyboard focus is visible; Enter, Shift+Enter, clear, close, and reopen work. The host page
  retains its original layout and controls.

Fix visual failures before calling the chat verified. If browser or mobile-keyboard testing is
unavailable, report that specific gap; do not claim it passed. For local-only work, validate the
theme against the platform markup in a temporary fixture and report live verification as pending;
do not add widget markup or scripts to the site's production source or deploy solely to test it.

## Configure the live theme

Configure after the stylesheet is live:

```json
{
  "site_slug": "example",
  "enabled": true,
  "config": {
    "css_path": "https://example.answer.cloud/assets/answercloud-chat.css",
    "chat": {
      "button_text": "Ask Example",
      "intro_text": "What can we help you find?",
      "button_text_color": "#1a1b1c"
    }
  }
}
```

`config` replaces the stored widget configuration when supplied; `{}` resets custom settings.
Omit `config` to toggle `enabled` without losing an existing theme. Text must be concise and
site-specific. `button_text_color` must be a readable hex color.

After configuration, inspect the canonical live page at desktop and mobile widths. Confirm the
CSS URL returns successfully, `list_sites` shows it under `widget.config.css_path`, the launcher
is styled in the parent page, the panel is styled inside the iframe, focus is visible, text meets
contrast expectations, content does not clip, and the host page outside the widget is unchanged.

## Identify the request path

The widget iframe URL contains the platform organization and site identifiers. Treat session
identifiers and chat history as private; do not quote or persist them.

The widget normally makes two independent requests:

- `POST https://app.answer.cloud/api/messages/get_pages` retrieves relevant indexed pages.
- `POST https://app.answer.cloud/api/messages` creates the conversation and generates the answer.

Use the same short, non-sensitive question for both checks. Prefer the live widget first. When a
direct HTTP diagnostic is necessary, send only the minimum JSON fields and do not include API
keys, cookies, personal information, or copied chat history.

## HTTP fallback for a meaningful chat check

Prefer a conversation through the live widget. If browser interaction is unavailable but
HTTP requests are authorized, exercise the public message API with a short, non-sensitive
question whose answer is present on this deployed site:

```text
POST https://app.answer.cloud/api/messages
Content-Type: application/json
```

```json
{
  "organization_id": 123,
  "site_origin": "https://reserved-site.answer.cloud",
  "content": "What services does this organization offer?"
}
```

Replace `123` with the bootstrap's actual numeric `organization_id`, `site_origin` with
its actual reserved `site_url`, and the question with one appropriate to the published
content. Preserve an explicit test application's origin rather than switching environments.
`site_origin` scopes hosted-site retrieval and reference URLs; an organization id alone
must not be used to assume the right site. If the live widget supplies a `site_id`, preserve
that real id as well rather than guessing one.

This endpoint is public and does not use the guest MCP credential. Do not send a build
Bearer token, API key, cookies, account email, personal information, or a custom system
prompt. A successful create returns HTTP 201 with JSON `conversation`, `user_message`,
`ai_message`, and `references`. Check the status and content type before parsing. Verify
that `ai_message.content` is substantive and agrees with the actual site's facts; a generic
greeting, nonempty error text, or HTTP success alone does not prove a useful answer. Check
any cited URLs against the intended live site.

For a follow-up, retain the returned `conversation.id` privately in session memory and
send it as `conversation_id` with the same `organization_id` and `site_origin`, plus the
follow-up `content`. Do not include private conversations, raw responses, or session
identifiers in logs, project files, screenshots, or downloadable artifacts. The test
creates an ordinary stored site conversation, so keep its content non-sensitive.

An optional retrieval diagnostic uses the same `organization_id`, `site_origin`, and
`content` JSON at `POST https://app.answer.cloud/api/messages/get_pages`; it returns
`{ "titles": [...] }`. Relevant titles do not establish that answer generation works.
An API conversation verifies message generation only. Report widget rendering, controls,
and any unavailable visual checks as unverified; do not describe this fallback as a
successful browser test. If no meaningful chat interaction can be completed, retain the
working site URL and state precisely what remains unresolved.

## Interpret failures

| Observation | Likely layer | Next action |
| --- | --- | --- |
| No widget launcher on a live page | Platform injection or stale deploy | Confirm the current deploy, valid HTML with `</body>`, and live hostname. Do not add the script manually. |
| Theme is absent everywhere | Missing/bad `css_path` or asset | Confirm `list_sites`, fetch the absolute CSS URL, and verify the file shipped in the live deploy. |
| Iframe is themed but launcher is not | Stale `embed.js` or unscoped selector mismatch | Confirm the current embed loads the theme in the parent page and that launcher selectors start with `#llmo-chat-button` or `#llmo-chat-iframe`. |
| Host website changes after theming | Leaking custom CSS | Remove unscoped selectors; every rule must begin with a supported widget ID. |
| `get_pages` fails | Ingestion, embeddings, or retrieval | Inspect deploy warnings, page ingestion, and whether published pages have embeddings. |
| `get_pages` succeeds with relevant titles, but `/api/messages` returns 500 | Chat-generation backend | Capture the request ID and response type; investigate the platform application. A site redeploy is not a fix. |
| `/api/messages` succeeds, but the widget shows a generic error | Widget parsing or rendering | Inspect the response shape and browser error. Confirm the endpoint returned JSON. |
| Answer renders but cites the wrong host | Canonical URL construction | Verify hosted pages use `Site#canonical_url`; verify companion pages use the configured `ai.` host. |
| Answer has no useful context | Content or retrieval quality | Confirm the page is ingested, answerable in raw HTML, and retrieved by `get_pages`. |

An HTML 500 page from `/api/messages` is especially important: the widget expects JSON, so
attempting to parse HTML usually collapses to “Sorry, something went wrong.” Record the
`x-request-id`, but do not mistake this client fallback for the root cause.

## Platform-source checks

Only inspect or change the answer.cloud application repository when the user has put that backend in
scope. Otherwise report the cross-repository defect and the evidence needed by the platform owner.

For hosted `hosting` sites:

- Public page and reference URLs must derive from `page.site.canonical_url`.
- A hosted site at `{slug}.answer.cloud` must not require
  `Organization#subdomain` or `Organization#custom_domain`.
- Normalize `source_path` to its pretty public URL. For example, `index.html` becomes `/`, and
  `guides/index.html` becomes `/guides/`.

For `byo_cms` companion sites:

- Canonical URLs should resolve through the site's `ai.{custom_domain}` host.
- Missing organization domain configuration is a real provisioning error; do not silently replace
  it with a new `hosting` site.

When fixing canonical URL handling, add regression coverage for:

1. A standalone hosted site with no custom domain.
2. A standalone hosted site with a custom domain.
3. A `byo_cms` companion site.
4. Root and nested `index.html` source paths.
5. Chat references and generated `.md` mirror URLs.

Also make the widget's message client check `response.ok` and content type before parsing JSON so
future server failures preserve useful diagnostics instead of surfacing only a generic parsing
error.

## Completion gate

Do not call chat fixed until all of these are true:

- Retrieval returns the expected deployed page title.
- Message generation returns a JSON success response.
- The live widget renders the answer.
- References use the canonical live hostname.
- No chat request returns 4xx/5xx.
- The launcher and iframe match the site's visual system at desktop and mobile widths.
- The required visual and interaction check above passes, including readable launcher text,
  compact messages, and a reachable composer after scrolling and resizing.
- The custom stylesheet causes no style changes outside the widget.
- The evidence comes from the live site, not only a local unit test.

Workflow version: launch-f4679309a5dc7dc7