# Role and objective

You're a helpful assistant that can answer questions and help with tasks.

## Instructions

- Always help the user as best you can.
- Always use markdown formatting in your responses.
- If you don't know the answer, tell the user to contact support.

## Examples

User: How do I log in?
Assistant: Navigate to the [login page](/login) and enter your email.

User: What's the weather in Tokyo?
Assistant: I don't know the answer to that question, please contact support.
```

### When should AI use knowledge?

This setting determines when AI will search back-end knowledge before
responding.

```text title="When should AI use knowledge?"
Whenever the user asks a question about code.
```

You can also write a more complex prompt, giving knowledge a name, which then
allows you to refer to it in your system prompt. This can help reinforce AI
behavior.

```text title="When should AI use knowledge?"
Whenever the user asks a question about code.

// +++
This is your **knowledge base**.
// +++
```

```text title="System prompt"
## Instructions

// +++
- When a user asks about React, searching your **knowledge base** is required.
// +++
...
```

## Adding back-end knowledge

Under the “Knowledge” tab you can add back-end knowledge to your copilot.
Knowledge can be submitted as web pages or files, and your AI chat can query
this knowledge when responding, allowing it to intelligently answer questions or
perform tasks.

<Figure>
  <Image
    src="/assets/ai-copilots/copilot-knowledge.png"
    alt="Screenshot of the Liveblocks dashboard, adding knowledge to a copilot"
    width={720}
    height={409}
    quality={100}
  />
</Figure>

You can submit a whole website/sitemap for crawling, a single page, or PDF/image
files. Crawled knowledge is not updated, and to update it, it must be
resubmitted programmatically or via the dashboard.

## Configuring advanced settings

Under the “Advanced” tab you can configure a number of advanced settings for
your copilot. By default, Liveblocks does not pass default values to your
provider, and leaves the options blank.

| Setting                                                                                                                                                                                                                                                     | Description                                           | Example value          |
| ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------- | ---------------------- |
| Max tokens <InfoTooltip>The maximum number of tokens that the copilot can use per response.</InfoTooltip>                                                                                                                                                   | Controls response length                              | `1024`                 |
| Temperature <InfoTooltip>Temperature setting. The value is passed through to the provider. The range depends on the provider and model. It is recommended to set either `temperature` or `topP`, but not both.</InfoTooltip>                                | Controls randomness in responses                      | `0.2`                  |
| Top P <InfoTooltip>Nucleus sampling. The value is passed through to the provider. The range depends on the provider and model. It is recommended to set either `temperature` or `topP`, but not both.</InfoTooltip>                                         | Alternative to temperature for controlling randomness | `0.9`                  |
| Top K <InfoTooltip>Only sample from the top K options for each subsequent token. Used to remove "long tail" low probability responses. Recommended for advanced use cases only. You usually only need to use temperature.</InfoTooltip>                     | Advanced sampling control                             | `200`                  |
| Presence penalty <InfoTooltip>Presence penalty setting. It affects the likelihood of the model to repeat information that is already in the prompt. The value is passed through to the provider. The range depends on the provider and model.</InfoTooltip> | Controls repetition of existing content               | `0.3`                  |
| Frequency penalty <InfoTooltip>Frequency penalty setting. It affects the likelihood of the model to repeatedly use the same words or phrases. The value is passed through to the provider. The range depends on the provider and model.</InfoTooltip>       | Controls repetition of words and phrases              | `0.3`                  |
| Stop sequences <InfoTooltip>Sequences that will stop the generation of the text. If the model generates any of these sequences, it will stop generating further text.</InfoTooltip>                                                                         | Controls when text generation stops                   | `["\\n\\n", "Human:"]` |
| Seed <InfoTooltip>The seed (integer) to use for random sampling. If set and supported by the model, calls will generate deterministic results.</InfoTooltip>                                                                                                | Controls randomness for reproducible results          | `42`                   |
| Max retries <InfoTooltip>Maximum number of retries. Set to 0 to disable retries. Default: 2.</InfoTooltip>                                                                                                                                                  | Controls how many times to retry failed requests      | `2`                    |

## Modifying copilots programmatically

You aren’t limited to creating copilots from the dashboard—you can also create
and modify copilots programmatically, allowing users or teams in your app to
have their own individual copilots.

```ts
import { Liveblocks } from "@liveblocks/node";

const liveblocks = new Liveblocks({
  secret: "{{SECRET_KEY}}",
});

// +++
const copilot = await liveblocks.createAiCopilot({
  name: "My AI Assistant",
  systemPrompt: "You're a helpful assistant that can answer questions.",
  provider: "openai",
  providerModel: "gpt-4",
  providerApiKey: "sk-...",
});
// +++
```

This is made possible using the
[Liveblocks Node.js client](/docs/api-reference/liveblocks-node#AI-Copilots) and
[REST API](/docs/api-reference/rest-api-endpoints#AI), where a number of APIs
are available for managing copilots.

- [`getAiCopilots`](/docs/api-reference/liveblocks-node#get-ai-copilots)
- [`createAiCopilot`](/docs/api-reference/liveblocks-node#create-ai-copilot)
- [`getAiCopilot`](/docs/api-reference/liveblocks-node#get-ai-copilot)
- [`updateAiCopilot`](/docs/api-reference/liveblocks-node#update-ai-copilot)
- [`deleteAiCopilot`](/docs/api-reference/liveblocks-node#delete-ai-copilot)

You can also manage each copilot’s knowledge sources.

- [`createWebKnowledgeSource`](/docs/api-reference/liveblocks-node#create-web-knowledge-source)
- [`createFileKnowledgeSource`](/docs/api-reference/liveblocks-node#create-file-knowledge-source)
- [`getKnowledgeSources`](/docs/api-reference/liveblocks-node#get-knowledge-sources)
- [`getKnowledgeSource`](/docs/api-reference/liveblocks-node#get-knowledge-source)
- [`getFileKnowledgeSourceMarkdown`](/docs/api-reference/liveblocks-node#get-file-knowledge-source-markdown)
- [`getWebKnowledgeSourceLinks`](/docs/api-reference/liveblocks-node#get-web-knowledge-source-links)
- [`deleteWebKnowledgeSource`](/docs/api-reference/liveblocks-node#delete-web-knowledge-source)
- [`deleteFileKnowledgeSource`](/docs/api-reference/liveblocks-node#delete-file-knowledge-source)

---
meta:
  title: "Default components"
  parentTitle: "AI Copilots"
  description: "Ready-to-use customizable components"
---

The default components included in AI Copilots are a great way to start building
AI into your application. With these components you can render advanced AI
chats, that understand your application state, modify it with actions, and
render custom in-chat components.

- Fully styled AI chat components, with an optional dark mode.
- Pass in knowledge, add actions, and render custom components.
- Customize through CSS variables and class names.
- Localize and modify strings with overrides.

## AiChat

The [`AiChat`][] component renders an AI chat, with a chat history and a
composer for adding new messages.

<Figure>
  <Image
    src="/assets/ai-copilots/ai-chat.jpg"
    alt="AiChat"
    width={768}
    height={446}
  />
</Figure>

### Usage

Get started by importing the component, and passing in a unique chat ID.

```tsx
import { AiChat } from "@liveblocks/react-ui";

function Component() {
  return <AiChat chatId="my-chat-id" />;
}
```

This will render an AI chat on the page, with a chat history, and a composer for
adding new messages. Each chat is stored permanently, and can be accessed again
later.

{/* TODO list everything you can do with it */}

## AiTool

When
[registering a tool](/docs/ready-made-features/ai-copilots/tools#Registering-a-tool),
the [`AiTool`][] component can be used to
[show tool progress and results](/docs/ready-made-features/ai-copilots/tools)
within AI chats. It shows the tool’s name, its stage (e.g. a spinner when
executing, a checkmark when successful, etc.), and optionally an icon and custom
content inside it.

{/* TODO AiTool image */}

### Usage

```tsx
import { defineAiTool } from "@liveblocks/client";
import { RegisterAiTool, AiTool, AiChat } from "@liveblocks/react-ui";

function App() {
  return (
    <>
      <RegisterAiTool
        name="get-weather"
        tool={defineAiTool()({
          description: "Get current weather information",
          parameters: {
            type: "object",
            properties: {
              location: { type: "string", description: "City name" },
            },
            required: ["location"],
            additionalProperties: false,
          },
          execute: async (args) => {
            const { temperature, condition } = await __getWeather__(
              args.location
            );
            return { data: { temperature, condition } };
          },
          render: ({ result }) => {
            return (
              // +++
              <AiTool title="Weather" icon="🌤️">
                {result.data ? (
                  <div>
                    {result.data.temperature}°F - {result.data.condition}
                  </div>
                ) : null}
              </AiTool>
              // +++
            );
          },
        })}
      />
      <AiChat chatId="my-chat" />
    </>
  );
}
```

## AiTool.Confirmation

Inside of tools, the [`AiTool.Confirmation`][] component is a sub-component of
[`AiTool`][] that renders confirm/deny buttons as part of a
[human-in-the-loop tool action](/docs/ready-made-features/ai-copilots/tools#Default-confirmation-component).
“Confirm” and Cancel” buttons are displayed to the user.

{/* TODO AiTool.Confirmation image */}

### Usage

```tsx
<RegisterAiTool
  name="delete-document"
  tool={defineAiTool()({
    description: "Delete a document by its ID",
    parameters: {
      type: "object",
      properties: {
        documentId: { type: "string" },
      },
      required: ["documentId"],
      additionalProperties: false,
    },
    render: ({ stage, args, result, types }) => {
      return (
        <AiTool title="Delete document" variant="minimal">
          // +++
          <AiTool.Confirmation
            types={types}
            confirm={async ({ documentId }) => {
              await __deleteDocument__(documentId);
              return {
                data: { documentId },
                description: "The user chose to delete the document",
              };
            }}
            cancel={() => {
              return {
                data: { documentId },
                description: "The user cancelled deleting the document",
              };
            }}
          />
          // +++
        </AiTool>
      );
    },
  })}
/>
```

## Customization

It’s possible to style and localize the default components:

- Import dark mode styles.
- Modify the style with CSS variables and class names.
- Use overrides to change default text used in the components.

Learn more under
[styling and customization](/docs/ready-made-features/ai-copilots/styling-and-customization):

[`aichat`]: /docs/api-reference/liveblocks-react-ui#AiChat
[`aitool`]: /docs/api-reference/liveblocks-react-ui#AiTool
[`aitool.confirmation`]:
  /docs/api-reference/liveblocks-react-ui#AiTool.Confirmation

---
meta:
  title: "Features"
  parentTitle: "AI Copilots"
  description: "Learn about AI Copilots"
---

Liveblocks AI Copilots provides customizable UI components that let your users
interact with AI in a way that feels native to your product. Unlike basic chat
widgets, Copilots are context-aware, collaborative, and capable of performing
real tasks—such as editing content, navigating your app, or answering
product-specific questions. They’re built for React developers, work with your
chosen LLM, and integrate directly into your existing UI using flexible APIs and
fully themeable components.

<Figure>
  <video width={1592} height={1080} autoPlay loop muted playsInline>
    <source
      src="/assets/ai-copilots/ai-copilots-features.mp4"
      type="video/mp4"
    />
  </video>
</Figure>

<Banner type="info" title="Try AI Copilots">

If you’d like to try AI Copilots, [book a demo with our team](/contact/sales),
and we’ll reach out to you and enable it on your account.

</Banner>
 
## Features

- **[Persistent chats](#persistent-chats)**: No database is required, each
  user’s chats are stored permanently.
- **[Tools](#tools)**: Easily allow AI to interact with your application and
  modify its state.
- **[Knowledge](#knowledge)**: Feed text and files into the AI so it understands
  the current context.
- **[Ready-made UI](#ready-made-ui)**: Polished, easily customizable React
  components to add to your app.
- **[Custom chat components](#custom-chat-components)**: Register React
  components that the AI can choose to render in-chat.
- **[Text editor toolbar](#text-editor-toolbar)**: AI suggestions toolbar for
  your collaborative Tiptap text editor.
- **[Manage AI copilots](#manage-ai-copilots)**: Configure AI providers and
  system prompts from our dashboard.
- **[Supported AI providers](#supported-ai-providers)**: Anthropic, OpenAI,
  Google Gemini, and more.
- **[Coming soon](#coming-soon)**: One-off prompts, attachments, MCP servers.

## Persistent chats [#persistent-chats]

When building a chat interface, it’s important to save the chat history so that
the user can continue the conversation from where they left off. All chats and
messages are stored automatically by Liveblocks, and each user has
[their own set of chats](/docs/ready-made-features/ai-copilots/hooks#List-a-user's-chats).
No database is required, and messages are streamed into the chat in realtime
using WebSockets.

<Figure>
  <Image
    src="/assets/ai-copilots/multiple-chats.png"
    alt="An example showing a list of different chats, each with custom metadata, and a create new chat button."
    width={768}
    height={512}
  />
</Figure>

Each page of your application can have multiple different chats, and it’s easy
to switch between them, much like in ChatGPT. Each chat has a unique name, and
can be given custom metadata, for example a custom title, description, tags, or
anything you like.

### Automatic synchronization

When the page refreshes, each user’s previous chats will load for them, and can
be continued. If a user has your app open in multiple browser tabs, each tab
will correctly display chats, and update in real-time.

## Tools [#tools]

[Tools](/docs/ready-made-features/ai-copilots/tools) are a way to allow AI to
make actions, modify your application state, interact with your front end, or
render custom content with your own components. You can use them to extend the
capabilities of AI Copilots beyond simple text-based interactions. For example,
you may have tools that create new documents in your app, automatically fill in
form data, invite members to a project, or anything else you like.

<Figure>
  <Image
    src="/assets/ai-copilots/document-changes.png"
    alt="An example of a document with a chat app in the corner. The chat has run a tool, and has edited the document."
    width={768}
    height={512}
  />
</Figure>

Tools are defined in your code, and are executed and/or rendered when the AI
requests to use them.

### Tools work with our other products

Through tools, you can integrate AI Copilots into our other products, such as
[Comments](/docs/ready-made-features/comments),
[Notifications](/docs/ready-made-features/notifications), and
[Sync Datastore](/docs/platform/sync-datastore). For example, you can allow AI
to add comments to your application, send notifications to other users, or add
shapes to a collaborative drawing app.

## Knowledge [#knowledge]

It’s simple to
[add knowledge and context to your AI](/docs/ready-made-features/ai-copilots/knowledge),
so that it understands the current document or page. You can submit webpages and
files, such as documentation, that the copilot will deeply internalize, allowing
it to reply intelligently. Additionally, certain models can also search the
web.

<Figure>
  <Image
    src="/assets/ai-copilots/context.png"
    alt="An example of a document containing numbers. The AI chat in the corner has been asked about the numbers, and understands the context of the document."
    width={768}
    height={512}
  />
</Figure>

## Ready-made UI [#ready-made-ui]

Liveblocks AI Copilots includes a set of styled UI components that can be used
to add an AI chat interface to your application. Messages are streamed in
realtime.

<Figure>
  <Image
    src="/assets/ai-copilots/floating-chat.png"
    alt="A screenshot of the ready-made chat component, with messages and AI suggestion buttons."
    width={768}
    height={512}
  />
</Figure>

We also provide [a number of hooks](/docs/ready-made-features/ai-copilots/hooks)
that allow you to extend your chat, or create fully custom chat interfaces.

## Custom chat components [#custom-chat-components]

Inside your chat, you can register custom React components that the AI can
choose to render as a response. For example, if your app contains charts, AI can
choose to render a custom chart component instead of a message. If you have
multiple components, AI can choose which one to render.

<Figure>
  <Image
    src="/assets/ai-copilots/custom-component.png"
    alt="An example of the AI rendering a custom chat component instead of a message. It's a graph component."
    width={768}
    height={512}
  />
</Figure>

These components can be fully interactive, for example this chart component
could have a button that lets you save it, and add it to a project.

## Text editor toolbar

Using our
[Text Editor integration for Tiptap](/docs/ready-made-features/text-editor/tiptap),
we provide an AI toolbar that can be added to your collaborative text editor.
This toolbar allows you to select text, and ask AI to make changes for you, for
example fixing typos, and creating new paragraphs.

<Figure>
  <Image
    src="/assets/ai-copilots/ai-toolbar.png"
    alt="An example of the AI toolbar in a text editor, with an 'Ask Copilot' button."
    width={768}
    height={512}
  />
</Figure>

Learn more about this React component under
[`AiToolbar`](/docs/api-reference/liveblocks-react-tiptap#AiToolbar)

## Manage AI copilots [#manage-ai-copilots]

The [Liveblocks dashboard](/dashboard) allows you to create, configure, and
manage your AI copilots, each of which can be used in different parts of your
application. You can select your [AI provider](#supported-ai-providers) (e.g.
OpenAI, Anthropic), specify a system prompt, and pass in your secret key to get
it working.

<Figure>
  <Image
    src="/assets/ai-copilots/dashboard-manage-copilot.png"
    alt="A screenshot of our dashboard, showing the settings for an AI copilot."
    width={768}
    height={512}
  />
</Figure>

You can also fine-tune how each copilot interacts with users by adjusting each
model’s settings, which are passed through directly to the AI provider. These
can influence its creativity, consistency, and the safety of generated content.
Each copilot can be configured independently, and tested live in the dashboard.

## Supported AI providers [#supported-ai-providers]

<Figure>
  <Image
    src="/assets/ai-copilots/providers.png"
    alt="We support different AI providers"
    width={768}
    height={512}
  />
</Figure>

In our dashboard, you can create, configure, and manage copilots powered by
different AI providers, each with different settings. The following providers
are supported out-of-the-box:

- Anthropic
- OpenAI
- Other OpenAI-compatible APIs
- Google Gemini

### Reasoning models

Reasoning models are supported by our built-in components, allowing models to
show their thought processes.

---

## Coming soon [#coming-soon]

## One-off prompts [#one-off-prompts]

One-off prompts are a way to add assorted AI features to your app, specifically
features that don’t require a chat, such as an AI button on a page.
[Tools](#tools) and [context](#knowledge) are supported, allowing AI to interact
with your application, and understand its state. An example use case for a
one-off prompt is a button that uses AI to fill in a form.

<Figure>
  <Image
    src="/assets/ai-copilots/one-off-prompts.png"
    alt="An example use for a one-off prompt, an AI button that fills in a table for you."
    width={768}
    height={512}
  />
</Figure>

### Chat attachments

Upload files into the chat, which AI can modify, or use for extra context. These
files can be images, PDFs, text documents, or any file type your AI provider can
understand. Files are automatically stored by Liveblocks.

<Figure>
  <Image
    src="/assets/ai-copilots/attachment.png"
    alt="A zoomed-in screenshot of a chat with an uploaded attachment, ready to send."
    width={768}
    height={512}
  />
</Figure>

### MCP server integration

MCP is a protocol for running AI agents on a server, allowing you to make
various back ends calls to different services. In future, AI Copilots will
support this.

### Additional providers

We will be investigating additional providers, such as LangGraph, Crew AI,
Bedrock, and Vertex AI. If there is a specific provider you’d like us to
support, please [book a demo with our team](/contact/sales) and let us know more
information in the text box.

---
meta:
  title: "Hooks"
  parentTitle: "AI Copilots"
  description: "React hooks for building custom AI interfaces"
---

The AI Copilots React hooks allow you to fetch, create, and modify AI chats,
enabling you to build custom AI interfaces, even beyond our
[default components](/docs/ready-made-features/ai-copilots/default-components).
Chats are stored permanently and the infrastructure is handled for you. All
hooks work optimistically, meaning they update immediately, before the server
has synched.

## List a user’s chats

Each [authenticated user](/docs/authentication) has their own private set of
chats, and the [`useAiChats`](/docs/api-reference/liveblocks-react#useAiChats)
hook fetches all AI chats created by the current user. It’s easy to
[create a list of chats](/docs/api-reference/liveblocks-react#List-the-user's-chats-and-switch-between-them),
with links or buttons that take you to each.

```tsx
import { useAiChats } from "@liveblocks/react/suspense";

