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

# resultCard

> Displays an AI Overview card above the results with an Agent Studio recommendation based on the query and the top hits.

export const customLabel_0 = undefined

export const FlavorSwitcher = ({current, baseHref = "", options = [], label = "InstantSearch framework"}) => {
  if (options.length === 0) {
    return <div className="not-prose" role="alert" style={{
      margin: "0.25rem 0 1.5rem",
      padding: "0.75rem",
      border: "1px solid #f59e0b",
      borderRadius: "0.625rem",
      color: "inherit",
      fontSize: "0.875rem"
    }}>
        FlavorSwitcher requires at least one option.
      </div>;
  }
  const selected = options.find(option => option.value === current) ?? options[0];
  return <div className="not-prose mint-flavor-switcher">
      <style>{`
        .mint-flavor-switcher {
          --mfs-bg: #ffffff;
          --mfs-bg-hover: #f4f4f5;
          --mfs-bg-current: #eef2ff;
          --mfs-border: #d4d4d8;
          --mfs-fg: #18181b;
          --mfs-muted: #71717a;
          --mfs-accent: #4f46e5;
          position: relative;
          width: min(100%, 19rem);
          margin: 0.25rem 0 1.5rem;
          color: var(--mfs-fg);
          font-size: 0.875rem;
          line-height: 1.25rem;
        }

        .dark .mint-flavor-switcher {
          --mfs-bg: #18181b;
          --mfs-bg-hover: #27272a;
          --mfs-bg-current: #272747;
          --mfs-border: #3f3f46;
          --mfs-fg: #fafafa;
          --mfs-muted: #a1a1aa;
          --mfs-accent: #a5b4fc;
        }

        .mint-flavor-switcher details {
          position: relative;
        }

        .mint-flavor-switcher summary {
          display: flex;
          min-height: 2.75rem;
          box-sizing: border-box;
          align-items: center;
          justify-content: space-between;
          gap: 0.75rem;
          padding: 0.625rem 0.75rem;
          border: 1px solid var(--mfs-border);
          border-radius: 0.625rem;
          background: var(--mfs-bg);
          color: var(--mfs-fg);
          cursor: pointer;
          font-weight: 600;
          list-style: none;
          transition: border-color 150ms ease, box-shadow 150ms ease;
        }

        .mint-flavor-switcher summary::-webkit-details-marker {
          display: none;
        }

        .mint-flavor-switcher summary:hover {
          border-color: var(--mfs-accent);
        }

        .mint-flavor-switcher summary:focus-visible {
          outline: 2px solid var(--mfs-accent);
          outline-offset: 2px;
        }

        .mint-flavor-switcher__label {
          overflow: hidden;
          text-overflow: ellipsis;
          white-space: nowrap;
        }

        .mint-flavor-switcher__chevron {
          flex: none;
          transition: transform 150ms ease;
        }

        .mint-flavor-switcher details[open] .mint-flavor-switcher__chevron {
          transform: rotate(180deg);
        }

        .mint-flavor-switcher__menu {
          position: absolute;
          z-index: 50;
          top: calc(100% + 0.375rem);
          left: 0;
          width: 100%;
          box-sizing: border-box;
          margin: 0;
          padding: 0.375rem;
          border: 1px solid var(--mfs-border);
          border-radius: 0.625rem;
          background: var(--mfs-bg);
          box-shadow: 0 12px 30px rgb(0 0 0 / 16%);
          list-style: none;
        }

        .mint-flavor-switcher__menu li {
          margin: 0;
          padding: 0;
        }

        .mint-flavor-switcher__option {
          display: grid;
          gap: 0.125rem;
          padding: 0.625rem 0.75rem;
          border-radius: 0.4rem;
          color: var(--mfs-fg);
          text-decoration: none;
        }

        .mint-flavor-switcher__option:hover {
          background: var(--mfs-bg-hover);
        }

        .mint-flavor-switcher__option:focus-visible {
          outline: 2px solid var(--mfs-accent);
          outline-offset: -2px;
        }

        .mint-flavor-switcher__option[aria-current="page"] {
          background: var(--mfs-bg-current);
          color: var(--mfs-accent);
        }

        .mint-flavor-switcher__name {
          font-weight: 600;
        }

        .mint-flavor-switcher__description {
          color: var(--mfs-muted);
          font-size: 0.8125rem;
        }

        @media (prefers-reduced-motion: reduce) {
          .mint-flavor-switcher summary,
          .mint-flavor-switcher__chevron {
            transition: none;
          }
        }
      `}</style>

      <details>
        <summary aria-label={`${label}: ${selected.label}`}>
          <span className="mint-flavor-switcher__label">{selected.label}</span>
          <svg className="mint-flavor-switcher__chevron" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
            <path d="m6 9 6 6 6-6" />
          </svg>
        </summary>

        <ul className="mint-flavor-switcher__menu" aria-label={label}>
          {options.map(option => {
    const isCurrent = option.value === selected.value;
    const href = option.href ?? `${baseHref.replace(/\/$/, "")}/${encodeURIComponent(option.value)}`;
    return <li key={option.value}>
                <a className="mint-flavor-switcher__option" href={href} aria-current={isCurrent ? "page" : undefined}>
                  <span className="mint-flavor-switcher__name">
                    {option.label}
                  </span>
                  {option.description ? <span className="mint-flavor-switcher__description">
                      {option.description}
                    </span> : null}
                </a>
              </li>;
  })}
        </ul>
      </details>
    </div>;
};

