Docs

React UI

Composer triggers

Build a composer with people mentions, slash commands, variables, and a custom trigger menu.

This recipe wires several trigger catalogs into one composer: @ for people, / for commands, and $ for variables. The same pattern works for files, tools, projects, issues, or any other application-owned entity.

Complete composer

import { useMemo, useState } from "react";
import { useChat } from "@anvia/react";
import {
  ChatProvider,
  Composer,
  Thread,
  type ComposerEntity,
  type ComposerTriggerDefinition,
} from "@anvia/react-ui";
import "@anvia/react-ui/styles.css";

const people = [
  { id: "user_ada", name: "Ada Lovelace", role: "Engineering" },
  { id: "user_grace", name: "Grace Hopper", role: "Platform" },
  { id: "user_katherine", name: "Katherine Johnson", role: "Research" },
];

const variables = [
  { id: "account_name", label: "account.name" },
  { id: "plan_name", label: "subscription.plan" },
  { id: "renewal_date", label: "subscription.renewalDate" },
];

export function TriggerChat() {
  const chat = useChat({ endpoint: "http://localhost:8787/api/chat" });
  const [entities, setEntities] = useState<ComposerEntity[]>([]);
  const triggers = useMemo<ComposerTriggerDefinition[]>(
    () => [
      {
        id: "people",
        char: "@",
        items: ({ query }) =>
          people
            .filter((person) => person.name.toLowerCase().includes(query.toLowerCase()))
            .map((person) => ({
              id: person.id,
              label: person.name,
              detail: person.role,
              data: { type: "person", role: person.role },
            })),
      },
      {
        id: "commands",
        char: "/",
        startOfLine: true,
        items: [
          { id: "summarize", label: "Summarize thread", text: "/summarize" },
          { id: "draft_reply", label: "Draft reply", text: "/draft-reply" },
          { id: "find_risks", label: "Find risks", text: "/find-risks" },
        ],
      },
      {
        id: "variables",
        char: "$",
        items: variables.map((variable) => ({
          id: variable.id,
          label: variable.label,
          text: `$${variable.label}`,
          data: { type: "variable" },
        })),
      },
    ],
    [],
  );

  return (
    <ChatProvider controller={chat}>
      <Thread.Root className="chat">
        <Thread.Viewport className="chat-scroll" autoScroll>
          <Thread.Messages />
          <Thread.Error />
        </Thread.Viewport>

        <Composer.Root
          className="composer"
          triggers={triggers}
          entities={entities}
          onEntitiesChange={setEntities}
        >
          <Composer.Input className="composer-input" placeholder="Try @, /, or $..." />
          <Composer.TriggerMenu className="trigger-menu">
            {(trigger) => (
              <>
                <div className="trigger-menu-heading">
                  {trigger.loading ? "Searching..." : `Select ${trigger.trigger.char}`}
                </div>
                {trigger.items.map((item, index) => (
                  <Composer.TriggerItem
                    className="trigger-menu-item"
                    item={item}
                    index={index}
                    key={item.id}
                  >
                    {(option) => (
                      <>
                        <span>{option.label}</span>
                        {option.detail ? <small>{option.detail}</small> : null}
                      </>
                    )}
                  </Composer.TriggerItem>
                ))}
              </>
            )}
          </Composer.TriggerMenu>
          <Composer.Submit>Send</Composer.Submit>
        </Composer.Root>
      </Thread.Root>
    </ChatProvider>
  );
}

The trigger primitives emit state attributes so CSS can track the active trigger, selected item, and loading state.

.trigger-menu {
  z-index: 40;
  display: grid;
  min-width: 220px;
  overflow: hidden;
  border: 1px solid rgb(39 39 42);
  border-radius: 8px;
  background: rgb(24 24 27);
  color: white;
  box-shadow: 0 18px 45px rgb(0 0 0 / 28%);
}

.trigger-menu-heading {
  padding: 8px 10px;
  border-bottom: 1px solid rgb(39 39 42);
  color: rgb(161 161 170);
  font-size: 12px;
}

.trigger-menu-item {
  display: grid;
  gap: 2px;
  padding: 9px 10px;
  border: 0;
  background: transparent;
  color: inherit;
  text-align: left;
}

.trigger-menu-item[data-selected] {
  background: rgb(39 39 42);
}

.trigger-menu-item small {
  color: rgb(161 161 170);
}

Submitted metadata

The default composer submit sends selected entities in UI message metadata:

type SubmittedMetadata = {
  composer?: {
    entities: ComposerEntity[];
  };
};

Each entity includes the trigger catalog, trigger character, inserted text, plain-text range, and the item data payload. Use this on the route to resolve ids, apply slash commands, or attach structured variables before the message reaches the agent.

For the lower-level API, see Composer triggers.