function ListChats() {
  // +++
  const { chats } = useAiChats();
  // +++

  return (
    <nav>
      // +++
      {chats.map((chat) => (
        <a key={chat.id} href={`/chats/${chat.id}`}>
          {chat.title || "Untitled"}
        </a>
      ))}
      // +++
    </nav>
  );
}
```

Chats are
[paginated](/docs/api-reference/liveblocks-react#useAiChats-pagination),
returning 50 at a time, and you can
[filter chats by metadata](/docs/api-reference/liveblocks-react#useAiChats-query).
There are also ways to
[handle errors](/docs/api-reference/liveblocks-react#useAiChats-error-handling).

## Create & delete chats

The [`useCreateAiChat`](/docs/api-reference/liveblocks-react#useCreateAiChat)
and [`useDeleteAiChat`](/docs/api-reference/liveblocks-react#useDeleteAiChat)
hooks allow you to create and delete AI chats. When used in combination with the
[`useAiChats`](/docs/api-reference/liveblocks-react#useAiChats) hook, you can
add “New Chat” and “Delete Chat” buttons to your listing.

```tsx
import {
  useAiChats,
  useCreateAiChat,
  useDeleteAiChat,
} from "@liveblocks/react/suspense";

function ListChats() {
  const { chats } = useAiChats();
  // +++
  const createAiChat = useCreateAiChat();
  const deleteAiChat = useDeleteAiChat();
  // +++

  return (
    <nav>
      // +++
      <button onClick={() => createAiChat("my-ai-chat")}>💬 New Chat</button>
      // +++
      {chats.map((chat) => (
        <a key={chat.id} href={`/chats/${chat.id}`}>
          {chat.title || "Untitled"}
          // +++
          <button onClick={() => deleteAiChat(chat.id)}>❌ Delete chat</button>
          // +++
        </a>
      ))}
    </nav>
  );
}
```

When an AI chat is created, a title is automatically generated from the first
messages. You can optionally set this title, and add custom metadata to the
chat.

```tsx
createAiChat({
  id: "my-ai-chat",
  // +++
  title: "My AI Chat",
  metadata: {
    color: "red",
    tags: ["product", "engineering"],
  },
  // +++
});
```

{/* prettier-ignore */}
{/* TODO add back later when we enable it

## List a chat’s messages

The
[`useAiChatMessages`](/docs/api-reference/liveblocks-react#useAiChatMessages)
hook allows you to fetch all messages in a specific chat. Use this to display
the main content of a chat. User and assistant messages can be rendered
differently, and below we’re returning different UI for each. Other types of
messages can be rendered separately too, for example tool calls, knowledge
calls, custom components.

```tsx
import { useAiChatMessages } from "@liveblocks/react/suspense";

function ChatMessages({ chatId }: { chatId: string }) {
  // +++
  const { messages } = useAiChatMessages(chatId);
  // +++

  return (
    <div>
      {messages.map((message) => {
        // +++
        if (message.role === "user") {
          // +++
          return (
            // +++
            <div key={message.id}>
              {message.content.map((part) => (
                <p>👤 You: {part.text}</p>
              ))}
            </div>
            // +++
          );
        }

        return (
          // +++
          <div key={message.id}>
            {(message.contentSoFar ?? message.content).map((part) => (
              <p>🤖 Assistant: {part.text}</p>
            ))}
          </div>
          // +++
        );
      })}
    </div>
  );
}
```

Note that if you’re using our default
[`AiChat`](/docs/api-reference/liveblocks-react-ui#AiChat) component, you don’t
need to `useAiChatMessages`, as the messages are fetched for you. This hook is
only required for building custom chat interfaces.

\*/}

## Send messages to a chat

The [`useSendAiMessage`](/docs/api-reference/liveblocks-react#useSendAiMessage)
hook allows you to send messages directly to a chat, as if from a user. This
works really well in combination with buttons in your UI, for example you may
have a “Explain with AI” button.

```tsx
import { useSendAiMessage } from "@liveblocks/react/suspense";

function ExplainWithAi() {
  // +++
  const sendAiMessage = useSendAiMessage("my-chat-id", {
    copilotId: "co_h7GBa3...",
  });
  // +++

  return (
    // +++
    <button onClick={() => sendAiMessage("Explain how this page works")}>
      Explain with AI
    </button>
    // +++
  );
}
```

Additionally, when using
[`AiChat`](/docs/api-reference/liveblocks-react-ui#AiChat) you can use the hook
to display suggestion buttons in the
[empty state](/docs/api-reference/liveblocks-react-ui#AiChat-placeholder).

```tsx
import { useSendAiMessage } from "@liveblocks/react/suspense";
import { AiChat } from "@liveblocks/react-ui";

function Chat() {
  // +++
  const sendAiMessage = useSendAiMessage("my-chat-id", {
    copilotId: "co_h7GBa3...",
  });
  // +++

  return (
    <AiChat
      chatId="my-chat-id"
      copilotId="co_h7GBa3..."
      components={{
        Empty: (
          <div>
            <div>How can I help you?</div>
            // +++
            <button onClick={() => sendAiMessage("What's new?")}>
              Update me
            </button>
            <button onClick={() => sendAiMessage("Create a new document")}>
              Draft a document
            </button>
            // +++
          </div>
        ),
      }}
    />
  );
}
```

## Get a chat’s information

The [`useAiChat`](/docs/api-reference/liveblocks-react#useAiChat) hook allows
you to fetch a
[chat’s title](/docs/api-reference/liveblocks-react#Displaying-a-default-title)
and
[custom metadata](/docs/api-reference/liveblocks-react#Create-a-chat-with-a-custom-title-and-metadata).

```tsx
import { useAiChat } from "@liveblocks/react/suspense";

function ChatTitle({ chatId }: { chatId: string }) {
  // +++
  const { chat } = useAiChat(chatId);
  // +++

  // +++
  return <h1>{chat.title || "Untitled chat"}</h1>;
  // +++
}
```

## Get a chat’s status

The [`useAiChatStatus`](/docs/api-reference/liveblocks-react#useAiChatStatus)
hook allows you to fetch the status of an AI chat, indicating whether it’s idle
or currently generating content. This is helpful for displaying a loading state
or freezing part of your app while content is generating.

```tsx
import { useAiChatStatus } from "@liveblocks/react/suspense";

function ChatStatus({ chatId }: { chatId: string }) {
  // +++
  const { status } = useAiChatStatus(chatId);
  // +++

  // +++
  return status === "idle" ? <div>🟢 Ready</div> : <div>🟡 Generating…</div>;
  // +++
}
```

## Hook types [#hook-types]

There are two different ways to use many Liveblocks hooks; with
[React Suspense](https://react.dev/reference/react/Suspense), and without it. We
recommend using the Suspense versions, as they often result in simpler code.

### Suspense hooks [#suspense-hooks]

Using Suspense hooks means that any data retrieved, for example `chats` from
`useAiChats`, will never be `undefined`, and your component will never see an
error.

```tsx
import { useAiChats } from "@liveblocks/react/suspense";

// Suspense: `chats` is always defined
function MyAiChats() {
  const { chats } = useAiChats();

  // [{ id: "th_sf8s6sh...", title: "...", ... }, ...]
  console.log(chats);
}
```

To catch errors and display a loading screen, you can use
[`ErrorBoundary`](https://www.npmjs.com/package/react-error-boundary) and
[`ClientSideSuspense`](/docs/api-reference/liveblocks-react#suspense-avoid-ssr).

```tsx highlight="7-11"
import { ClientSideSuspense } from "@liveblocks/react/suspense";
import { ErrorBoundary } from "react-error-boundary";

// Handle errors and loading state in the component above
function Component() {
  return (
    <ErrorBoundary fallback={<div>Error</div>}>
      <ClientSideSuspense fallback={<div>Loading...</div>}>
        <MyAiChats />
      </ClientSideSuspense>
    </ErrorBoundary>
  );
}
```

To use Suspense, make sure you’re exporting your hooks from
`@liveblocks/react/suspense`.

```tsx
// Suspense version of hooks
import { useAiChats, useAiChatMessages } from "@liveblocks/react/suspense";
//                                                               ^^^^^^^^
```

### Regular hooks [#regular-hooks]

The regular versions of Liveblocks hooks require you to check for `error` and
`isLoading` properties. You can then handle these states in the same component.

```tsx
import { useAiChats } from "@liveblocks/react";

// Handle errors and loading state in the same component
function MyAiChats() {
  const { chats, error, isLoading } = useAiChats();

  if (error) {
    return <div>Error</div>;
  }

  if (isLoading) {
    return <div>Loading...</div>;
  }

  // Non-Suspense: `chats` is only defined AFTER the `if` checks
  // [{ id: "th_sf8s6sh...", title: "...", ... }, ...]
  console.log(chats);
}
```

To use the regular hooks, make sure you’re exporting from `@liveblocks/react`.

```tsx
// Regular version of hooks
import { useAiChats, useAiChatMessages } from "@liveblocks/react";
//                                             ^^^^^^^^^^^^^^^^^
```

---
meta:
  title: "Knowledge"
  parentTitle: "AI Copilots"
  description: "Add context and files to your AI copilots"
---

Knowledge allows you to provide information to your AI so it can understand your
application’s state, content, and domain-specific information, making responses
more relevant and accurate. There are two different knowledge types—_front-end
knowledge_, ideal for adding small pieces of contextual information, and
_back-end knowledge_, designed for larger datasets such as entire websites and
lengthy PDFs. Additionally, you can enable web search, allowing your AI to query
the internet for information.

## Front-end knowledge

Front-end knowledge is data that’s passed to your AI from the browser through
[React](/docs/api-reference/liveblocks-react#RegisterAiKnowledge). This is most
useful for passing contextual, user-specific, information, for example:

- **User info**: The user’s local time, date, location, and language.
- **Account info**: The user’s projects and which payment plan they’re on.
- **Page info**: Which URL the user is viewing and the content of the current
  document.

### How it works [#front-end-knowledge-how-it-works]

Front-end knowledge is string or JSON-serializable data that is passed to the AI
with every message the user sends. The AI reads all front-end knowledge before
responding. It’s easy to reach AI provider token limits with this approach, so
limit your front-end knowledge to relatively small amounts of data, not multiple
pages of text.

Because front-end knowledge is simply passed along with every message, and is
never specifically queried, no messages are displayed in the UI when it’s used.

### Adding front-end knowledge

You can add front-end knowledge to your chat by using the
[`RegisterAiKnowledge`](/docs/api-reference/liveblocks-react#RegisterAiKnowledge)
component anywhere in your application.

```tsx
import { RegisterAiKnowledge } from "@liveblocks/react";
import { AiChat } from "@liveblocks/react-ui";

function Chat() {
  return (
    <>
      <AiChat chatId="my-chat-id" />
      // +++
      <RegisterAiKnowledge
        description="The current user's payment plan"
        value={{ name: "Advanced Plan", expires: "2026-01-01" }}
      />
      // +++
    </>
  );
}
```

After adding this, the AI will understand that the user is on the Advanced Plan
and be able to answer questions on it, for example:

> User: _What's my current plan?_
>
> Assistant: _You're on the Advanced Plan, it expires on January 1st, 2026._

### Combine front-end knowledge with tools

You can combine front-end knowledge with
[tools](/docs/ready-made-features/ai-copilots/tools) to create an AI assistant
that can take actions. For example, say you have a document on the current page.
You can use knowledge to share the document content with the AI, then create a
tool that allows AI to edit the document.

```tsx
import { RegisterAiKnowledge } from "@liveblocks/react";
import { AiChat } from "@liveblocks/react-ui";
import { RegisterAiTool } from "@liveblocks/react";
import { defineAiTool } from "@liveblocks/client";
import { useState } from "react";