<div className="mint-flavor-switcher-slot not-prose">
  <FlavorSwitcher
    current="js"
    baseHref="/doc/api-reference/widgets/result-card"
    options={[
{ value: "js", label: "JavaScript", description: "InstantSearch.js" },
{ value: "react", label: "React", description: "React InstantSearch" },
]}
  />
</div>

<Callout icon="flask-conical" color="#14b8a6">
  This widget is **{customLabel_0 || "experimental"}** and is subject to change in minor versions.
</Callout>

For more information, see [Agent Studio](/doc/guides/algolia-ai/agent-studio).

```ts Signature theme={"system"}
resultCard({
  container: string | HTMLElement,
  agentId: string,
  // Optional parameters, at most one of
  transport?: object,
  requestOptions?: object,
  // Optional parameters
  templates?: object,
  cssClasses?: object,
  translations?: object,
});
```

## Import

<CodeGroup>
  ```js Package manager theme={"system"}
  import { resultCard } from 'instantsearch.js/es/widgets';
  ```

  ```js CDN theme={"system"}
  const { resultCard } = instantsearch.widgets;
  ```
</CodeGroup>

## About this widget

Use the `resultCard` widget to display an "AI Overview" card above your results.
The widget sends the query, the active filters, and the first five hits to an Agent Studio agent as [per-turn context](/doc/guides/algolia-ai/agent-studio/how-to/turn-context),
and asks the agent which of these results it recommends and why.
The answer streams into the card, and a skeleton placeholder shows until the first text arrives.

The card only shows when the search meets both conditions:

