Back to Copilotkit

JSX Callbacks

showcase/shell-docs/src/content/docs/channels/reference/callbacks.mdx

1.64.14.7 KB
Original Source

Interactive components carry a callback. When the user clicks a button, picks from a select, submits an input, or reacts to a message, your handler runs with the thread and the interaction in hand.

This is the reference. For the walkthrough, see the UI library.

Which component takes which callback

ComponentPropHandler typeFires when
ButtononClickClickHandler<TValue>the button is clicked
SelectonSelectClickHandler<string | string[]>an option is chosen (multi gives an array)
InputonSubmitClickHandler<string>the input is submitted
MessageonReactionMessageReactionHandlera user reacts to the message

ClickHandler

onClick, onSelect, and onSubmit are all a ClickHandler. It receives one InteractionContext:

ts
type ClickHandler<TValue = unknown> = (ctx: InteractionContext<TValue>) => void | Promise<void>;
FieldTypeDescription
threadThreadThe conversation. Post, update, run the agent, block on a choice. See the Thread API.
messageIncomingMessageThe message the control lives on. message.ref updates that message in place.
action.idstringThe control's opaque id.
action.valueTValueThe value the control carried, typed by TValue.
valuesRecord<string, unknown>All input values submitted with this interaction.
userPlatformUserWho interacted.
platformstringThe platform the interaction came from.
openModal(view: ModalView) => Promise<{ ok, error? }>Open a modal. Capability-gated. On Discord, call it before any long work (the trigger expires in ~3s).

The value round-trip

A control's value prop is echoed back on action.value, typed by TValue. That is how you tell clicks apart without separate ids:

tsx
<Button
  value={{ choice: "approve" }}
  onClick={async ({ thread, message, action }) => {
    action.value; // { choice: "approve" }
    await thread.update(message.ref, "Approved.");
  }}
>
  Approve
</Button>

Button, Select, Input

tsx
import { Actions, Button, Select, Input } from "@copilotkit/channels/ui";

<Actions>
  <Button
    value={{ id: 42 }}
    style="primary"
    onClick={async ({ thread, action }) => {
      await thread.post(`Picked ${action.value.id}`);
    }}
  >
    Choose
  </Button>

  <Select
    placeholder="Priority"
    options={[
      { label: "High", value: "high" },
      { label: "Low", value: "low" },
    ]}
    onSelect={async ({ thread, action }) => {
      await thread.post(`Priority: ${action.value}`);
    }}
  />

  <Input
    placeholder="Add a note"
    onSubmit={async ({ thread, action }) => {
      await thread.post(`Note: ${action.value}`);
    }}
  />
</Actions>;

Button also takes url (a link button, which ignores onClick/value) and style: "primary" | "danger". Select takes multi for multi-select (where action.value is a string[]).

MessageReactionHandler

<Message onReaction> is different: it takes the emoji first, then the full reaction.

ts
type MessageReactionHandler = (emoji: EmojiValue, reaction: MessageReaction) => void | Promise<void>;
reaction fieldTypeDescription
emojiEmojiValueCanonical name when recognized, else the raw token. Same as the first argument.
rawEmojistringPlatform-native token.
addedbooleantrue = added, false = removed.
userPlatformUser?The reacting user, when reported.
messageIdstringId of the reacted message.
threadThreadPost back, run the agent, block on a choice, react.
messageRefMessageRefPass to thread.update(messageRef, ui) to redraw this message.
tsx
import { Message, Section } from "@copilotkit/channels/ui";

<Message
  onReaction={async (emoji, reaction) => {
    if (!reaction.added || emoji !== "thumbs_up") return;
    await reaction.thread.post(`Thanks, ${reaction.user?.name ?? "friend"}.`);
  }}
>
  <Section>{"React with a thumbs up."}</Section>
</Message>;

See reactions for more, and global reactions for reacting to any message rather than a specific card.

Handlers must survive a restart

<Callout type="warn"> A click or reaction can arrive after your process restarts, when the in-memory handler is gone. Register every component that carries a callback in `createChannel({ components })` so the handler is rebuilt from the durable snapshot. See [the UI library](/channels/ui-library#register-components). </Callout>