> ## 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="react"
    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).

```tsx Signature theme={"system"}
<ResultCard
  // Required prop
  agentId={string}
  // Optional props, at most one of
  transport={object}
  requestOptions={object}
  // Optional props
  layoutComponent={function}
  classNames={object}
  translations={object}

  ...props={ComponentProps<'section'>}
/>
```

## Import

```jsx JavaScript icon=code theme={"system"}
import { ResultCard } from "react-instantsearch";
```

## About this widget

Use `ResultCard` 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/react) 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/react#param-disable-trigger-validation) for the chat, so a page with both doesn't also need a [`ChatTrigger`](/doc/api-reference/widgets/chat-trigger/react) 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

```jsx JavaScript icon=code expandable theme={"system"}
import React from "react";
import { liteClient as algoliasearch } from "algoliasearch/lite";
import { InstantSearch, Chat, Hits, ResultCard, SearchBox } from "react-instantsearch";

const searchClient = algoliasearch("ALGOLIA_APPLICATION_ID", "ALGOLIA_SEARCH_API_KEY");

function App() {
  return (
    <InstantSearch indexName="INDEX_NAME" searchClient={searchClient}>
      <SearchBox />
      <ResultCard agentId="AGENT_ID" />
      <Hits />
      <Chat agentId="AGENT_ID" />
    </InstantSearch>
  );
}
```

## Props

<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`.

  ```jsx JavaScript icon=code theme={"system"}
  <ResultCard agentId="AGENT_ID" />
  ```
</ParamField>

<ParamField body="transport" type="HttpChatTransportInitOptions">
  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/react#param-transport) prop of the `Chat` widget.
  You can't use it together with [`requestOptions`](#param-request-options).

  ```jsx JavaScript icon=code theme={"system"}
  <ResultCard
    agentId="AGENT_ID"
    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.

  ```jsx JavaScript icon=code theme={"system"}
  <ResultCard
    agentId="AGENT_ID"
    requestOptions={{
      headers: {
        "X-Session-Id": "SESSION_ID",
      },
    }}
  />
  ```
</ParamField>

<ParamField body="layoutComponent" type="(props: ResultCardLayoutComponentProps) => JSX.Element | null">
  A component to replace the default card with custom markup.
  The component owns the full rendering, including every status and the retry and handoff actions.
  It receives the same values as the [`useResultCard`](#hook) Hook.

  ```jsx JavaScript icon=code expandable theme={"system"}
  function CustomLayout({ status, messages, error, retry }) {
    if (status === "hidden") {
      return null;
    }

    if (status === "failed") {
      return (
        <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 (
      <section>
        <p>{answer || "Loading..."}</p>
      </section>
    );
  }

  <ResultCard agentId="AGENT_ID" layoutComponent={CustomLayout} />;
  ```
</ParamField>

<ParamField body="classNames" type="Partial<ResultCardClassNames>">
  The CSS classes you can override and pass to the widget's elements.
  It's useful to style widgets with class-based CSS frameworks like [Bootstrap](https://getbootstrap.com/) or [Tailwind CSS](https://tailwindcss.com/).

  * `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.

  ```jsx JavaScript icon=code theme={"system"}
  <ResultCard
    agentId="AGENT_ID"
    classNames={{
      root: "MyCustomResultCard",
      body: ["MyCustomResultCardBody", "MyCustomResultCardBody--subclass"],
    }}
  />
  ```
</ParamField>

<ParamField body="translations" type="Partial<ResultCardTranslations>">
  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"`.

  ```jsx JavaScript icon=code theme={"system"}
  <ResultCard
    agentId="AGENT_ID"
    translations={{
      headerTitle: "Our pick",
      continueInChatText: "Ask a follow-up question",
    }}
  />
  ```
</ParamField>

<ParamField body="...props" type="React.ComponentProps<'section'>">
  Any `section` prop to forward to the root element of the widget.
  It doesn't apply when you set [`layoutComponent`](#param-layout-component).

  ```jsx JavaScript icon=code theme={"system"}
  <ResultCard agentId="AGENT_ID" className="MyCustomResultCard" />
  ```
</ParamField>

## Hook

Use the `useResultCard` Hook to build a fully custom UI.
It accepts the same `agentId`, `transport`, and `requestOptions` props as the widget and returns:

* `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.
  The user message contains the full question that the widget sends to the agent. To display the query, use `query` instead.
* `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 middleware.

```jsx JavaScript icon=code expandable theme={"system"}
import { useResultCard } from "react-instantsearch";

function CustomResultCard() {
  const { status, messages, canContinueInChat, continueInChat } =
    useResultCard({
      agentId: "AGENT_ID",
    });

  if (status === "hidden") {
    return null;
  }

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

  return (
    <section>
      <p>{answer || "Loading..."}</p>
      {status === "complete" && canContinueInChat && (
        <button onClick={() => continueInChat()}>Continue in chat</button>
      )}
    </section>
  );
}
```