* A [rule](/doc/guides/managing-results/rules/rules-overview) that matches the search sets `renderingContent.widgets.resultCard.enabled` to `true` in the search response. For more information, see [Activate the card with a rule](#activate-the-card-with-a-rule).
* The query has at least two words.

The widget sends a new request only when the query, the filters, or the first five hits change.
Going to another page of results doesn't send a new request.

Users can collapse the card to its header, which hides the answer, the follow-up suggestions, and the **Continue in chat** button.
The answer keeps generating in the meantime, and the card stays collapsed for new searches until users restore it.
The card clips long answers and shows a **Show more** button to reveal the full text.
If the answer completes without any text, the card doesn't render.

When a [`chat`](/doc/api-reference/widgets/chat/js) widget with the same `agentId` is on the same index,
the card shows a **Continue in chat** button and up to two follow-up suggestions from the agent.
Both hand the card's conversation to the chat and open it.
A suggestion is also sent to the chat as the next message.
If the chat is generating a response, the handoff doesn't happen.
That makes the widget an [entry point](/doc/api-reference/widgets/chat/js#param-disable-trigger-validation) for the chat, so a page with both doesn't also need a [`chatTrigger`](/doc/api-reference/widgets/chat-trigger/js) widget.
Without a matching `chat` widget, the card hides these actions and, in development, logs a warning.

## Activate the card with a rule

The widget adds the [rule context](/doc/api-reference/api-parameters/ruleContexts) `agent-studio-result-card-AGENT_ID` to every search,
where `_` replaces any character in the agent ID other than letters, digits, `_`, and `-`.
Use this context as a rule condition to limit the rule to searches that include the widget.

To show the card, add a rule whose consequence sets [`renderingContent`](/doc/api-reference/api-parameters/renderingContent) in its search parameters:

```json JSON icon=braces theme={"system"}
{
  "renderingContent": {
    "widgets": {
      "resultCard": {
        "enabled": true
      }
    }
  }
}
```

## Examples

```js JavaScript icon=code theme={"system"}
resultCard({
  container: '#result-card',
  agentId: 'AGENT_ID',
});

chat({
  container: '#chat',
  agentId: 'AGENT_ID',
});
```

## Options

<ParamField body="container" type="string | HTMLElement" required>
  The CSS Selector or `HTMLElement` to insert the widget into.

  <CodeGroup>
    ```js string theme={"system"}
    resultCard({
      container: '#result-card',
    });
    ```

    ```js HTMLElement theme={"system"}
    resultCard({
      container: document.querySelector('#result-card'),
    });
    ```
  </CodeGroup>
</ParamField>

<ParamField body="agentId" type="string" required>
  The unique identifier of the agent to connect to.
  You can find the `agentId` in the [Agent Studio dashboard](https://dashboard.algolia.com/generativeAi/agent-studio/agents).
  It's required even with a custom [`transport`](#param-transport), because the widget derives its rule context from it.
  The card only hands its conversation to a `chat` widget with the same `agentId`.

  ```js JavaScript icon=code theme={"system"}
  resultCard({
    // ...
    agentId: 'AGENT_ID',
  });
  ```
</ParamField>

<ParamField body="transport" type="object">
  A custom transport object to send the card's requests to your own backend instead of Agent Studio.
  It has the same shape as the [`transport`](/doc/api-reference/widgets/chat/js#param-transport) option of the `chat` widget.
  You can't use it together with [`requestOptions`](#param-request-options).

  ```js JavaScript icon=code theme={"system"}
  resultCard({
    // ...
    transport: {
      api: 'https://chatapi.example.com/api/v1/chat',
    },
  });
  ```
</ParamField>

<ParamField body="requestOptions" type="object">
  Request options to send with the built-in Agent Studio completion requests.
  You can't use it together with [`transport`](#param-transport).

  The object accepts the following properties:

  * `queryParameters` (`Record<string, string | number | boolean>`). Query parameters to append to each completion request.
  * `headers` (`Record<string, string> | Headers`). Headers to send with each completion request.

  ```js JavaScript icon=code theme={"system"}
  resultCard({
    // ...
    requestOptions: {
      headers: {
        'X-Session-Id': 'SESSION_ID',
      },
    },
  });
  ```
</ParamField>

<ParamField body="templates" type="object">
  The [templates](#templates) to use for the widget.

  ```js JavaScript icon=code theme={"system"}
  resultCard({
    // ...
    templates: {
      // ...
    },
  });
  ```
</ParamField>

<ParamField body="cssClasses" type="object">
  The [CSS classes you can override](/doc/guides/building-search-ui/widgets/customize-an-existing-widget/js#style-your-widgets):

  * `root`. The root element of the widget.
  * `header`. The header element.
  * `headerTitle`. The header title element.
  * `continueButton`. The **Continue in chat** button.
  * `minimizeButton`. The button that minimizes or maximizes the card.
  * `body`. The element that holds the answer and clips it.
  * `loader`. The skeleton placeholder shown while the answer loads.
  * `message`. Each answer message.
  * `suggestions`. The follow-up suggestions container.
  * `expandButton`. The button that shows or clips a long answer.

  ```js JavaScript icon=code theme={"system"}
  resultCard({
    // ...
    cssClasses: {
      root: 'MyCustomResultCard',
      body: ['MyCustomResultCardBody', 'MyCustomResultCardBody--subclass'],
    },
  });
  ```
</ParamField>

<ParamField body="translations" type="object">
  A dictionary of translations to customize the UI text and support internationalization.

  * `headerTitle`. The title displayed in the header. Defaults to `"AI Overview"`.
  * `continueInChatText`. The text of the button that hands the conversation to the chat. Defaults to `"Continue in chat"`.
  * `minimizeLabel`. The accessible label of the button that minimizes the card. Defaults to `"Minimize"`.
  * `maximizeLabel`. The accessible label of the button that maximizes a minimized card. Defaults to `"Maximize"`.
  * `expandText`. The text of the button that shows a clipped answer in full. Defaults to `"Show more"`.
  * `collapseText`. The text of the button that clips an expanded answer. Defaults to `"Show less"`.
  * `retryText`. The text of the retry button shown when the request fails. Defaults to `"Retry"`.

  ```js JavaScript icon=code theme={"system"}
  resultCard({
    // ...
    translations: {
      headerTitle: 'Our pick',
      continueInChatText: 'Ask a follow-up question',
    },
  });
  ```
</ParamField>

## Templates

You can customize parts of a widget's UI using the Templates API.

Each template includes an `html` function,
which you can use as a [tagged template](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals#tagged_templates).
This function safely renders templates as HTML strings and works directly in the browser—no build step required.
For details, see [Templating your UI](/doc/guides/building-search-ui/widgets/customize-an-existing-widget/js#templating-your-ui).

<Note>
  The `html` function is available in InstantSearch.js version 4.46.0 or later.
</Note>

<ParamField body="layout" type="function">
  A template to replace the default card with custom markup.
  The template owns the full rendering, including every status and the retry and handoff actions.
  It receives the same data as the [connector render state](#connector-render-state).

  ```js JavaScript icon=code expandable theme={"system"}
  resultCard({
    // ...
    templates: {
      layout({ status, messages, error, retry }, { html }) {
        if (status === 'hidden') {
          return html``;
        }

        if (status === 'failed') {
          return html`<div>
            <p>${error ? error.message : 'Something went wrong.'}</p>
            <button onClick=${retry}>Retry</button>
          </div>`;
        }

        const answer = messages
          .filter((message) => message.role === 'assistant')
          .flatMap((message) => message.parts)
          .filter((part) => part.type === 'text')
          .map((part) => part.text)
          .join('');

        return html`<section>
          <p>${answer || 'Loading...'}</p>
        </section>`;
      },
    },
  });
  ```
</ParamField>

## HTML output

```html HTML icon=code-xml theme={"system"}
<section class="ais-ResultCard" data-status="complete" aria-live="polite">
  <div class="ais-ResultCard-header">
    <span class="ais-ResultCard-headerTitle">
      <svg>...</svg>
      AI Overview
    </span>
    <button class="ais-Button ais-Button--outline ais-Button--sm ais-ResultCard-continueButton">
      Continue in chat
    </button>
    <button class="ais-Button ais-Button--ghost ais-Button--sm ais-Button--icon-only ais-ResultCard-minimizeButton" title="Minimize" aria-label="Minimize" aria-expanded="true">
      <svg>...</svg>
    </button>
  </div>
  <div class="ais-ResultCard-body ais-ResultCard-body--clipped">
    <article class="ais-ChatMessage ais-ChatMessage--left ais-ChatMessage--subtle ais-ResultCard-message">...</article>
  </div>
  <button class="ais-Button ais-Button--ghost ais-Button--sm ais-ResultCard-expandButton" aria-expanded="false">
    Show more
    <svg>...</svg>
  </button>
</section>
```

While the answer loads, the body holds a skeleton placeholder instead of the message:

```html HTML icon=code-xml theme={"system"}
<div class="ais-ResultCard-body">
  <div class="ais-ResultCard-loader">
    <div class="ais-ResultCard-loaderLine"></div>
    <div class="ais-ResultCard-loaderLine"></div>
    <div class="ais-ResultCard-loaderLine"></div>
  </div>
</div>
```

When users collapse the card, the `ais-ResultCard-minimizeButton` button has `aria-expanded="false"` and the `Maximize` label, and the body, the expand button, the **Continue in chat** button, and the suggestions don't render.

When users expand a long answer, or when the request fails, the body has the `ais-ResultCard-body--expanded` class.
When the answer is complete and isn't clipped, the follow-up suggestions render after the body in a `div` with the `ais-ChatPromptSuggestions ais-ResultCard-suggestions` classes.

## Connector render state

Use `connectResultCard` to build a fully custom UI.
It accepts the same `agentId`, `transport`, and `requestOptions` options as the widget.
Its render state exposes:

* `status` (`'hidden' | 'loading' | 'streaming' | 'complete' | 'failed'`). The card status. It's `hidden` when no rule enables the card or when the query is shorter than two words. It's `loading` while the widget waits to send the request and until the response starts streaming.
* `query` (`string`). The query the answer is about.
* `messages` (`UIMessage[]`). The card's conversation: the user message, then the assistant answer as it streams.
* `error` (`Error | undefined`). The error of the latest failed request. It's only set when `status` is `failed`.
* `suggestions` (`string[] | undefined`). Follow-up suggestions sent by the agent with the answer.
* `retry` (`() => void`). Sends the request again.
* `canContinueInChat` (`boolean`). Whether a `chat` widget with the same `agentId` is on the same index.
* `continueInChat` (`(message?: string) => void`). Hands the conversation to the `chat` widget and opens it. When you pass `message`, the chat sends it as the next message.
* `expanded` (`boolean`). Whether a long answer shows in full instead of clipped.
* `setExpanded` (`(expanded: boolean) => void`). Shows or clips a long answer.
* `sendEvent` (`function`). Sends an event to the [`insights`](/doc/api-reference/widgets/insights/js) middleware.

The user message in `messages` contains the full question that the widget sends to the agent.
To display the query, use `query` instead.

```js JavaScript icon=code expandable theme={"system"}
import { connectResultCard } from 'instantsearch.js/es/connectors';

const renderResultCard = (renderOptions, isFirstRender) => {
  const { status, messages, canContinueInChat, continueInChat } =
    renderOptions;
  const container = document.querySelector('#result-card');

  if (isFirstRender) {
    return;
  }

  if (status === 'hidden') {
    container.innerHTML = '';
    return;
  }

  const answer = messages
    .filter((message) => message.role === 'assistant')
    .flatMap((message) => message.parts)
    .filter((part) => part.type === 'text')
    .map((part) => part.text)
    .join('');

  container.innerHTML = '';

  const text = document.createElement('p');
  text.textContent = answer || 'Loading...';
  container.appendChild(text);

  if (status === 'complete' && canContinueInChat) {
    const continueButton = document.createElement('button');
    continueButton.textContent = 'Continue in chat';
    continueButton.addEventListener('click', () => continueInChat());
    container.appendChild(continueButton);
  }
};

const customResultCard = connectResultCard(renderResultCard);

search.addWidgets([
  customResultCard({
    agentId: 'AGENT_ID',
  }),
]);
```