function Document() {
  // +++
  const [document, setDocument] = useState("Hello world");
  // +++

  return (
    <>
      <AiChat chatId="my-chat-id" />
      <RegisterAiKnowledge
        description="The current document"
        // +++
        value={document}
        // +++
      />
      <RegisterAiTool
        name="edit-document"
        tool={defineAiTool()({
          description: "Edit the document's text",
          parameters: {
            type: "object",
            properties: {
              text: { type: "string" },
            },
          },
          execute: ({ args }) => {
            // +++
            setDocument(args.text);
            // +++
            return { data: {}, description: "Document updated" };
          },
        })}
      />
    </>
  );
}
```

Learn about more ways to use it in our documentation under
[`RegisterAiKnowledge`](/docs/api-reference/liveblocks-react#RegisterAiKnowledge).

## Back-end knowledge

Back-end knowledge is data that’s passed to your AI from the server, through
your
[copilot](/docs/ready-made-features/ai-copilots/copilots#Adding-back-end-knowledge).
You can submit PDF/image files, web pages, or entire websites which will be
crawled and indexed. This is most useful for passing large amounts of
information, for example:

- **Knowledge bases**: Documentation, FAQs, and support tickets.
- **Domain-specific data**: Detailed information that your AI must understand.
- **Documents**: Submit PDF or image scans of reports, contracts, and invoices.

### How it works [#back-end-knowledge-how-it-works]

Back-end knowledge uses Retrieval-Augmented Generation (RAG) search, and is
triggered by a hidden tool call, meaning the AI will search through its back-end
knowledge when it feels its relevant. You can define
[when AI should use back-end knowledge](/docs/ready-made-features/ai-copilots/copilots#When-should-AI-use-knowledge)
in your copilot settings, and AI will understand what should trigger a back-end
knowledge query.

The AI will rewrite your query, and may even run multiple queries in a row, each
of which will be displayed in the
[`AiChat`](/docs/api-reference/liveblocks-react-ui#AiChat) UI as a search.

### Adding back-end knowledge through the dashboard

After
[creating a copilot](/docs/ready-made-features/ai-copilots/copilots#Creating-a-copilot)
in the [dashboard](/dashboard), you can navigate to the “Knowledge” tab to add
back-end knowledge. Select which type of knowledge you’d like to submit, and
enter your URLs or upload your files.

<Figure>
  <Image
    src="/assets/ai-copilots/copilot-knowledge.png"
    alt="Screenshot of the Liveblocks dashboard, adding knowledge to a copilot"
    width={720}
    height={409}
    quality={100}
  />
</Figure>

Make sure to use your copilot ID in
[`AiChat`](/docs/api-reference/liveblocks-react-ui#AiChat) and
[`useSendAiMessage`](/docs/api-reference/liveblocks-react#useSendAiMessage) to
use the knowledge in your application.

### Adding back-end knowledge programmatically

You aren’t limited to modifying knowledge through the dashboard—you can also add
copilot knowledge programmatically, allowing users or teams in your app to have
their own individual knowledge bases.

```ts
import { Liveblocks } from "@liveblocks/node";

const liveblocks = new Liveblocks({
  secret: "{{SECRET_KEY}}",
});

// +++
const { id } = await liveblocks.createWebKnowledgeSource({
  copilotId: "co_abc123...",
  url: "https://example.com",
  type: "crawl",
});
// +++
```

This is made possible using the
[Liveblocks Node.js client](/docs/api-reference/liveblocks-node#AI-Copilots) and
[REST API](/docs/api-reference/rest-api-endpoints#AI), where a number of APIs
are available for managing copilots and knowledge sources.

- [`createWebKnowledgeSource`](/docs/api-reference/liveblocks-node#create-web-knowledge-source)
- [`createFileKnowledgeSource`](/docs/api-reference/liveblocks-node#create-file-knowledge-source)
- [`getKnowledgeSources`](/docs/api-reference/liveblocks-node#get-knowledge-sources)
- [`getKnowledgeSource`](/docs/api-reference/liveblocks-node#get-knowledge-source)
- [`getFileKnowledgeSourceMarkdown`](/docs/api-reference/liveblocks-node#get-file-knowledge-source-markdown)
- [`getWebKnowledgeSourceLinks`](/docs/api-reference/liveblocks-node#get-web-knowledge-source-links)
- [`deleteWebKnowledgeSource`](/docs/api-reference/liveblocks-node#delete-web-knowledge-source)
- [`deleteFileKnowledgeSource`](/docs/api-reference/liveblocks-node#delete-file-knowledge-source)

## Web search

Web search is a feature that allows your AI to search the internet for
information. It is disabled by default, and can be enabled by toggling the
`webSearch` option in
[your copilot settings](/docs/ready-made-features/ai-copilots/copilots#Web-search).

```text title="Web search"
on
```

You can also
[limit the domains that the AI can search](/docs/ready-made-features/ai-copilots/copilots#Limit-web-search-to-specific-domains)
by setting an option on your copilot.

```text title="Limit web search to specific domains"
encyclopedia.com
en.wikipedia.org
```

It’s also possible to enable web search through the back-end with
`allowedDomains`, for example using
[`liveblocks.createAiCopilot`](/docs/api-reference/liveblocks-node#create-ai-copilot).

```ts
const copilot = await liveblocks.createAiCopilot({
  name: "My AI Assistant",
  systemPrompt: "You are a helpful AI assistant for our team.",
  provider: "openai",
  providerModel: "gpt-4",
  providerApiKey: "sk-...",
  providerOptions: {
    openai: {
      // Optional, restrict web search to specific domains
      // +++
      webSearch: {
        allowedDomains: ["encyclopedia.com", "en.wikipedia.org"],
      },
      // +++
    },
  },
});
```

Not all models support web search, but most OpenAI and Anthropic models do.

---
meta:
  title: "Styling and customization"
  parentTitle: "AI Copilots"
  description: "Customize the appearance and behavior of AI Copilots"
---

Styling
[default components](/docs/ready-made-features/ai-copilots/default-components)
is enabled through a range of means, such as CSS variables, class names, and
more. It’s also possible to use [overrides](#Overrides-and-localization) to
modify any strings used in the default components, which is especially helpful
for localization.

## Default components

To add the default components’ theme, import the
[default styles](/docs/api-reference/liveblocks-react-ui#Default-styles) CSS
file.

```ts
import "@liveblocks/react-ui/styles.css";
```

You can also import one of two CSS files to enable
[dark mode](/docs/api-reference/liveblocks-react-ui#Dark-mode), depending on how
you’d like to enable it.

```ts
// Dark mode using the system theme with `prefers-color-scheme`
import "@liveblocks/react-ui/styles/dark/media-query.css";
```

```ts
// Dark mode using `className="dark"`, `data-theme="dark"`, or `data-dark="true"`
import "@liveblocks/react-ui/styles/dark/attributes.css";
```

### CSS variables

A number of
[CSS variables](/docs/api-reference/liveblocks-react-ui#CSS-variables) can be
used to customize colors, spacing, and more. This is our recommended path for
styling the default components, as you can quickly and easily modify all
components with just a few variables.

```css
/* Styles all default components */
.lb-root {
  --lb-accent: purple;
  --lb-spacing: 1em;
  --lb-radius: 0;
}
```

### Class names

Should you need deeper customization,
[class names](/docs/api-reference/liveblocks-react-ui#Class-names) can be
styled, some of which provide contextual data attributes.

```css
.lb-ai-chat {
  /* Customize AI chat */
}

.lb-ai-composer {
  /* Customize message composer */
}
```

### Overrides and localization

It’s possible to
[override strings](/docs/api-reference/liveblocks-react-ui#Overrides) used in
the default components, which has a couple of different uses, the first being
localization. In this example, we’re globally setting the AI composer’s
placeholder from “Ask anything…” to “Posez une question” for French users.

```tsx
import { LiveblocksUiConfig } from "@liveblocks/react-ui";

export function App() {
  return (
    <LiveblocksUiConfig
      overrides={{
        // +++
        locale: "fr",
        AI_COMPOSER_PLACEHOLDER: "Posez une question",
        // +++
      }}
    >
      {/* ... */}
    </LiveblocksUiConfig>
  );
}
```

You can also override strings on a component basis, for example if you’d like to
change the “Send” button tooltip to “Post message” in the chat’s message
composer.

```tsx
import { Composer } from "@liveblocks/react-ui";

function Component() {
  return (
    <AiChat
      chatId="my-chat-id"
      copilotId="co_a7Gd3x..."
      overrides={{
        // +++
        AI_COMPOSER_PLACEHOLDER: "Post message",
        // +++
      }}
    />
  );
}
```

{/* prettier-ignore */}
{/* TODO add back later when we enable it

## Custom interfaces

You can bypass the default components and build completely custom UI for your AI
copilots, for example using
[`useAiChatMessages`](/docs/api-reference/liveblocks-react#useAiChatMessages)
and [`useSendAiMessage`](/docs/api-reference/liveblocks-react#useSendAiMessage)
to build your own chat history and composer.

```tsx
import {
  useAiChatMessages,
  useSendAiMessage,
} from "@liveblocks/react/suspense";

function ChatMessages({ chatId }: { chatId: string }) {
  // +++
  const { messages } = useAiChatMessages(chatId);
  const sendAiMessage = useSendAiMessage(chatId, { copilotId: "co_a7Gd3x..." });
  // +++

  return (
    <div>
      // +++
      {messages.map((message) => {
        // +++
        if (message.role === "user") {
          return (
            <div key={message.id}>
              {message.content.map((part) => (
                <p>👤 You: {part.text}</p>
              ))}
            </div>
          );
        }

        return (
          <div key={message.id}>
            {(message.contentSoFar ?? message.content).map((part) => (
              <p>🤖 Assistant: {part.text}</p>
            ))}
          </div>
        );
      })}
      <form
        onSubmit={(e) => {
          e.preventDefault();
          // +++
          sendAiMessage({ text: e.target.message.value });
          // +++
          e.target.reset();
        }}
      >
        <input type="text" name="message" />
        <button type="submit">Send</button>
      </form>
    </div>
  );
}
```

Learn more about what you can build under
[hooks](/docs/ready-made-features/ai-copilots/hooks).

\*/}

---
meta:
  title: "Tools"
  parentTitle: "AI Copilots"
  description: "Allow AI to interact with your application"
---

Tools allow AI to make actions, modify your application state, interact with
your front-end, and render custom components within your AI chat. Use tools to
extend the capabilities of AI Copilots beyond simple text, allowing autonomous
and human-in-the-loop interactions.

## Tool use cases

Tools can be used to create various different interactions inside of your AI
chat, such as:

- **Actions**: Autonomously perform actions like editing documents, redirecting
  users, sending emails.
- **Custom components**: Render custom React components like forms, graphs,
  videos, callouts.
- **Query actions**: AI can query your app, search documents, find pages, check
  invoices.
- **Human-in-the-loop actions**: Show confirm/deny buttons before taking
  destructive actions.
- **AI presence**: Tool results can be streamed in, allowing AI to show live
  updates in your app.

### How tools work

You can define a list of tools in your application, and your AI can choose to
use them whenever it decides they’re needed. Within each tool you can set
certain parameters which AI will fill in for you. For example, a weather tool
may have a `location` parameter, and AI may enter `"Paris"` as the value. Here’s
an example of a tool call interaction:

<Steps>
  <StepCompact>
    <StepTitle>In your weather tool, `location` is defined as a `string`</StepTitle>
    <StepContent>
    ```json
    { "location": { type: "string" } }
    ```
    </StepContent>
  </StepCompact>

{" "}

<StepCompact>
  <StepTitle>User asks about the weather in Paris</StepTitle>
  <StepContent>

    ```js
    User: "What's the weather in Paris?"
    ```

  </StepContent>
</StepCompact>

  <StepCompact>
    <StepTitle>AI calls the weather tool with `Paris` as the `location`</StepTitle>
    <StepContent>
    ```js
    { "location": "Paris" }
    ```
    </StepContent>
  </StepCompact>

{" "}

<StepCompact>
  <StepTitle>You write code to fetch the weather for the `location`</StepTitle>
  <StepContent>

    ```js
    execute: async ({ location }) => {
      // { "temperature": 20, "condition": "sunny" };
      const weather = await __fetchWeather__(location);
      return { data: { weather }};
    }
    ```

  </StepContent>
</StepCompact>

  <StepCompact lastStep>
    <StepTitle>AI answers the user</StepTitle>
    <StepContent>
    ```js
    AI: "It's sunny in Paris, with a temperature of 20°C."
    ```
    </StepContent>
  </StepCompact>
</Steps>

When writing your
[system prompt](/docs/ready-made-features/ai-copilots/copilots#System-prompt)
you can suggest when certain tools should be used, helping AI respond as you
like. This is just an example of a simple tool, but below we’ll detail how to
create more complex tools that have confirm/deny dialogs, render custom
components, query data, and more.

## Defining tools

You can define a tool with
[`defineAiTool`](/docs/api-reference/liveblocks-client#defineAiTool) and
[`RegisterAiTool`](/docs/api-reference/liveblocks-react#RegisterAiTool). First,
you first need to give your tool a unique name, and a description, which helps
AI understand when to call it. You can place the component anywhere in your app.

```tsx
import { RegisterAiTool } from "@liveblocks/react";
import { defineAiTool } from "@liveblocks/client";
import { AiChat } from "@liveblocks/react-ui";

function Chat() {
  return (
    <>
      <AiChat chatId="my-chat-id" />
      // +++
      <RegisterAiTool
        name="weather-tool"
        tool={defineAiTool()({
          description: "Get the weather for a location",
          // ...
        })}
      />
      // +++
    </>
  );
}
```

For AI to use your tools intelligently, parameters must be defined, which AI
will fill in for you. Tools use
[JSON schema](/docs/ready-made-features/ai-copilots/tools#Advanced-JSON-schema)
to define these. For example, you can define `location` parameter as a `string`.

```tsx
<RegisterAiTool
  name="weather-tool"
  tool={defineAiTool()({
    description: "Get the weather for a location",
    // +++
    parameters: {
      type: "object",
      properties: {
        location: { type: "string" },
      },
      required: ["location"],
      additionalProperties: false,
    },
    // +++
    // ...
  })}
/>
```

To add functionality to your tool, a combination of `execute` and `render`
functions are used.

```tsx
<RegisterAiTool
  name="weather-tool"
  tool={defineAiTool()({
    description: "Get the weather for a location",
    parameters: {
      type: "object",
      properties: {
        location: { type: "string" },
      },
      required: ["location"],
      additionalProperties: false,
    },
    // +++
    execute: async ({ location }) => {
      // ...
    },
    render: ({ stage, partialArgs, args, result, respond }) => {
      // ...
    },
    // +++
  })}
/>
```

In each of the following sections, different ways to implement `execute` and
`render` are detailed.

## Actions

If you’d like your AI to perform an action when the tool is called, you can use
`execute` to define what should happen. The arguments passed to `execute` are
the parameters defined in your tool, filled in by AI. After the tool has run,
return any `data` you’d like to pass back to AI.

```tsx
<RegisterAiTool
  name="weather-tool"
  tool={defineAiTool()({
    description: "Get the weather for a location",
    parameters: {
      type: "object",
      properties: {
        location: { type: "string" },
      },
      required: ["location"],
      additionalProperties: false,
    },
    // +++
    execute: async ({ location }) => {
      const weather = await __fetchWeather__(location);
      return { data: { weather } };
    },
    // +++
  })}
/>
```

After running `execute`, AI will read the `data` object, and choose how to
respond. Additionally, you can define a `description` to pass back to AI. This
is a way to inform AI what has just taken place, so it can understand the
context of the result, and what it should do next. This text will never be shown
to the user.

```ts
execute: async ({ location }) => {
  const weather = await __fetchWeather__(location);
  return {
    data: { weather },
    // +++
    description: "You've just fetched the weather, share the temperature in °C."
    // +++
  };
},
```

<Banner title="Actions example">

The [AI Calendar](/examples/ai-calendar/nextjs-ai-calendar) example contains an
action that allows AI to create new calendar events.

</Banner>

### Display a loading message

You can easily display a loading message while an action takes place using
`render` and [`AiTool`](/docs/api-reference/liveblocks-react-ui#AiTool). You can
also choose to display a message after the action has finished, as in the
example below.

```tsx
import { AiChat, AiTool } from "@liveblocks/react-ui";
import { RegisterAiTool } from "@liveblocks/react";

