INTEGRATIONS

Add reactions to your chat UI

For agents with a chat interface you build yourself. Emote chooses a reaction; your app displays it on the original incoming user message.

Prerequisites

An Emote API key, a server that handles incoming messages, and a way to send message updates to your chat UI. Any frontend framework works; the rendering example below uses React.

Set EMOTE_API_KEY on your server. Each incoming message needs a stable ID so the reaction can be attached to the correct bubble.

You don’t need an existing reaction UI. Reuse one if you have it; otherwise, add a small accessible badge to the user’s message bubble using the rendering example below. Match your chat’s existing design.

Copy the TypeScript or Python example into your server code. TypeScript uses Node.js 20 or later; Python uses Python 3.10 or later with pip install httpx. Each example includes the Emote request function. Keep API keys in server environment variables.

Reaction options

Use emojis, icons, or your own reaction badges. Send only the options your UI can render, using built-in emoji strings or custom IDs with descriptions. For API setup and running Emote alongside your model, follow the quickstart and example.

reaction options
{
  "reactions": [
    "❤️",
    {
      "id": "acknowledge",
      "description": "A quiet acknowledgment, without implying a requested task is complete."
    }
  ]
}

The response will be "❤️", "acknowledge", or "none". Choose the symbol and accessible label for each ID in your UI.

Integration example

Keep the Emote request on your server. Once it finishes, save the reaction against the original message ID and agent ID, then publish the update through your existing database subscription, WebSocket, or event stream. The text reply can keep streaming independently.

Include saved reactions in later requests as context[].reaction. That gives Emote the context to avoid reacting repeatedly to the same exchange.

The server helper below takes your existing model and delivery functions. Implement the attach-reaction callback as an upsert and publish the changed message to authorized participants. The active-message callback should discard deleted messages or results from a conversation that has been reset. Callback names follow the language you choose below. Set EMOTE_API_KEY on the server.

import { createHash } from "node:crypto";

export const REACTIONS = [
  "👍", "❤️", "😂", "🎉", "😮", "😢",
  "🤔", "🔥", "💀", "🙌", "👀", "💯",
];

export type ReactionOption = string | { id: string; description: string };
export type ContextMessage = { role: "user" | "assistant"; content: string; reaction?: string };
export type ReactionInput = {
  eventId: string; message: string; agent?: string;
  context?: ContextMessage[]; reactions?: ReactionOption[];
  frequency?: "reserved" | "balanced" | "expressive";
};

// Server only. eventId must identify this message across all your conversations.
export async function chooseReaction({
  eventId, message, agent, context = [], reactions = REACTIONS, frequency = "balanced",
}: ReactionInput): Promise<string> {
  const key = process.env.EMOTE_API_KEY;
  if (!key) throw new Error("Set EMOTE_API_KEY on your server.");
  if (!eventId) throw new Error("A stable eventId is required.");

  const response = await fetch("https://useemote.com/v1/react", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${key}`,
      "Content-Type": "application/json",
      "Idempotency-Key": createHash("sha256").update(eventId).digest("hex"),
    },
    body: JSON.stringify({ message, agent, context, reactions, frequency }),
    signal: AbortSignal.timeout(3000),
  });
  if (!response.ok) throw new Error(`Emote: HTTP ${response.status}`);
  const reaction = await response.json();
  const allowed = reactions.map((option) => typeof option === "string" ? option : option.id);
  if (typeof reaction !== "string" || (reaction !== "none" && !allowed.includes(reaction))) {
    throw new Error("Unexpected Emote response.");
  }
  return reaction;
}

type ChatInput = ReactionInput & {
  messageId: string;
  generateReply: (message: string, context: NonNullable<ReactionInput["context"]>) => Promise<string>;
  sendReply: (messageId: string, reply: string) => Promise<void>;
  attachReaction: (messageId: string, reaction: string) => Promise<void>;
  isMessageActive: (messageId: string) => Promise<boolean>;
  onReactionError?: (error: unknown) => void;
};

// Call from your existing authenticated, deduplicated message handler.
// The callbacks belong to your app: use your model, database, and UI transport.
export async function handleMessage({
  eventId, messageId, message, agent, context = [], reactions, frequency = "balanced",
  generateReply, sendReply, attachReaction, isMessageActive,
  onReactionError = () => console.error("Reaction failed; reply continues."),
}: ChatInput) {
  const startedAt = Date.now();
  const reactionTask = chooseReaction({
    eventId, message, agent, context, reactions, frequency,
  }).then(async (reaction) => {
    if (reaction === "none" || Date.now() - startedAt > 5000) return;
    if (await isMessageActive(messageId)) {
      await attachReaction(messageId, reaction);
    }
  }).catch(onReactionError);

  const replyTask = Promise.resolve()
    .then(() => generateReply(message, context))
    .then((reply) => sendReply(messageId, reply));

  // Each task delivers independently; await both to keep server work alive.
  const [reactionResult, replyResult] = await Promise.allSettled([
    reactionTask, replyTask,
  ]);
  if (replyResult.status === "rejected") throw replyResult.reason;
  if (reactionResult.status === "rejected") {
    console.error("Reaction error handler failed.");
  }
}

Render the update

Any chat UI or framework can display the result. This React example shows one way to do it; the server code above works with other frontends too. setMessages is your state setter; wire onReactionUpdate to the authenticated transport you already use. It updates the matching message and ignores messages no longer in the view.

React display example
// In your chat component. message.reaction comes from your server update.
const reactionViews = {
  "❤️": { symbol: "❤️", label: "Love" },
  acknowledge: { symbol: "✓", label: "Acknowledged" },
};

function MessageReaction({ reaction }) {
  if (!reaction || reaction === "none") return null;
  const view = Object.hasOwn(reactionViews, reaction)
    ? reactionViews[reaction] : undefined;
  if (!view) return null;
  return <span aria-label={view.label}>{view.symbol}</span>;
}

// In your existing authenticated subscription/event handler:
function onReactionUpdate({ messageId, reaction }) {
  setMessages((messages) => messages.map((message) =>
    message.id === messageId ? { ...message, reaction } : message
  ));
}

Render <MessageReaction reaction={message.reaction} /> beside the original user message bubble. Use your known display mapping rather than interpreting an ID as HTML. A reaction badge should be visually distinct from a delivery status or a task-completion indicator.

Verification

  • With a real API key configured, send a fresh message through the actual chat. Confirm any chosen reaction appears on that user message and the normal reply still completes independently. Mocked responses test your wiring; they don’t verify live API access.
  • "none" creates no reaction. An API error also leaves the UI unchanged, but should be recorded as a failure on your server.
  • Use one saved reaction per message and agent. Duplicate events should update that record, not add another badge.
  • Deduplicate incoming messages and keep a stable input snapshot for retries. Idempotency protects the Emote request, not your UI event delivery.
  • Test a delayed result, a deleted message, a duplicate event, a custom ID, and "none". Never attach a late result to “the latest message.”

API reference · Get an API key