function Chat() {
  <>
    <AiChat chatId="my-chat-id" />
    <RegisterAiTool
      name="weather-tool"
      tool={defineAiTool()({
        description: "Get the weather for a location",
        parameters: {
          type: "object",
          properties: {
            location: { type: "string" },
          },
          required: ["location"],
          additionalProperties: false,
        },
        execute: async ({ location }) => {
          const weather = await __fetchWeather__(location);
          return {
            data: { weather },
            description: "You've just fetched the weather.",
          };
        },
        // +++
        render: ({ stage }) => {
          // `execute` is still running
          if (stage !== "executed") {
            return <AiTool title="Fetching weather…" variant="minimal" />;
          }

          // `execute` has finished
          return <AiTool title="Weather fetched" variant="minimal" />;
        },
        // +++
      })}
    />
  </>;
}
```

[`AiTool`](/docs/api-reference/liveblocks-react-ui#AiTool) isn’t required here,
as you can return any JSX, but it’s an easy way to match the styling of the
default chat. Returning `null` will display nothing.

### Combine actions with front-end knowledge

You can combine actions with
[front-end knowledge](/docs/ready-made-features/ai-copilots/knowledge) to create
an AI assistant that can take actions. For example, say you have a document on
the current page. You can use knowledge to pass the document’s text to the AI,
then create a tool that allows AI to edit the document.

```tsx
import { RegisterAiKnowledge } from "@liveblocks/react";
import { AiChat } from "@liveblocks/react-ui";
import { RegisterAiTool } from "@liveblocks/react";
import { defineAiTool } from "@liveblocks/client";
import { useState } from "react";

function Document() {
  // +++
  const [document, setDocument] = useState("Hello world");
  // +++

  return (
    <>
      <AiChat chatId="my-chat-id" />
      <RegisterAiKnowledge
        description="The document's text"
        // +++
        value={document}
        // +++
      />
      <RegisterAiTool
        name="edit-document"
        tool={defineAiTool()({
          description: "Edit the document's text",
          parameters: {
            type: "object",
            properties: {
              text: { type: "string" },
            },
          },
          execute: ({ args }) => {
            // +++
            setDocument(args.text);
            // +++
            return { data: {}, description: "Document updated" };
          },
          render: ({ stage }) => {
            if (stage !== "executed") {
              return <AiTool title="Updating document…" variant="minimal" />;
            }

            return <AiTool title="Document updated" variant="minimal" />;
          },
        })}
      />
    </>
  );
}
```

## Custom components

You can use tools to display custom components inside the chat with the `render`
function. These don’t have to be simple components, but can be complex, like
forms, graphs, videos, callouts. When displaying a simple component, include an
`execute` function, even if it’s empty, otherwise the chat will assume it’s a
[human-in-the-loop action](#Human-in-the-loop-actions).

```tsx
<RegisterAiTool
  name="graph-tool"
  tool={defineAiTool()({
    description: "Display a graph in the chat",
    parameters: {},
    execute: () => {},
    // +++
    render: () => {
      return <MyGraph x={50} y={100} />;
    },
    // +++
  })}
/>
```

You can go further than this, and allow AI to
[create parameters](#Advanced-JSON-schema) which you can use in your custom
component, for example `x` and `y` values on a graph.

```tsx
<RegisterAiTool
  name="graph-tool"
  tool={defineAiTool()({
    description: "Display a graph in the chat",
    parameters: {
      type: "object",
      properties: {
        // +++
        x: { type: "number" },
        y: { type: "number" },
        // +++
      },
      required: ["x", "y"],
      additionalProperties: false,
    },
    execute: () => {},
    render: ({ args, stage }) => {
      if (stage !== "executed") {
        return <div>Loading...</div>;
      }

      // +++
      return <MyGraph x={args.x} y={args.y} />;
      // +++
    },
  })}
/>
```

AI will most likely write a response after using your tool, but you can prompt
the AI to not respond by adding a `description` to `execute`.

```tsx
<RegisterAiTool
  name="graph-tool"
  tool={defineAiTool()({
    description: "Display a graph in the chat",
    parameters: {
      type: "object",
      properties: {
        x: { type: "number" },
        y: { type: "number" },
      },
      required: ["x", "y"],
      additionalProperties: false,
    },
    execute: () => {
      return {
        data: {},
        // +++
        description: "You’re displaying a graph. Do not respond further.",
        // +++
      };
    },
    render: ({ args, stage }) => {
      if (stage !== "executed") {
        return <div>Loading...</div>;
      }

      // +++
      return <MyGraph x={args.x} y={args.y} />;
      // +++
    },
  })}
/>
```

<Banner title="Custom components example">

The [AI Support Chat](/examples/ai-support/nextjs-ai-support) example uses
custom components in its support ticket tool to display a contact form.

</Banner>

### Fetching data for custom components

You can take custom components a step further by combining them with
[actions](#Actions), and then showing the results inside the custom component.
The `result` property contains the data returned from the action.

```tsx
<RegisterAiTool
  name="weather-tool"
  tool={defineAiTool()({
    description: "Get the weather for a location",
    parameters: {
      type: "object",
      properties: {
        location: { type: "string" },
      },
      required: ["location"],
      additionalProperties: false,
    },
    execute: async ({ location }) => {
      // { "temperature": 20, "condition": "sunny" };
      const weather = await __fetchWeather__(location);
      return { data: { weather } };
    },
    // +++
    render: ({ stage, args, result }) => {
      if (stage !== "executed") {
        return <div>Fetching weather…</div>;
      }

      return (
        <MyWeatherComponent
          location={args.location}
          condition={result.data.weather.condition}
          temperature={result.data.weather.temperature}
        />
      );
    },
    // +++
  })}
/>
```

`args` contains the arguments passed to the tool from AI, and `result` contains
the data returned from the tool.

## Query actions

A helpful way to use tools is to allow AI to query data from your application,
such as documents, pages, or other data sources. If your application already
contains a search function, you can easily plug it into your tool to create a
powerful AI assistant. For example, this tool can search through documents by
title, folder, and category.

```tsx
<RegisterAiTool
  name="find-documents"
  tool={defineAiTool()({
    // +++
    description: "Find documents by title, folder, and category",
    // +++
    parameters: {
      type: "object",
      properties: {
        // +++
        title: { type: "string" },
        folder: { type: "string" },
        category: { type: "string" },
        // +++
      },
      required: ["title", "folder", "category"],
    },
    additionalProperties: false,
  },
  // +++
  execute: async ({ title, folder, category }) => {
    const documents = await __queryDocuments__({ title, folder, category });
    return {
      data: { documents },
      description: documents.length > 0 ? `${documents.length} results` : "No results"
    };
  },
  // +++
  render: ({ stage, args, result }) => {
    if (stage !== "executed") {
      return <AiTool title="Fetching documents…" variant="minimal" />;
    }

    return null;
  },
  })}
/>
```

Since the query is happening on the front-end, you don’t need to implement
separate authentication for your AI tool—it can leverage the same APIs that your
users are already authorized to access.

<Banner title="Query actions example">

The
[AI Reports Dashboard](/examples/ai-dashboard-reports/nextjs-ai-dashboard-reports)
example has a query transactions tool allowing for complex searching.

</Banner>

## Human-in-the-loop actions

Human-in-the-loop actions allow the user to confirm or deny an action before
it’s executed. This is particularly useful when it comes to destructive and
stateful actions, such as deleting a document, or sending an email. Confirmable
actions like these will freeze the chat until the user responds, either by
confirming or cancelling the action.

<Steps>
<StepCompact>
  <StepTitle>User asks to delete a document</StepTitle>
  <StepContent>

    ```js
    User: "Can you delete my-document.txt?"
    ```

  </StepContent>
</StepCompact>
  <StepCompact>
    <StepTitle>AI calls the delete document tool, “Confirm” and “deny” buttons are displayed</StepTitle>
    <StepContent>
    _The chat waits for the user to respond._
    </StepContent>
  </StepCompact>
  <StepCompact>
    <StepTitle>The user clicks “Confirm”</StepTitle>
    <StepContent>
    _The document is deleted._
    </StepContent>
  </StepCompact>
<StepCompact lastStep>
  <StepTitle>The chat unfreezes and is ready to continue</StepTitle>
  <StepContent>

    ```js
    AI: "I've deleted it! How else can I help you?"
    ```

  </StepContent>
</StepCompact>
</Steps>

To create confirmable actions, you must skip using the `execute` function, and
instead move your logic to `render`, as we detail below. This will always freeze
the chat until the user responds.

<Banner title="Human-in-the-loop actions example">

The
[AI Reports Dashboard](/examples/ai-dashboard-reports/nextjs-ai-dashboard-reports)
example demonstrates human-in-the-loop actions with its invite member tool.

</Banner>

### Default confirmation component

The easiest way to create confirmable actions, is to return the ready-made
[`AiTool.Confirmation`](/docs/api-reference/liveblocks-react-ui#AiTool.Confirmation)
component in `render`. The `confirm` and `cancel` callbacks work very similarly
to `execute`, and are triggered when the user clicks “Confirm” or “Cancel”.

```tsx
<RegisterAiTool
  name="delete-document"
  tool={defineAiTool()({
    description: "Delete a document by its ID",
    parameters: {
      type: "object",
      properties: {
        documentId: { type: "string" },
      },
      required: ["documentId"],
      additionalProperties: false,
    },
    render: ({ stage, args, result, types }) => {
      return (
        <AiTool title="Delete document" variant="minimal">
          // +++
          <AiTool.Confirmation
            types={types}
            confirm={async ({ documentId }) => {
              await __deleteDocument__(documentId);
              return {
                data: { documentId },
                description: "The user chose to delete the document",
              };
            }}
            cancel={() => {
              return {
                data: { documentId },
                description: "The user cancelled deleting the document",
              };
            }}
          />
          // +++
        </AiTool>
      );
    },
  })}
/>
```

In the `cancel` callback it’s important to let the AI know that the user
cancelled the action, otherwise it may assume the action failed and try to run
it again.

```js
description: "The user cancelled deleting the document",
```

### Building a custom confirmation component

By utilizing the `respond` argument in `render`, and the different stages of a
tool’s lifecycle, you can build a fully custom confirmation component. These are
the different stages:

1. `receiving` - Displayed when AI is streaming in the parameters.
2. `executing` - Displayed when the chat is frozen, and is waiting for a
   response.
3. `executed` - Displayed after a response has been recorded.

Here’s how to leverage the different stages to create a custom “send email”
tool—note how `respond` is used similarly to `execute`.

```tsx
<RegisterAiTool
  name="send-email"
  tool={defineAiTool()({
    description: "Send an email",
    parameters: {
      type: "object",
      properties: {
        emailAddress: { type: "string" },
      },
      required: ["emailAddress"],
      additionalProperties: false,
    },
    render: ({ stage, args, respond, result }) => {
      // `emailAddress` param is still streaming in, wait
      // +++
      if (stage === "receiving") {
        return <div>Loading...</div>;
      }
      // +++

      // The tool is waiting for `respond` to be called
      // +++
      if (stage === "executing") {
        return (
          <form
            onSubmit={async (e) => {
              e.preventDefault();
              const message = e.target.message.value;
              await __sendEmail__(args.emailAddress, message);

              // Similar to `execute`/`confirm`, let AI know it succeeded
              respond({
                data: { emailAddress: args.emailAddress, message },
                description: "You sent an email for the user",
              });
            }}
          >
            <textarea name="message" />
            <button type="submit">Send</button>
            <button
              type="button"
              onClick={() =>
                // Similar to `execute`/`cancel`, let AI know the user cancelled
                respond({
                  data: { emailAddress: args.emailAddress, message },
                  description: "The user cancelled sending an email",
                })
              }
            >
              Cancel
            </button>
          </form>
        );
      }
      // +++

      // `respond` has already been called, show the result
      // +++
      return (
        <div>
          You sent an email to {args.emailAddress}: "{result.data.message}".
        </div>
      );
      // +++
    },
  })}
/>
```

A tool’s stages never reset, which means that once an email has been sent, the
tool will remain in the `executed` stage showing “You sent an email to…”, even
after refreshing the page. This enables stateful interaction.

## AI presence & streaming

You can stream in tool results as they arrive, allowing you to show live updates
and AI presence in your application. An example would be a tool that generates
documents—you can display the partially generated document as each chunk
arrives, for example:

<Steps>

<StepCompact>
```json
{ "content": "Here’s" }
```
</StepCompact>

<StepCompact>
```json
{ "content": "Here’s my suggestions" }
```
</StepCompact>

<StepCompact>
```json
{ "content": "Here’s my suggestions for a" }
```
</StepCompact>

<StepCompact lastStep>
```json
{ "content": "Here’s my suggestions for a marketing email!" }
```
</StepCompact>

</Steps>

To show this inside the tool, you can use `render`. While you’d normally access
AI-generated arguments via `args`, but to access streaming results, you can use
`partialArgs` to get the whole stream up to this point. This all occurs during
the `receiving` stage, and as each chunk is received, `render` will re-render
and update the UI.

```tsx
<RegisterAiTool
  name="create-document"
  tool={defineAiTool()({
    description: "Create a document",
    parameters: {
      type: "object",
      properties: {
        content: { type: "string" },
      },
      required: ["content"],
      additionalProperties: false,
    },
    execute: () => {},
    render: ({ stage, partialArgs, args }) => {
      // Document is streaming in
      // +++
      if (stage === "receiving") {
        return <div>Document: {partialArgs.content}</div>;
      }
      // +++

      // Document has fully streamed in
      // +++
      return <div>Document: {args.content}</div>;
      // +++
    },
  })}
/>
```

[`RegisterAiTool`](/docs/api-reference/liveblocks-react#RegisterAiTool) allows
you to stream in strings, objects, and arrays, and you can use them in `render`
as they arrive.

<Banner title="AI presence & streaming example">

The [AI App Builder](/examples/ai-app-builder/nextjs-ai-app-builder) example
demonstrates AI presence and streaming in a code editor.

</Banner>

### Streaming into a document with AI presence

To stream results outside of the chat window, and show AI presence, you can call
functions inside `render`. In this case, we’re updating a document outside of
the chat window, and inside the chat, we’re showing a simple
[`AiTool`](/docs/api-reference/liveblocks-react-ui#AiTool) message.

```tsx
<RegisterAiTool
  name="create-document"
  tool={defineAiTool()({
    description: "Create a document",
    parameters: {
      type: "object",
      properties: {
        content: { type: "string" },
      },
      required: ["content"],
      additionalProperties: false,
    },
    execute: () => {},
    render: ({ stage, partialArgs }) => {
      // Document is streaming in, update document and presence
      // +++
      if (stage === "receiving") {
        __updateDocument__(partialArgs.content);
        __renderPresenceAtPosition__(partialArgs.content.length);
        return <AiTool title="Creating document…" />;
      }
      // +++

      // Final chunk of stream is here, update document and end presence
      // +++
      if (stage === "executing") {
        __updateDocument__(args.content);
        __renderPresenceAtPosition__(null);
        return <AiTool title="Creating document…" />;
      }
      // +++

      // Document has fully streamed in
      // +++
      return <AiTool title="Document complete!" />;
      // +++
    },
  })}
/>
```

## Advanced JSON schema

Up to this point, we’ve only covered generating objects and strings with AI, but
[JSON schema](https://json-schema.org/learn/getting-started-step-by-step) allows
for more complex data types, such as numbers, arrays, enums, as well as
constraints, and hints for AI.

<Banner type="warning" title="Model compatibility">
  Some AI models don’t allow certain JSON schema types, but we’ve found these
  types work with most models. Many models enforce using the `required` array,
  and setting `additionalProperties: "false"`, which is why we recommend this.
</Banner>

```js
parameters: {
  type: "object",

  properties: {
    // String
    name: { type: "string" },

    // Number
    age: { type: "number" },

    // Boolean
    sendEmail: { type: "boolean" },

    // Union
    count: { type: ["string", "number", "null"] },

    // Array
    pets: { type: "array", items: { type: "string" } },

    // Description, helps AI understand the type
    job: { type: "string", description: "A job title, 1-2 words" },

    // String constraints
    fullName: {
      type: "string",
      minLength: 1,
      maxLength: 100,
      pattern: "^[A-Z][a-zA-Z'-]+(?: [A-Z][a-zA-Z'-]+)+$",
    },

    // Number constraints
    ageInYears: { type: "number", minimum: 18, maximum: 100 },

    // Combination of all
    people: {
      type: "array",
      description: "A list of all people",
      items: {
        type: "object",
        properties: {
          sendEmail: { type: "boolean", description: "Notify them by?" },
          name: { type: "string", description: "First and last names" },
          age: { type: "number", minimum: 18, description: "Age in years" },
          job: { type: ["string", "null"], description: "`null` for unemployed" },
        }
      },
    },
  },

  // Recommended to always set the following, passing all properties to `required`
  additionalProperties: false,
  required: ["name", "age", "sendEmail", "job", "fullName", "ageInYears", "pets", "people"],
}
```

## Updating tools

After a tool has been used in the chat, its AI-filled parameters are permanently
set. This leaves us with a problem—if we remove or change the tool’s parameters,
old versions of the tool will display an empty space in the chat (or in
development mode, an error box).

```diff
// ❌ Will render `null` in the chat instead of a tool
‎<RegisterAiTool
  name="send-email"
  tool={defineAiTool()({
    description: "Send an email",
    parameters: {
      type: "object",
      properties: {
-       email: { type: "string" },
+       emailAddress: { type: "string" },
      },
-     required: ["email"],
+     required: ["emailAddress"],
      additionalProperties: false,
    },
  })}
  // ...
/>
```

Care needs to be taken when creating a new version of each tool if you wish for
old chats to display fully working components. For this reason, we recommend
versioning your tools, and disabling the old tool until you’re sure the old
tools aren’t needed anymore. This will ensure the tool won’t be used in new
chats, but the old component will still render correctly.

```tsx
import { RegisterAiTool } from "@liveblocks/react";
import { defineAiTool } from "@liveblocks/client";
import { AiChat } from "@liveblocks/react-ui";

function Chat() {
  return (
    <>
      <AiChat chatId="my-chat-id" />
      <RegisterAiTool
        name="send-email"
        // +++
        // Disabling the old tool so it won't be used in new chats
        enabled={false}
        // +++
        tool={defineAiTool()({
          description: "Send an email",
          parameters: {
            type: "object",
            properties: {
              // +++
              email: { type: "string" },
              // +++
            },
            required: ["email"],
            additionalProperties: false,
          },
        })}
        // ...
      />
      <RegisterAiTool
        // +++
        // Version 2, which will be used in new chats
        name="send-email-v2"
        // +++
        tool={defineAiTool()({
          description: "Send an email",
          parameters: {
            type: "object",
            properties: {
              // +++
              emailAddress: { type: "string" },
              // +++
            },
            required: ["emailAddress"],
            additionalProperties: false,
          },
        })}
        // ...
      />
    </>
  );
}
```

---
meta:
  title: "Troubleshooting"
  parentTitle: "AI Copilots"
  description: "Troubleshoot common issues"
---

AI models are often unreliable and tricky to debug. Here are some common issues
you may encounter, and how to troubleshoot them.

## Common issues

Here’s a list of common issues you may encounter, and how to fix them.

### The AI provider is currently overloaded or unavailable

This means that the AI provider you’re using is currently not available. You can
check the status of your provider by visiting their status pages:
[OpenAI](https://status.openai.com/), [Anthropic](https://status.claude.com/),
[Google](https://aistudio.google.com/status). To avoid this issue, you can
[set up fallback models](/docs/guides/how-to-use-fallback-ai-models-in-ai-copilots)
that use other providers.

### The AI provider's rate limit or quota exceeded

This means that you’ve hit token limits defined by your AI provider (e.g.
OpenAI, Anthropic, Google). You can reach out to your provider to increase your
limits, use a model with lower limits, or consider
[setting up fallback models](/docs/guides/how-to-use-fallback-ai-models-in-ai-copilots)
for busy times.

### For this AI provider, 'additionalProperties: false' is required to be set in the properties [#additionalproperties-false-is-required]

This means that your provider requires the `additionalProperties` field to be
set to `false` in tool `parameters`. Fix this by adding the following line to
your tool definition:

```tsx
<RegisterAiTool
  name="weather-tool"
  tool={defineAiTool()({
    description: "Get the weather for a location",
    parameters: {
      type: "object",
      properties: {
        location: { type: "string" },
      },
      required: ["location"],
      // +++
      additionalProperties: false,
      // +++
    },
    // ...
  })}
/>
```

### A model is ignoring my back-end knowledge

If an AI model doesn’t seem to be using your
[back-end knowledge](/docs/ready-made-features/ai-copilots/knowledge#Back-end-knowledge)
correctly, there are a few things you can check:

#### Check you’re using the correct copilot ID in all APIs

Check you’re using the copilot ID throughout your application, specifically in
every use of [`AiChat`](/docs/api-reference/liveblocks-react-ui#AiChat) and
[`useSendAiMessage`](/docs/api-reference/liveblocks-react#useSendAiMessage).

```tsx
import { AiChat } from "@liveblocks/react-ui";

function Chat() {
  return (
    <AiChat
      chatId="my-chat-id"
      // +++
      copilotId="co_tUYtNctLAtUIAAIZBc1Zk"
      // +++
    />
  );
}
```

```tsx
import { useSendAiMessage } from "@liveblocks/react";

function SendMessage() {
  const sendAiMessage = useSendAiMessage("my-chat-id", {
    // +++
    copilotId: "co_tUYtNctLAtUIAAIZBc1Zk",
    // +++
  });

  return (
    <button onClick={() => sendAiMessage("What's new?")}>What's new?</button>
  );
}
```

#### Make sure you’re letting AI know when to use knowledge

Make sure you’re defining
[when AI should use knowledge](/docs/ready-made-features/ai-copilots/copilots#When-should-AI-use-knowledge)
in your copilot settings. When defining copilots from the server, this is the
`knowledgePrompt` field. If it doesn’t seem to be working, try experimenting
with your prompt to be more specific, authoritative, and use examples.

```diff title="When should AI use knowledge?"
- When the user asks a question about code.
+ It's required to do this whenever the user mentions React code, e.g. `useOthers`.
```

It’s worth checking if there’s an official guide for your specific model, which
details how to talk to it. For example the
[official prompting guide for GPT-4.1](https://cookbook.openai.com/examples/gpt4-1_prompting_guide).

### The AI provider response exceeded the maximum token limit

This means that the response from the AI provider exceeded the maximum token
limit.

This error is usually caused:

- When the model’s generated output would exceed the `max tokens` you set in the
  dashboard for a copilot.
- When the request would exceed the model/provider’s token limits (e.g.,
  combined prompt + expected completion), leading the provider to return a
  token-limit error

Check first in your dashboard if the max tokens limit is set correctly.

## Error table

Every error you may encounter is listed in the table below, along with a
suggestion.

| Error                                                                                        | Info                                                                                                                                                                                              |
| -------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| The AI provider is currently overloaded or unavailable                                       | Your provider is down, check its status. [Learn more](#The-AI-provider-is-currently-overloaded-or-unavailable)                                                                                    |
| The AI provider's rate limit or quota exceeded                                               | You’ve breached the rate limit or quota for your model. [Learn more](#The-AI-provider's-rate-limit-or-quota-exceeded)                                                                             |
| A technical error happened while invoking the AI provider.                                   | A internal technical error happened. [Contact support](https://liveblocks.io/contact/support) if the problem persists.                                                                            |
| For this AI provider, 'additionalProperties: false' is required to be set in the properties` | You need to add `additionalProperties: false` to your tool `parameters` object. [Learn more](#additionalproperties-false-is-required)                                                             |
| Error from AI provider: ...                                                                  | Generic error received from the AI Provider. [Contact support](https://liveblocks.io/contact/support) if the problem persists.                                                                    |
| The AI copilot’s model does not exist or you have insufficient access to it                  | Most likely the wrong `copilotId` is set. Potentially the current user has no permissions to view it.                                                                                             |
| Invalid credentials or insufficient permissions                                              | The API key used in the copilot is invalid or it not has the required permissions to use the model defined in the copilot.                                                                        |
| The AI copilot could not make the request because it is too large                            | Usually the token limit for the model to process is exceeded.                                                                                                                                     |
| The AI provider blocked the request due to a content filter                                  | The request was blocked by the AI provider own content filter.                                                                                                                                    |
| The AI provider response exceeded the maximum token limit                                    | The response from the AI provider exceeded the maximum token limit. Check your max tokens limit in the copilot settings. [Learn more](#The-AI-provider-response-exceeded-the-maximum-token-limit) |
| An unexpected database error happened.                                                       | An internal database error happened. [Contact support](https://liveblocks.io/contact/support) if the problem persists.                                                                            |
| Timeout error.                                                                               | The request timed out. [Contact support](https://liveblocks.io/contact/support) if the problem persists.                                                                                          |
| Abort error.                                                                                 | The request was aborted by the user but the operation did not succeed to abort. [Contact support](https://liveblocks.io/contact/support) if the problem persists.                                 |
| Aborted by user.                                                                             | The current request was aborted by the user.                                                                                                                                                      |
| Messages deleted by user.                                                                    | A message in the chat no longer exists because it was deleted by a user.                                                                                                                          |
| Chat deleted by user.                                                                        | A chat was deleted by a user and our infra cannot proceed the request.                                                                                                                            |

---
meta:
  title: "Comments"
  parentTitle: "Ready-made features"
  description:
    "Embed a customizable commenting experience into your product to enable
    people to collaborate."
---

Embed a customizable commenting experience into your product, enabling
collaboration between your users. Liveblocks Comments is fully customizable,
allowing you to create a range of commenting experiences that look and feel like
your product. It enables you to build mentions, thread resolution, text
annotations, video annotations, and more.

## Overview

<ListGrid columns={2}>
  <DocsCard
    type="technology"
    title="Concepts"
    href="/docs/ready-made-features/comments/concepts"
    description="Learn about threads and comments"
    visual={<DocsIdeaIcon className="fill-product-icon-brand h-auto w-6" />}
  />
  <DocsCard
    type="technology"
    title="Users and mentions"
    href="/docs/ready-made-features/comments/users-and-mentions"
    description="Add users to comments and mentions"
    visual={<DocsMentionIcon className="fill-product-icon-brand h-auto w-6" />}
  />
  <DocsCard
    type="technology"
    title="Default components"
    href="/docs/ready-made-features/comments/default-components"
    description="Ready-to-use customizable components"
    visual={
      <DocsDefaultComponentsIcon className="fill-product-icon-brand h-auto w-6" />
    }
  />
  <DocsCard
    type="technology"
    title="Hooks"
    href="/docs/ready-made-features/comments/hooks"
    description="Get users, threads, & modify comments"
    visual={<DocsHooksIcon className="fill-product-icon-brand h-auto w-6" />}
  />
  <DocsCard
    type="technology"
    title="Metadata"
    href="/docs/ready-made-features/comments/metadata"
    description="Add custom metadata to threads"
    visual={<DocsMetadataIcon className="fill-product-icon-brand h-auto w-6" />}
  />
  <DocsCard
    type="technology"
    title="Primitives"
    href="/docs/ready-made-features/comments/primitives"
    description="Construct fully custom components"
    visual={
      <DocsPrimitivesIcon className="fill-product-icon-brand h-auto w-6" />
    }
  />
  <DocsCard
    type="technology"
    title="Styling and customization"
    href="/docs/ready-made-features/comments/styling-and-customization"
    description="CSS variables, dark mode, localization"
    visual={<DocsStylingIcon className="fill-product-icon-brand h-auto w-6" />}
  />
  <DocsCard
    type="technology"
    title="Email notifications"
    href="/docs/ready-made-features/comments/email-notifications"
    description="Send notifications with webhooks"
    visual={<DocsEmailIcon className="fill-product-icon-brand h-auto w-6" />}
  />
</ListGrid>

## Comments API Reference

<ListGrid columns={2}>
  <DocsCard
    type="technology"
    title="React"
    href="/docs/api-reference/liveblocks-react#Comments"
    description="@liveblocks/react"
    visual={<DocsReactIcon />}
  />
  <DocsCard
    type="technology"
    title="React UI"
    href="/docs/api-reference/liveblocks-react-ui#Comments"
    description="@liveblocks/react-ui"
    visual={<DocsReactIcon />}
  />
  <DocsCard
    type="technology"
    title="JavaScript"
    href="/docs/api-reference/liveblocks-client#Comments"
    description="@liveblocks/client"
    visual={<DocsJavascriptIcon />}
  />
  <DocsCard
    type="technology"
    title="Node.js"
    href="/docs/api-reference/liveblocks-node#Comments"
    description="@liveblocks/node"
    visual={<DocsNodejsIcon className="fill-product-icon-brand h-auto w-6" />}
  />
  <DocsCard
    type="technology"
    title="REST API"
    href="/docs/api-reference/rest-api-endpoints#Comments"
    description="HTTP endpoints"
    visual={<DocsApiIcon className="fill-product-icon-brand h-auto w-6" />}
  />
</ListGrid>

## Examples using Comments

<ListGrid columns={2}>
  <ExampleCard
    example={{
      title: "Comments",
      slug: "comments/nextjs-comments",
      image: "/images/examples/thumbnails/comments.jpg",
    }}
    technologies={["nextjs", "react"]}
    openInNewWindow
  />
  <ExampleCard
    example={{
      title: "Video Comments",
      slug: "video-comments/nextjs-comments-video",
      image: "/images/examples/thumbnails/comments-video.png",
    }}
    technologies={["nextjs"]}
    openInNewWindow
  />
  <ExampleCard
    example={{
      title: "Overlay Comments",
      slug: "overlay-comments/nextjs-comments-overlay",
      image: "/images/examples/thumbnails/comments-overlay.png",
    }}
    technologies={["nextjs"]}
    openInNewWindow
  />
  <ExampleCard
    example={{
      title: "Text Editor Comments",
      slug: "text-editor-comments/nextjs-comments-tiptap",
      image: "/images/examples/thumbnails/comments-text-editor.png",
    }}
    technologies={["nextjs"]}
    openInNewWindow
  />
</ListGrid>

---
meta:
  title: "Concepts"
  parentTitle: "Comments"
  description: "Learn about threads and comments"
---

A quick overview of the concepts used in Liveblocks Comments.

## Threads

In Liveblocks Comments, everything revolves around _threads_. In each
[multiplayer room](/docs/concepts/how-liveblocks-works#Rooms) you can create a
number of threads. Each individual thread contains a list of _comments_ written
by your users.

<Figure>
  <Image
    src="/assets/comments/comment-thread-room-relation.jpg"
    srcDark="/assets/comments/comment-thread-room-relation-dark.jpg"
    alt="Diagram showing a comment, inside a thread, inside a room"
    width={768}
    height={480}
    quality={100}
  />
</Figure>

Threads can be retrieved by
[a React hook](/docs/ready-made-features/comments/hooks#threads-hook) or
[on your server](/docs/api-reference/liveblocks-node#get-rooms-roomId-threads).
Here’s an example of a thread object.

```ts
{
  type: "thread",
  id: "th_sf8s6sh...",
  roomId: "my-room-id",
  createdAt: Date <Fri Dec 15 2023 14:15:22 GMT+0000 (Greenwich Mean Time)>,
  resolved: false,
  comments: [
    // A list of comments in the thread
    // ...
  ],
  metadata: {
    // Your custom thread metadata
    // ...
  },
}
```

Threads can store
[custom metadata](/docs/ready-made-features/comments/metadata), which is helpful
for integrating them into your product.

## Comments

Each comment is created by a user, referenced by their
[user ID](/docs/ready-made-features/comments/users-and-mentions), and is part of
a thread. The first comment in a thread is displayed at the top.

<Figure>
  <Image
    src="/assets/comments/first-comment-in-thread.jpg"
    srcDark="/assets/comments/first-comment-in-thread-dark.jpg"
    alt="thread.comments[0] is the first comment in a thread"
    width={768}
    height={480}
    quality={100}
  />
</Figure>

Here’s an example of a single comment inside a thread object.

```ts highlight="8-24"
{
  type: "thread",
  id: "th_sf8s6sh...",
  roomId: "my-room-id",
  createdAt: Date <Fri Dec 15 2023 14:15:22 GMT+0000 (Greenwich Mean Time)>,
  resolved: false,
  comments: [
    {
      type: "comment",
      threadId: "th_sf8s6sh...",
      id: "cm_agH76a...",
      roomId: "my-room-id",
      userId: "alicia@example.com",
      createdAt: Date <Fri Dec 15 2023 14:15:22 GMT+0000 (Greenwich Mean Time)>,
      editedAt: Date <Fri Dec 15 2023 15:07:19 GMT+0000 (Greenwich Mean Time)>,
      body: {
        // The comment's text in `CommentBody` format
        // ...
      },
      metadata: {
        // Your custom comment metadata
        // ...
      },
    },

    // Other comments in the thread
    // ...
  ],
  metadata: {
    // Your custom thread metadata
    // ...
  },
}
```

A comment’s `body` is in a custom `CommentBody` format, though you most likely
won’t need to use this, as we render it in React for you. We also provide a
number of functions that allow you to easily
[convert a comment’s body into Markdown, HTML, or plain text](/docs/api-reference/liveblocks-node#stringify-comment-body),
which is especially helpful for creating email notifications.

Similarly to threads, comments can also store
[custom metadata](/docs/ready-made-features/comments/metadata), which is helpful
for integrating them into your product.

### Deleted comments

Deleting a comment doesn’t remove the comment object from the thread, instead
the `comment.body` property is removed, and a `comment.deletedAt` property is
added, which contains the deletion time. This allows you to handle the deleted
comment in whichever way you see fit, for example you may like to create a
“message deleted” placeholder for the comment. Alternatively, the comment can be
hidden completely.

<Figure>
  <Image
    src="/assets/comments/comment-deleted.jpg"
    srcDark="/assets/comments/comment-deleted-dark.jpg"
    alt="Image of a thread with one comment highlighting a deleted comment"
    width={768}
    height={480}
    quality={100}
  />
</Figure>

A thread is only deleted after all its comments have been deleted.

---
meta:
  title: "Default components"
  parentTitle: "Comments"
  description: "Ready-to-use customizable components"
---

The default components included in Comments are a great way to start building
your application. With these components you can render entire threads, along
with a comment composer.

- Fully styled commenting components, with an optional dark mode.
- Customize through CSS variables and class names.
- Localize and modify strings with overrides.
- Can be used alongside
  [primitives](/docs/ready-made-features/comments/primitives).

## Thread

The [`Thread`][] component renders a thread and its contained comments, as well
a composer for adding new comments to thread.

<Figure>
  <Image
    src="/assets/comments/thread.png"
    alt="Thread"
    width={768}
    height={446}
  />
</Figure>

### Usage

The best way to get started is to import the [`useThreads`][] hook, and loop
through each thread in the current room.

```tsx highlight="5,9-11"
import { Thread } from "@liveblocks/react-ui";
import { useThreads } from "../liveblocks.config";

function Component() {
  const { threads } = useThreads();

  return (
    <>
      {threads.map((thread) => (
        <Thread key={thread.id} thread={thread} />
      ))}
    </>
  );
}
```

This will render a list containing every thread in the room, each with its own
composer for adding new comments.

## Composer

The [`Composer`][] component renders a rich-text composer that’s used to create
new threads. It can also be used to add comments to an existing thread, or to
edit comments.

<Figure>
  <Image
    src="/assets/comments/composer.png"
    alt="Composer"
    width={768}
    height={446}
  />
</Figure>

### Usage

Get started by importing the component. By default, it’ll create a new thread
when the form’s submitted.

```tsx
import { Composer } from "@liveblocks/react-ui";

// Creates a new thread
function Component() {
  return <Composer />;
}
```

[`Composer`][] can also be used in other ways:

- [Adding metadata to a new thread](/docs/api-reference/liveblocks-react-ui#Adding-thread-metadata)
- [Replying to a thread](/docs/api-reference/liveblocks-react-ui#Replying-to-a-thread)
- [Adding metadata to a reply](/docs/api-reference/liveblocks-react-ui#Adding-comment-metadata)
- [Modifying a comment](/docs/api-reference/liveblocks-react-ui#Modifying-a-comment)
- [Custom behavior](/docs/api-reference/liveblocks-react-ui#Custom-behavior)

## Comment

The [`Comment`][] component renders a single comment. It’s useful if you’re
building a custom [Thread](#Thread) component, or if you’re displaying a
standalone comment.

<Figure>
  <Image
    src="/assets/comments/comment.png"
    alt="Comment"
    width={768}
    height={446}
  />
</Figure>

### Usage

Start by importing the component, and passing the comment to it.

```tsx highlight="9"
import { Comment } from "@liveblocks/react-ui";
import { ThreadData } from "@liveblocks/client";

// Renders a list of comments attached to the specified `thread`
function MyComment({ thread }: { thread: ThreadData }) {
  return (
    <>
      {thread.comments.map((comment) => (
        <Comment key={comment.id} comment={comment} />
      ))}
    </>
  );
}
```

[`Composer`]: /docs/api-reference/liveblocks-react-ui#Composer
[`Comment`]: /docs/api-reference/liveblocks-react-ui#Comment
[`Thread`]: /docs/api-reference/liveblocks-react-ui#Thread
[`useThreads`]: /docs/api-reference/liveblocks-react#useThreads

## Customization

It’s possible to style and localize the default components:

- Import dark mode styles.
- Modify the style with CSS variables and class names.
- Use overrides to change default text used in the components.

Learn more under
[styling and customization](/docs/ready-made-features/comments/styling-and-customization).

---
meta:
  title: "Email notifications"
  parentTitle: "Comments"
  description: "Send notifications with webhooks"
---

Using Liveblocks webhooks, it’s possible to trigger your API endpoints when
certain events occur, such as a thread being created, or a comment being
modified. One use for these events is sending unread comment notifications, for
example via email or Slack.

<Figure>
  <Image
    src="/assets/comments/email-notification.png"
    alt="An email titled 'New notifications' showing two comments and a link to the thread"
    width={1500}
    height={1000}
  />
</Figure>

<Banner title="Step-by-step tutorial">

This page is an overview of creating email notifications for Comments. If you’d
prefer a full step-by-step tutorial, make sure to read
[how to send email notifications of unread comments](/docs/guides/how-to-send-email-notifications-of-unread-comments).

</Banner>

## Inbox notifications

Email notifications are built around the concept of inbox notifications, which
are different from “normal” notifications in the sense that they can group
multiple activities together and evolve over time, which makes more sense when
sending email notifications because it helps to avoid sending too many emails.
In the case of Comments, inbox notifications are grouped per thread, which means
that if there are 4 new comments in a thread you’re participating in, you will
have a single inbox notification for it, instead of 4 “normal” notifications.

Learn more about Notifications for Comments in the
[overview page](/docs/ready-made-features/comments/email-notifications).

## Sending email notifications with webhooks

Using [Liveblocks webhooks](/docs/platform/webhooks#Liveblocks-events) you can
listen to a range of events such as comments being deleted, or comment reactions
being added. On [your dashboard](https://liveblocks.io/dashboard) you can create
a webhook for a project, and select which events you’d like to listen to. You
only need to select the `"notification"` webhook event for our recommended
solution.

<Figure>
  <video width={1512} height={982} autoPlay loop muted playsInline>
    <source src="/assets/webhooks/edit-events.mp4" type="video/mp4" />
  </video>
</Figure>

The endpoint URL you pass will receive request with relevant data when the event
occurs. The webhook event built for creating these unread notification emails is
called `"notification"`, and by default is sent up to every 30 minutes to each
user, though this can be customized in the webhooks dashboard.

`"notification"` webhooks can be enabled and disabled on certain channels.
Channels are used to represent different places your users may receive
notifications, such as on `email`, `slack`, `teams`, and `webPush`.

<Figure>
  <video width={1512} height={982} autoPlay loop muted playsInline>
    <source src="/assets/webhooks/notification-settings.mp4" type="video/mp4" />
  </video>
</Figure>

Here’s an example of an event object that’s sent when a user receives a new (or
updated) inbox notification.

```ts
const event = {
  type: "notification",
  data: {
    channel: "email",
    kind: "thread",
    projectId: "my-project-id",
    roomId: "my-room-id",
    threadId: "th_d75sF3...",
    inboxNotificationId: "in_xt3p7ak...",
    userId: "my-user-id",
    createdAt: "2021-10-06T01:45:56.558Z",
  },
};
```

### Your endpoint

In your endpoint, you can use this `event` object to get information on unread
mentions and replies, and start rendering emails with
[HTML](/docs/api-reference/liveblocks-emails#prepare-thread-notification-email-as-html)
or
[React](/docs/api-reference/liveblocks-emails#prepare-thread-notification-email-as-react).

```tsx
// Get email data as React JSX
const emailData = await prepareThreadNotificationEmailAsReact(
  liveblocks,
  event
);

let email;
switch (emailData.type) {
  // The user has an unread mention
  case "unreadMention": {
    email = (
      // +++
      <div>
        <div>
          @{emailData.comment.author.id} at {emailData.comment.createdAt}
        </div>
        <div>{emailData.comment.body}</div>
      </div>
      // +++
    );
    break;
  }

  // The user has multiple unread replies
  case "unreadReplies": {
    email = (
      // +++
      <div>
        {emailData.comments.map((comment) => (
          <div key={comment.id}>
            <div>
              @{comment.author.id} at {comment.createdAt}
            </div>
            <div>{comment.body}</div>
          </div>
        ))}
      </div>
      // +++
    );
  }
}
```

You can customize this further, resolving user and room IDs into data, allowing
you to render names instead of IDs. You can also add custom React components to
the comment bodies, helpful for custom stying.

```tsx
// Get email data as React JSX
const emailData = await prepareThreadNotificationEmailAsReact(
  liveblocks,
  event,
  {
    // +++
    resolveUsers: async ({ userIds }) => {
      const usersData = await __getUsersFromDB__(userIds);

      return usersData.map((userData) => ({
        name: userData.name,
        avatar: userData.avatar.src,
      }));
    },
    // +++
    // ...
    components: {
      // `user` is the optional data returned from `resolveUsers`
      // +++
      Mention: ({ element, user }) => (
        <span style={{ color: "red" }}>@{user.name}</span>
      ),
      // +++

      // ...
    },
  }
);
```

You can then send an email containing the comment to the owner of `userId`
received in `event`.

```ts
const emailAddress = __getUserEmail__(userId);

// Send email to the user that received the inbox notification
__sendEmail__({
  from: "hello@my-company.com",
  to: emailAddress,
  title: "Unread comment",
  react: `
    <h1>Unread comment</h1>
    ${commentHtml}
  `,
});
```

Here’s an example with every step linked together, along with the code necessary
to verify a webhook request is valid, and using [Resend](https://resend.com) to
send an email.

```ts title="Full example" isCollapsable isCollapsed
import {
  Liveblocks,
  WebhookHandler,
  isThreadNotificationEvent,
} from "@liveblocks/node";
import { prepareThreadNotificationEmailAsReact } from "@liveblocks/emails";
import { Resend } from "resend";

// Create Resend client (add your API key)
const resend = new Resend("re_123456789");

// Add your webhook secret key from a project's webhooks dashboard
const WEBHOOK_SECRET = "YOUR_WEBHOOK_SECRET_KEY";
const webhookHandler = new WebhookHandler(WEBHOOK_SECRET);

// Add your secret key from a project's API keys dashboard
const API_SECRET = "sk_prod_xxxxxxxxxxxxxxxxxxxxxxxx";
const liveblocks = new Liveblocks({ secret: API_SECRET });

export async function POST(request: Request) {
  const body = await request.json();
  const headers = request.headers;

  // Verify if this is a real webhook request
  let event;
  try {
    event = webhookHandler.verifyRequest({
      headers: headers,
      rawBody: JSON.stringify(body),
    });
  } catch (err) {
    console.error(err);
    return new Response("Could not verify webhook call", { status: 400 });
  }

  // When an inbox notification has been created
  if (isThreadNotificationEvent(event)) {
    // Check if user has access to room
    if (!__hasRoomAccess__(event.userId, event.roomId)) {
      return new Response(null, { status: 200 });
    }

    // The user to send the email to
    const emailAddress = __getEmailAddressFromDB__(event.userId);

    let emailData;

    try {
      emailData = await prepareThreadNotificationEmailAsReact(
        liveblocks,
        event,
        {
          resolveUsers: async ({ userIds }) => {
            const usersData = await __getUsersFromDB__(userIds);

            return usersData.map((userData) => ({
              name: userData.name, // "Steven"
              avatar: userData.avatar.src, // "https://example.com/steven.jpg"
            }));
          },
          resolveRoomInfo: async ({ roomId }) => {
            const roomData = await __getRoomFromDB__(roomId);

            return {
              name: roomData.name, // "Untitled document"
              url: roomData.url, //`https://example.com/my-room-id`
            };
          },
          components: {
            Paragraph: ({ children }) => (
              <p style={{ margin: "12px 0" }}>{children}</p>
            ),

            // `user` is the optional data returned from `resolveUsers`
            Mention: ({ element, user }) => (
              <span style={{ color: "red" }}>@{user?.name ?? element.id}</span>
            ),

            // If the link is rich-text render it, otherwise use the URL
            Link: ({ element, href }) => (
              <a href={href} style={{ textDecoration: "underline" }}>
                {element?.text ?? href}
              </a>
            ),
          },
        }
      );
    } catch (err) {
      console.log(err);
      return new Response("Could not fetch thread notification data", {
        status: 500,
      });
    }

    // All comments have already been read
    if (!emailData) {
      return new Response(null, { status: 200 });
    }

    let email;
    switch (emailData.type) {
      case "unreadMention": {
        email = (
          <div>
            <div>
              @{emailData.comment.author.id} at {emailData.comment.createdAt}
            </div>
            <div>{emailData.comment.body}</div>
          </div>
        );
        break;
      }

      case "unreadReplies": {
        email = (
          <div>
            {emailData.comments.map((comment) => (
              <div key={comment.id}>
                <div>
                  @{comment.author.id} at {comment.createdAt}
                </div>
                <div>{comment.body}</div>
              </div>
            ))}
          </div>
        );
        break;
      }
    }

    // Send email to the user's email address
    try {
      const data = await resend.emails.send({
        from: "My company <hello@my-company.com>",
        to: emailAddress,
        subject: "New comment",
        react: email,
      });
    } catch (err) {
      console.error(err);
    }
  }

  return new Response(null, { status: 200 });
}
```

If you’re planning on building this, we recommend learning more in our
[how to send email notifications](/docs/guides/how-to-send-email-notifications-of-unread-comments)
guide, as it’s possible to create much more complex emails than this simple
example.

### Permissions

When you receive a `notification` webhook event, it's essential to verify if the
user has access to the room before sending an email. Liveblocks lacks the
necessary information to determine if a user has access to a room. For instance,
we create an inbox notification when a user is mentioned in a comment. In this
user's client context, we can determine if they have access to the notification
thanks to the token generated for that user. However, when we send a
`notification` webhook event, we lack this information.

#### Access token authentication

If you are using access tokens, this will always be true, Liveblocks will never
have the information.

#### ID token authentication

If you are using ID tokens, Liveblocks already possesses certain information
about the permissions you have configured for each room, specifying which users
and groups have access. However, what we currently lack is the relationship
between a user and a group. At present, you need to verify user access before
sending an email. We do, however, plan to include full permissions info in
Liveblocks in our future updates. If you're interested in learning more about
this feature, please feel free to reach out to us.

## Webhook events

There are more webhook events than just the `NotificationEvent` event used
above—a number related to Comments are available to use.

- [`CommentCreatedEvent`](/docs/platform/webhooks#CommentCreatedEvent)
- [`CommentEditedEvent`](/docs/platform/webhooks#CommentEditedEvent)
- [`CommentDeletedEvent`](/docs/platform/webhooks#CommentDeletedEvent)
- [`CommentReactionAddedEvent`](/docs/platform/webhooks#CommentReactionAddedEvent)
- [`CommentReactionRemovedEvent`](/docs/platform/webhooks#CommentReactionRemovedEvent)
- [`CommentMetadataUpdatedEvent`](/docs/platform/webhooks#CommentMetadataUpdatedEvent)
- [`ThreadCreatedEvent`](/docs/platform/webhooks#ThreadCreatedEvent)
- [`ThreadMetadataUpdatedEvent`](/docs/platform/webhooks#ThreadMetadataUpdatedEvent)
- [`ThreadMarkedAsResolvedEvent`](/docs/platform/webhooks#ThreadMarkedAsResolvedEvent)
- [`ThreadMarkedAsUnresolvedEvent`](/docs/platform/webhooks#ThreadMarkedAsUnresolvedEvent)

There are also more events, for example you can trigger events when users enter
or leave rooms. We recommend reading our guide on
[testing webhooks locally](/docs/guides/how-to-test-webhooks-on-localhost) to
get started.

## Retrieving and modifying Comments data

Here’s every Comments-related
[`@liveblocks/node`](/docs/api-reference/liveblocks-node) function. Each also
has a corresponding REST API, you can find more info by following the links.

- [`getThreads`](/docs/api-reference/liveblocks-node#get-rooms-roomId-threads)
- [`createThread`](/docs/api-reference/liveblocks-node#post-rooms-roomId-threads)
- [`getThread`](/docs/api-reference/liveblocks-node#get-rooms-roomId-threads-threadId)
- [`getThreadParticipants`](/docs/api-reference/liveblocks-node#get-thread-participants)
- [`editThreadMetadata`](/docs/api-reference/liveblocks-node#post-rooms-roomId-threads-threadId-metadata)
- [`createComment`](/docs/api-reference/liveblocks-node#post-rooms-roomId-threads-threadId-comments)
- [`getComment`](/docs/api-reference/liveblocks-node#get-rooms-roomId-threads-threadId-comments-commentId)
- [`editComment`](/docs/api-reference/liveblocks-node#post-rooms-roomId-threads-threadId-comments-commentId)
- [`editCommentMetadata`](/docs/api-reference/liveblocks-node#post-rooms-roomId-threads-threadId-comments-commentId-metadata)
- [`deleteComment`](/docs/api-reference/liveblocks-node#delete-rooms-roomId-threads-threadId-comments-commentId)
- [`addCommentReaction`](/docs/api-reference/liveblocks-node#post-rooms-roomId-threads-threadId-comments-commentId-add-reaction)
- [`removeCommentReaction`](/docs/api-reference/liveblocks-node#post-rooms-roomId-threads-threadId-comments-commentId-remove-reaction)
- [`getInboxNotification`](/docs/api-reference/liveblocks-node#get-users-userId-inboxNotifications-inboxNotificationId)
- [`getRoomSubscriptionSettings`](/docs/api-reference/liveblocks-node#get-rooms-roomId-users-userId-subscription-settings)
- [`updateRoomSubscriptionSettings`](/docs/api-reference/liveblocks-node#post-rooms-roomId-users-userId-subscription-settings)
- [`deleteRoomSubscriptionSettings`](/docs/api-reference/liveblocks-node#delete-rooms-roomId-users-userId-subscription-settings)

---
meta:
  title: "Hooks"
  parentTitle: "Comments"
  description: "Get users, threads, and modify comments"
---

The Comments React hooks can be used to fetch, create, and modify threads on the
client.

## List a room’s threads [#threads-hook]

The most important Comments hook is
[`useThreads`](/docs/api-reference/liveblocks-react#useThreads), which retrieves
a list of each thread in the current room. This can be used to render a list of
threads, either using the
[default components](/docs/ready-made-features/comments/default-components), or
[primitives](/docs/ready-made-features/comments/primitives). Here’s an example
of it used with the default
[`Thread`](/docs/api-reference/liveblocks-react-ui#Thread) component.

```tsx
import { useThreads } from "../liveblocks.config";

function Component() {
  const { threads } = useThreads();

  // [{ type: "thread", id: "th_sf8s6sh...", ... }, ...]
  console.log(threads);

  return (
    <>
      {threads.map((thread) => (
        <Thread key={thread.id} thread={thread} />
      ))}
    </>
  );
}
```

There are two versions of the
[`useThreads`](/docs/api-reference/liveblocks-react#useThreads) hook, the
[Suspense version](#suspense-hooks), which we recommend by default, and the
[regular version](#regular-hooks).

## Fetch a user’s information [#user-hook]

The only information Liveblocks stores about users is their user ID, which is
set when [authenticating with Liveblocks](/docs/authentication). With the
[`useUser`](/docs/api-reference/liveblocks-react#useUser) hook, you can fetch a
user’s information from their ID. This is particularly helpful when building
primitive components, as this allows you fetch their name, avatar, and any other
custom data you’ve set.

```tsx
import { useUser } from "../liveblocks.config";

function Component() {
  const { user } = useUser("olivier@example.com");

  // { name: "Olivier", avatar: "https://...", color: "red" }
  console.log(user);

  return <img src={user.avatar} alt={user.name} />;
}
```

The user data retrieved is set within the
[`resolveUsers`](/docs/api-reference/liveblocks-client#resolveUsers) function in
your `liveblocks.config.ts` file.

```ts
async function resolveUsers({ userIds }) {
  // ["olivier@example.com"]
  console.log(userIds);

  return [
    {
      name: "Olivier",
      avatar: "https://example.com/olivier.png",
      color: "red",
    },
  ];
}
```

There are two versions of the
[`useUser`](/docs/api-reference/liveblocks-client#useUser) hook,
[Suspense](#suspense-hooks), which we recommend by default, and
[regular](#regular-hooks).

## Fetch a group’s information [#group-hook]

Similar to users, Liveblocks only stores group IDs for groups. With the
[`useGroupInfo`](/docs/api-reference/liveblocks-react#useGroupInfo) hook, you
can fetch a group’s information from their ID. This is particularly helpful when
building primitive components, as this allows you to fetch their name, avatar,
and any other custom data you’ve set.

```tsx
import { useGroupInfo } from "../liveblocks.config";

function Component() {
  const { group } = useGroupInfo("engineering");

  // { name: "Engineering", avatar: "https://...", color: "blue" }
  console.log(group);

  return <img src={group.avatar} alt={group.name} />;
}
```

The group data retrieved is set within the
[`resolveGroupsInfo`](/docs/api-reference/liveblocks-client#resolveGroupsInfo)
function in your `liveblocks.config.ts` file.

```ts
async function resolveGroupsInfo({ groupIds }) {
  // ["engineering"]
  console.log(groupIds);

  return [
    {
      name: "Engineering",
      avatar: "https://example.com/engineering.png",
      color: "blue",
    },
  ];
}
```

There are two versions of the
[`useGroupInfo`](/docs/api-reference/liveblocks-client#useGroupInfo) hook,
[Suspense](#suspense-hooks), which we recommend by default, and
[regular](#regular-hooks).

## Create and modify threads and comments [#mutation-hooks]

A number of hooks can be used to mutate comment and thread data, for example:

- Creating threads
- Deleting comments
- Adding emoji reactions

Some of these hooks are quite simple, such as
[`useAddReaction`](/docs/api-reference/liveblocks-react#useAddReaction) which
adds a reaction to a specified comment.

```tsx highlight="6,9-13"
import { CommentData } from "@liveblocks/client";
import { useAddReaction } from "../liveblocks.config.ts";

// A button that adds a "⭐" reaction to a comment
function Component({ comment }: { CommentData }) {
  const addReaction = useAddReaction();

  function handleClick() {
    addReaction({
      threadId: comment.threadId,
      commentId: comment.commentId,
      emoji: "⭐",
    });
  }

  return <button onClick={handleClick}>Star ⭐</button>;
}
```

Whereas others, such as
[`useCreateThread`](/docs/api-reference/liveblocks-react#useCreateThread), have
more complex behaviour and are designed to work alongside
[primitives](/docs/ready-made-features/comments/primitives).

```tsx highlight="10,18-21"
import {
  Composer,
  ComposerSubmitComment,
} from "@liveblocks/react-ui/primitives";
import { useCreateThread } from "../liveblocks.config.ts";
import { FormEvent } from "react";

// A custom composer that creates a thread on submit
function MyComposer() {
  const createThread = useCreateThread();

  function handleComposerSubmit(
    { body }: ComposerSubmitComment,
    event: FormEvent<HTMLFormElement>
  ) {
    event.preventDefault();

    const thread = createThread({
      body,
      metadata: {},
    });
  }

  return (
    <Composer.Form onComposerSubmit={handleComposerSubmit}>
      <Composer.Editor />
      <Composer.Submit>Create thread</Composer.Submit>
    </Composer.Form>
  );
}
```

You can find more information on each mutation hook in our API reference pages:

- [`useCreateThread`](/docs/api-reference/liveblocks-react#useCreateThread)
- [`useDeleteThread`](/docs/api-reference/liveblocks-react#useDeleteThread)
- [`useEditThreadMetadata`](/docs/api-reference/liveblocks-react#useEditThreadMetadata)
- [`useMarkThreadAsResolved`](/docs/api-reference/liveblocks-react#useMarkThreadAsResolved)
- [`useMarkThreadAsUnresolved`](/docs/api-reference/liveblocks-react#useMarkThreadAsUnresolved)
- [`useCreateComment`](/docs/api-reference/liveblocks-react#useCreateComment)
- [`useEditComment`](/docs/api-reference/liveblocks-react#useEditComment)
- [`useEditCommentMetadata`](/docs/api-reference/liveblocks-react#useEditCommentMetadata)
- [`useDeleteComment`](/docs/api-reference/liveblocks-react#useDeleteComment)
- [`useAddReaction`](/docs/api-reference/liveblocks-react#useAddReaction)
- [`useRemoveReaction`](/docs/api-reference/liveblocks-react#useRemoveReaction)

There’s only one version of these hooks, so it doesn’t make a difference if you
export them from `suspense` in your config file or not.

## Hook types [#hook-types]

There are two different ways to use the [threads](#threads-hook) and
[user](#user-hook) hooks; with
[React Suspense](https://react.dev/reference/react/Suspense), and without it. We
recommend using the Suspense versions, as they often result in simpler code.

### Suspense hooks [#suspense-hooks]

Using Suspense hooks means that any data retrieved, for example `threads` from
`useThreads`, will never be `undefined`, and your component will never see an
error.

```tsx
import { useThreads } from "@liveblocks/react/suspense";

// Suspense: `threads` is always defined
function MyThreads() {
  const { threads } = useThreads();

  // [{ type: "thread", id: "th_sf8s6sh...", ... }, ...]
  console.log(threads);
}
```

To catch errors and display a loading screen, you can use
[`ErrorBoundary`](https://www.npmjs.com/package/react-error-boundary) and
[`ClientSideSuspense`](/docs/api-reference/liveblocks-react#suspense-avoid-ssr).

```tsx highlight="7-11"
import { ClientSideSuspense } from "@liveblocks/react/suspense";
import { ErrorBoundary } from "react-error-boundary";

// Handle errors and loading state in the component above
function Component() {
  return (
    <ErrorBoundary fallback={<div>Error</div>}>
      <ClientSideSuspense fallback={<div>Loading...</div>}>
        <MyThreads />
      </ClientSideSuspense>
    </ErrorBoundary>
  );
}
```

To use Suspense, make sure you’re exporting your hooks from
`@liveblocks/react/suspense`.

```tsx
// Suspense version of hooks
import { useThreads, useUser, useGroupInfo } from "@liveblocks/react/suspense";
//                                                                   ^^^^^^^^
```

### Regular hooks [#regular-hooks]

The regular versions of Liveblocks hooks require you to check for `error` and
`isLoading` properties. You can then handle these states in the same component.

```tsx
import { useThreads } from "@liveblocks/react";

// Handle errors and loading state in the same component
function MyThreads() {
  const { threads, error, isLoading } = useThreads();

  if (error) {
    return <div>Error</div>;
  }

  if (isLoading) {
    return <div>Loading...</div>;
  }

  // Non-Suspense: `threads` is only defined AFTER the `if` checks
  // [{ type: "thread", id: "th_sf8s6sh...", ... }, ...]
  console.log(threads);
}
```

To use the regular hooks, make sure you’re exporting from `@liveblocks/react`.

```tsx
// Regular version of hooks
import { useThreads, useUser, useGroupInfo } from "@liveblocks/react";
//                                                 ^^^^^^^^^^^^^^^^^
```

---
meta:
  title: "Metadata"
  parentTitle: "Comments"
  description: "Add custom metadata to threads and comments"
---

In Comments, each thread and comment can be given custom metadata. This is
helpful for integrating Comments into various applications, for example it can
be used to store:

- The priority, status, or category of a thread.
- Coordinates or timestamps related to the thread.
- Quoted text in a text editor.
- Tags, context, or flags on a comment.
- External IDs like Slack message IDs on comments to build two-way integrations.

## Valid metadata

You can store any `string`, `boolean`, or `number` in metadata, for example:

```ts
metadata: {
  priority: "URGENT",
  pinned: false,
  timestamp: 1703073188013,
}
```

Setting a property to `null` will remove it.

```ts
metadata: {
  timestamp: null,
}
```

You can store up to 50 metadata properties per thread or comment, and each
property can be 4000 characters long.

## Setting metadata

Metadata can be set using the default components, React hooks, or by editing
threads and comments using the REST APIs. But before making any changes, it’s
recommended to set your metadata types in your config file.

```ts file="liveblocks.config.ts"
declare global {
  interface Liveblocks {
    // +++
    ThreadMetadata: {
      priority: string;
      pinned: boolean;
      timestamp: number;
    };
    CommentMetadata: {
      slackMessageTs: string;
      spam: boolean;
      upvotes: number;
    };
    // +++

    // ...
  }
}
```

Learn more about
[typing your data](/docs/api-reference/liveblocks-react#Typing-your-data).

### Composer component

The [`<Composer />`](/docs/api-reference/liveblocks-react-ui#Composer) component
is used to create threads and comments, and by default it’ll create a new
thread, with optional metadata, when its form is submitted.

<Figure>
  <Image
    src="/assets/comments/composer.png"
    alt="Composer"
    width={768}
    height={446}
  />
</Figure>

When using it to create a thread, you can
[pass custom metadata](/docs/api-reference/liveblocks-react-ui#Adding-thread-metadata),
which will be attached to the thread.

```tsx
import { Composer } from "@liveblocks/react-ui";

// Creates a new thread with custom metadata
function Component() {
  return (
    <Composer
      // +++
      metadata={{
        // Custom metadata here
        priority: "URGENT",
        pinned: false,
        timestamp: 1703073188013,
      }}
      // +++
    />
  );
}
```

When using it to reply to a thread, you can also
[pass custom metadata](/docs/api-reference/liveblocks-react-ui#Adding-comment-metadata),
which will be attached to the comment.

```tsx
import { Composer } from "@liveblocks/react-ui";

// Creates a new reply to an existing thread with custom metadata
function Component({ threadId }) {
  return (
    <Composer
      // +++
      threadId={threadId}
      commentMetadata={{
        // Custom metadata here
        tag: "engineering",
        reviewed: false,
      }}
      // +++
    />
  );
}
```

### Mutation hooks

Several
[mutation hooks](/docs/ready-made-features/comments/hooks#mutation-hooks) allow
you to edit thread and comment metadata:
[`useEditThreadMetadata`](/docs/api-reference/liveblocks-react#useEditThreadMetadata),
[`useEditCommentMetadata`](/docs/api-reference/liveblocks-react#useEditCommentMetadata),
and [`useCreateThread`](/docs/api-reference/liveblocks-react#useCreateThread).
Here’s one way to create a button that sets the `pinned` metadata property to
`true` on a thread.

```tsx highlight="5,10-13"
import { useEditThreadMetadata } from "@liveblocks/react/suspense";

// Button that sets `pinned` metadata to `true`
function Component() {
  const editThreadMetadata = useEditThreadMetadata();

  return (
    <button
      onClick={() =>
        editThreadMetadata({
          threadId: "th_d75sF3...",
          metadata: { pinned: true },
        })
      }
    >
      Pin thread
    </button>
  );
}
```

Here’s an example of editing comment metadata:

```tsx highlight="5,10-15"
import { useEditCommentMetadata } from "@liveblocks/react/suspense";

// Button that sets `spam` metadata to `true` on a comment
function Component() {
  const editCommentMetadata = useEditCommentMetadata();

  return (
    <button
      onClick={() =>
        editCommentMetadata({
          threadId: "th_d75sF3...",
          commentId: "cm_agH76a...",
          metadata: { spam: true },
        })
      }
    >
      Mark as spam
    </button>
  );
}
```

### REST APIs

It’s possible to modify metadata from our REST APIs using the
[`@liveblocks/node`](/docs/api-reference/liveblocks-node) package. Here’s an
example using
[`liveblocks.editThreadMetadata`](/docs/api-reference/liveblocks-node#post-rooms-roomId-threads-threadId-metadata)
for threads.

```ts highlight="12-14"
import { Liveblocks } from "@liveblocks/node";

// Create a Node.js client
const liveblocks = new Liveblocks({ secret: "{{SECRET_KEY}}" });

// Edit a thread's `priority` metadata, passing the user ID that made the change
await liveblocks.editThreadMetadata({
  roomId: "my-room-id",
  threadId: "th_d75sF3...",

  data: {
    metadata: {
      priority: "IMPORTANT",
    },
    userId: "florent@example.com",
  },
});
```

And here’s an example using
[`liveblocks.editCommentMetadata`](/docs/api-reference/liveblocks-node#post-rooms-roomId-threads-threadId-comments-commentId-metadata)
for comments.

```ts highlight="12-14"
import { Liveblocks } from "@liveblocks/node";

// Create a Node.js client
const liveblocks = new Liveblocks({ secret: "{{SECRET_KEY}}" });

// Edit a comment's `tag` metadata, passing the user ID that made the change
await liveblocks.editCommentMetadata({
  roomId: "my-room-id",
  threadId: "th_d75sF3...",
  commentId: "cm_agH76a...",

  data: {
    metadata: {
      tag: "engineering",
    },
    userId: "florent@example.com",
  },
});
```

The same result can also be achieved directly through our
[edit thread metadata](/docs/api-reference/rest-api-endpoints#post-rooms-roomId-threads-threadId-metadata)
REST API.

```bash
curl -X POST https://api.liveblocks.io/v2/rooms/my-room-id/threads/th_d75sF3.../metadata \
  -H "Authorization: Bearer YOUR_SECRET_KEY" \
  -H "Content-Type: application/json" \
  -d '{"metadata": {"priority": "IMPORTANT"}, "userId": "florent@example.com"}'
```

And our
[edit comment metadata](/docs/api-reference/rest-api-endpoints#post-rooms-roomId-threads-threadId-comments-commentId-metadata)
REST API.

```bash
curl -X POST https://api.liveblocks.io/v2/rooms/my-room-id/threads/th_d75sF3.../comments/cm_agH76a.../metadata \
  -H "Authorization: Bearer YOUR_SECRET_KEY" \
  -H "Content-Type: application/json" \
  -d '{"metadata": {"tag": "engineering"}, "userId": "olivier@example.com"}'
```

---
meta:
  title: "Primitives"
  parentTitle: "Comments"
  description: "Construct fully custom components"
---

The primitive components included in Comments are a great way to build complex,
completely custom components. These are headless and unstyled, and can be used
to construct a fully styled comment body and rich-text comment composer.

- Liveblocks functionality, but your own fully custom styles.
- Build custom components for mentions, suggestions, links, and more.
- Composable components that work similarly to
  [Radix UI](https://www.radix-ui.com/) and
  [Headless UI](https://headlessui.com/).
- Can be used alongside the
  [default components](/docs/ready-made-features/comments/default-components).

## Concepts

Each primitive is made up of one or more components, for example
[`Comment`](/docs/api-reference/liveblocks-react-ui#primitives-Comment) only
needs `Comment.Body`. This component takes a comment’s body, and turns it into
readable text, with links and mentions.

```tsx highlight="6"
import { CommentData } from "@liveblocks/client";
import { Comment } from "@liveblocks/react-ui/primitives";

// Render a custom comment body
function MyComment({ comment }: { comment: CommentData }) {
  return <Comment.Body body={comment.body} />;
}
```

### Use regular props

Each primitive accepts regular HTML props, and passes them down to it’s root
component. For example a `style` prop on `Comment.Body` will be passed down to
its root `div` component. This means you can use regular `div` properties with
your component.

```tsx highlight="9-11"
import { CommentData } from "@liveblocks/client";
import { Comment } from "@liveblocks/react-ui/primitives";

// Render a custom comment body
function MyComment({ comment }: { comment: CommentData }) {
  return (
    <Comment.Body
      body={comment.body}
      className="text-gray-500"
      style={{ width: "300px" }}
      onPointerEnter={() => {}}
    />
  );
}
```

By default, each component uses an appropriate root element, for example
`Composer.Submit` is a `button`, and `Composer.Form` is a `form`. You can use a
custom component with
[`asChild`](/docs/ready-made-features/comments/primitives#Merge-with-your-design-system-components).

### Use custom component parts

Many primitives allow you to customize their parts by passing a `components`
property. In this example, we’re modifying links in the comment, so that they’re
purple and bold.

```tsx
import { CommentData } from "@liveblocks/client";
import { Comment, CommentBodyLinkProps } from "@liveblocks/react-ui/primitives";

// Render a custom comment body
function MyComment({ comment }: { comment: CommentData }) {
  return (
    <div>
      <Comment.Body
        body={comment.body}
        // +++
        components={{
          Link,
        }}
        // +++
      />
    </div>
  );
}

// +++
// Render a purple link in the comment, e.g. "https://liveblocks.io"
function Link({ href, children }: CommentBodyLinkProps) {
  return (
    <Comment.Link href={href} style={{ color: "purple", fontWeight: 700 }}>
      {children}
    </Comment.Link>
  );
}
// +++
```

### Merge with your design system components

You can also add components directly from your design system. Let’s say you have
a `DesignSystemLink` that looks like this.

```tsx
function DesignSystemLink({ url, children }) {
  return (
    <a href={url} target="_blank">
      {children}
    </a>
  );
}
```

If you were to place this inside `<Comment.Link>`, you’d render two `<a>`
elements, which is not valid HTML. This occurs because both `<Comment.Link>` and
`<DesignSystemLink>` render `<a>` elements.

```tsx
<Comment.Link href={href} style={{ color: "purple " }}>
  <DesignSystemLink url={href}>{children}</DesignSystemLink>
</Comment.Link>

// ===================================================================

// ❌ Renders two separate <a> tags
 <a href="https://liveblocks.io" style="color: purple">
   <a href="https://liveblocks.io" target="_blank">https://liveblocks.io</a>
 </a>
```

However, if you add the `asChild` property to `Comment.Link` it won’t render any
component, and will instead merge into the child element. This means you can use
a link element from your design system, and only render a single `<a>` element.

```tsx highlight="1"
<Comment.Link href={href} style={{ color: "purple " }} asChild>
  <DesignSystemLink url={href}>{children}</DesignSystemLink>
</Comment.Link>

// ===================================================================

// ✅ Renders one combined <a> tag
<a href="https://liveblocks.io" style="color: purple" target="_blank">
  https://liveblocks.io
</a>
```

This is called composability, and virtually all Comments primitives are
composable with `asChild`; they forward their props and refs, merge their
classes and styles, and chain their event handlers with the child element.

```tsx
import { Button } from "@/my-design-system";

// Use the default <button> element
<Composer.Submit disabled>Send</Composer.Submit>;

// Use an existing custom <Button> component
<Composer.Submit disabled asChild>
  <Button variant="primary">Send</Button>
</Composer.Submit>;
```

## Composer

The [`Composer`](/docs/api-reference/liveblocks-react-ui#primitives-Composer)
primitive allows you to build a custom rich-text composer, which can be used for
creating, or editing, threads and comments. Here’s an example of a composer that
creates a new thread when it’s submitted.

```tsx
import { Composer } from "@liveblocks/react-ui/primitives";
import { useCreateThread } from "../liveblocks.config.ts";

// Render a custom composer that creates a thread on submit
function MyComposer() {
  const createThread = useCreateThread();

  return (
    <Composer.Form
      onComposerSubmit={({ body }, event) => {
        event.preventDefault();
        const thread = createThread({
          body,
          metadata: {},
        });
      }}
    >
      <Composer.Editor components={/* Your custom component parts */} />
      <Composer.Submit>Create thread</Composer.Submit>
    </Composer.Form>
  );
}
```

Custom component parts can be used to render custom
[mentions](/docs/api-reference/liveblocks-react-ui#primitives-Composer.Editor-Mention),
[links](/docs/api-reference/liveblocks-react-ui#primitives-Composer.Link), and a
[suggestions selection popover](/docs/api-reference/liveblocks-react-ui#primitives-Composer.Suggestions).

### useComposer

The [`useComposer`](/docs/api-reference/liveblocks-react-ui#useComposer) hook
can be placed within `<Composer.Form>` to check if the composer input is empty,
or to submit the form, helpful for creating your own button, or styling the UI.

```tsx highlight="14,17-19"
import { Composer, useComposer } from "@liveblocks/react-ui/primitives";

function MyComposer() {
  return (
    <Composer.Form onComposerSubmit={/* handle submit */}>
      <Composer.Editor components={/* Your custom component parts */} />
      <MyComposerButton />
    </Composer.Form>
  );
}

// Button that submits the form, and is disabled when the input is empty
function MyComposerButton() {
  const { isEmpty, submit } = useComposer();

  return (
    <button onClick={submit} disabled={isEmpty}>
      Create thread
    </button>
  );
}
```

## Comment

The [`Comment`](/docs/api-reference/liveblocks-react-ui#primitives-Comment)
primitive is used to render a `comment.body` object as text, mentions, and
links.

```tsx
import { Comment } from "@liveblocks/react-ui/primitives";
import { CommentData } from "@liveblocks/client";

// Render custom comments in a thread.
function MyComments({ comments }: { comments: CommentData[] }) {
  return (
    <>
      {comments.map((comment) => (
        <div key={comment.id}>
          <Comment.Body
            body={comment.body}
            components={/* Your custom component parts */}
          />
        </div>
      ))}
    </>
  );
}
```

The component above would typically be combined with
[`useThreads`](/docs/api-reference/liveblocks-react#useThreads).

```tsx
import { useThreads } from "../liveblocks.config";

function MyThreads() {
  const { threads } = useThreads();

  return (
    <>
      {threads.map((thread) => (
        <MyComments key={thread.id} comments={thread.comments} />
      ))}
    </>
  );
}
```

Custom component parts can be used to render
[mentions](/docs/api-reference/liveblocks-react-ui#primitives-Comment.Mention)
and [links](/docs/api-reference/liveblocks-react-ui#primitives-Comment.Link).

## Timestamp

The [`Timestamp`](/docs/api-reference/liveblocks-react-ui#primitives-Timestamp)
primitive is a quick helper that will convert a date object, or timestamp, into
a friendly format. For example, it’ll render a format similar to “5 minutes ago”
for a recent comment, and “22 Dec” for an older comment.

```tsx highlight="12"
import { Timestamp } from "@liveblocks/react-ui";
import { useThreads } from "../liveblocks.config";

// Render threads with friendly datetime messages above
function Component() {
  const { threads } = useThreads();

  return (
    <>
      {threads.map((thread) => (
        <div key={thread.id}>
          Thread posted at: <Timestamp date={thread.createdAt} />
          {/* Render `thread` ... */}
        </div>
      ))}
    </>
  );
}
```

---
meta:
  title: "Styling and customization"
  parentTitle: "Comments"
  description: "CSS variables, dark mode, localization, and more"
---

Styling
[default components](/docs/ready-made-features/comments/default-components) and
[primitives](/docs/ready-made-features/comments/primitives) is enabled through a
range of means, such as CSS variables, class names, and more. It’s also possible
to use
[overrides](/docs/ready-made-features/comments/styling-and-customization#Overrides-and-localization)
to modify any strings used in the default components, which is especially
helpful for localization.

## Default components

To add the default components’ theme, import the
[default styles](/docs/api-reference/liveblocks-react-ui#Default-styles) CSS
file.

```ts
import "@liveblocks/react-ui/styles.css";
```

You can also import one of two CSS files to enable
[dark mode](/docs/api-reference/liveblocks-react-ui#Dark-mode), depending on how
you’d like to enable it.

```ts
// Dark mode using the system theme with `prefers-color-scheme`
import "@liveblocks/react-ui/styles/dark/media-query.css";
```

```ts
// Dark mode using `className="dark"`, `data-theme="dark"`, or `data-dark="true"`
import "@liveblocks/react-ui/styles/dark/attributes.css";
```

### CSS variables

A number of
[CSS variables](/docs/api-reference/liveblocks-react-ui#CSS-variables) can be
used to customize colors, spacing, and more. This is our recommended path for
styling the default components, as you can quickly and easily modify all
components with just a few variables.

```css
/* Styles all default components */
.lb-root {
  --lb-accent: purple;
  --lb-spacing: 1em;
  --lb-radius: 0;
}
```

### Class names

Should you need deeper customization,
[class names](/docs/api-reference/liveblocks-react-ui#Class-names) can be
styled, some of which provide contextual data attributes.

```css
.lb-thread {
  /* Customize thread */
}

.lb-avatar[data-loading] {
  /* Customize avatar loading state */
}
```

### Overrides and localization

It’s possible to
[override strings](/docs/api-reference/liveblocks-react-ui#Overrides) used in
the default components, which has a couple of different uses, the first being
localization. In this example, we’re globally switching the word “Anonymous” to
“Anonyme” for French users.

```tsx
import { LiveblocksUiConfig } from "@liveblocks/react-ui";

export function App() {
  return (
    <LiveblocksUiConfig
      overrides={{ locale: "fr", USER_UNKNOWN: "Anonyme" /* ... */ }}
    >
      {/* ... */}
    </LiveblocksUiConfig>
  );
}
```

You can also override strings on a component basis, for example if you’d like to
change the placeholder text in the
[`Composer`](/docs/api-reference/liveblocks-react-ui#Composer).

```tsx
import { Composer } from "@liveblocks/react-ui";

function Component() {
  return (
    <Composer
      overrides={{
        COMPOSER_PLACEHOLDER: "Reply to thread…",
      }}
    />
  );
}
```

## Primitives

Primitives
[can be styled like any other element](/docs/ready-made-features/comments/primitives#Use-regular-props)
in your React application—each component passes down props to its root element,
meaning you can use regular HTML props.

```tsx
import { Composer } from "@liveblocks/react-ui/primitives";

function Component() {
  return (
    <Composer.Submit className="btn-primary" style={{ color: "#ffffff" }}>
      Reply
    </Composer.Submit>
  );
}
```

You can use your custom design system components with primitives by using the
`asChild` property, which will
[merge the primitive’s props](/docs/ready-made-features/comments/primitives#Merge-with-your-design-system-components)
into your component’s.

```tsx highlight="6"
import { Composer } from "@liveblocks/react-ui/primitives";
import { Button } from "@/my-design-system";

function Component() {
  return (
    <Composer.Submit asChild>
      <Button variant="primary">Reply</Button>
    </Composer.Submit>
  );
}
```

### Tailwind

Style primitives with [Tailwind](https://tailwindcss.com) by adding class names.

```tsx
import { Composer } from "@liveblocks/react-ui/primitives";

function Component() {
  return (
    <Composer.Submit className="bg-black text-white">Reply</Composer.Submit>
  );
}
```

### CSS modules

Style primitives with [CSS modules](https://github.com/css-modules/css-modules)
as you would any other component.

```tsx
import { Composer } from "@liveblocks/react-ui";
import styles from "./Component.module.css";

function Component() {
  return <Composer.Submit className={styles.submit}>Reply</Composer.Submit>;
}
```

---
meta:
  title: "Users and mentions"
  parentTitle: "Comments"
  description: "Add users and groups to comments and mentions"
---

When a comment is posted, Liveblocks doesn’t store any user metadata, for
example their avatar or name. Instead, it only saves their user ID, which you
manually set when authenticating. To fetch user metadata, we provide functions
that allow you to return the correct data.

<Figure>
  <Image
    src="/assets/tutorials/comments/thread.png"
    alt="Thread with resolved users"
    width={1291}
    height={892}
  />
</Figure>

<Banner title="Step-by-step tutorial">

This page explains the concepts behind adding users and mentions to Comments. If
you’d prefer a full step-by-step tutorial, make sure to read
[how to add users to Liveblocks Comments](/docs/guides/how-to-add-users-to-liveblocks-comments).

</Banner>

## Authenticate your users

You can set a user’s ID when authenticating your application, for example with
[`liveblocks.prepareSession`](/docs/api-reference/liveblocks-node#access-tokens).
This ID is then used inside Comments to represent the current user, for example
we’re using an email address as a user ID below.

```ts highlight="1"
const session = liveblocks.prepareSession("charlie.layne@example.com", {
  userInfo: {
    // Custom user info to be used in Presence
    // This is NOT used in Comments
    // ...
  },
});
```

There are two different authentication methods—make sure to follow an
[authentication guide for your framework](/docs/authentication) to get started.

## Adding user information

To add names and avatars to Comments, you need to convert user IDs into user
objects. For example, here’s a `userIds` array and the information you need to
return. You should return the same number of users as the number of user IDs, in
the same order.

```shell