Info:
This is release candidate documentation for Stream Chat React SDK v15. For the latest stable version, see the latest version (v14).

AI hooks

These hooks build AI chat UI: what the assistant is doing, its reply typed out as it streams in, and voice input. useAIState and useMessageTextStreaming are exported from stream-chat-react. useSpeechToText is exported from stream-chat-react/ai-components, the entry point of the AI components.

How the AI states, the streamed reply and the AI components fit together is on AI integrations overview, and the components of the main entry point on SDK integration.

Each entry gives the signature, then:

  • Returns: what the hook gives back.
  • Reads: the store or the context it reads.
  • Re-renders: what makes the component render again.
  • Needs: where the component must sit, and what the hook does outside that place.

useAIState

Returns what the AI is doing in a channel.

useAIState(channel?: Channel): { aiState: AIState }
  • Returns: { aiState }, one of the AIStates: AI_STATE_IDLE, AI_STATE_THINKING, AI_STATE_EXTERNAL_SOURCES, AI_STATE_GENERATING, AI_STATE_ERROR or AI_STATE_STOP, or a state of your own that your backend sends. AI_STATE_IDLE without a channel.
  • Reads: channel.state, field aiState. The client sets it from the ai_indicator.update, ai_indicator.clear and ai_indicator.stop events.
  • Re-renders: when aiState changes, or the hook gets another channel.
  • Needs: nothing. It doesn't read the channel from context: pass it, for example from useChannel(). Without a channel it subscribes to nothing and returns AI_STATE_IDLE.
import { AIStates } from "stream-chat";
import { useAIState, useChannel } from "stream-chat-react";

export const ThinkingIndicator = () => {
  const channel = useChannel();
  const { aiState } = useAIState(channel);

  if (aiState !== AIStates.Thinking) return null;
  return <p className="thinking">The assistant is thinking…</p>;
};

Render it inside Channel. AIStateIndicator and the composer's stop button read the same state. Drive the AI indicator from events explains which state follows which event, and Let users stop the response builds a stop button on it.

useMessageTextStreaming

Types a text out a few letters at a time, the way an AI reply appears as it streams in.

useMessageTextStreaming(params: UseMessageTextStreamingProps): {
  readonly skipAnimation: () => void;
  readonly streamedMessageText: string;
}

The params:

  • text: the full text so far, such as the message's text. Required.

  • streamingLetterIntervalMs: the time between two steps, 30 ms by default.

  • renderingLetterCount: the letters added at each step, 2 by default.

  • Returns:

    • streamedMessageText: the part of text typed out so far.
    • skipAnimation(): shows the whole text at once.
  • Reads: no store and no context. It runs its own timer.

  • Re-renders: at each step until it reaches the end of text, and when text changes.

  • Needs: nothing.

How the typing follows text:

  1. The text there is when the component mounts shows at once. Only text added later is typed out.
  2. When text grows, typing goes on from where it was.
  3. When text gets shorter than what's on screen, the old text stays until skipAnimation() is called or the component mounts again.

StreamedMessageText renders AI messages with it: the ones Chat's isMessageAIGenerated marks. It also calls skipAnimation() when an ai_indicator.stop event reaches the channel. StreamingMessage, from stream-chat-react/ai-components, uses the hook too, and names the interval letterIntervalMs.

To change only the speed, give the default component other values: <StreamedMessageText {...props} streamingLetterIntervalMs={15} />. To render the text your own way, call the hook in a StreamedMessageText of your own:

import type { PropsWithChildren } from "react";
import {
  MessageText,
  useMessageContext,
  useMessageTextStreaming,
  WithComponents,
} from "stream-chat-react";
import type { StreamedMessageTextProps } from "stream-chat-react";

const TypedMessageText = ({
  message: messageFromProps,
  renderText,
}: StreamedMessageTextProps) => {
  const { message: messageFromContext } = useMessageContext();
  const message = messageFromProps ?? messageFromContext;
  const text = message.text ?? "";
  const { skipAnimation, streamedMessageText } = useMessageTextStreaming({
    renderingLetterCount: 4,
    streamingLetterIntervalMs: 15,
    text,
  });

  return (
    <div className="typed-message">
      <MessageText
        message={{ ...message, text: streamedMessageText }}
        renderText={renderText}
      />
      {streamedMessageText.length < text.length && (
        <button onClick={skipAnimation} type="button">
          Show all
        </button>
      )}
    </div>
  );
};

const overrides = { StreamedMessageText: TypedMessageText };

export const WithTypedAIMessages = ({ children }: PropsWithChildren) => (
  <WithComponents overrides={overrides}>{children}</WithComponents>
);

Show all appears while the text is still being typed out, and shows the rest of it at once.

useSpeechToText

Turns speech into text with the browser's Web Speech API. Import it from stream-chat-react/ai-components.

useSpeechToText(options?: UseSpeechToTextOptions): {
  readonly isListening: boolean;
  readonly isSupported: boolean;
  readonly startListening: () => void;
  readonly stopListening: () => void;
}

The options, each optional:

  • onTranscript(text): called with the transcript each time the browser reports a result. With interimResults, that includes the words so far while the user speaks.

  • onError(message): called with an English description of the error, such as "No speech detected. Please try again."

  • lang: the language to recognize, 'en-US' by default.

  • interimResults: report partial results, true by default.

  • continuous: keep listening after the user stops speaking, false by default.

  • maxAlternatives: the most alternatives the browser returns for each result, 1 by default. The hook uses the first.

  • Returns:

    • isSupported: whether the browser has speech recognition. Always false during server rendering.
    • isListening: whether recognition is running.
    • startListening(): starts listening. It throws when isSupported is false, and calls onError when the browser refuses to start.
    • stopListening(): stops listening.
  • Reads: no store and no context. It holds one recognizer, created again when lang, interimResults, continuous or maxAlternatives changes.

  • Re-renders: when listening starts or stops.

  • Needs: nothing.

onTranscript and onError may be inline functions: the hook always calls the latest ones. AIMessageComposer.SpeechToTextButton is built on this hook. A dictation button for the default composer replaces its text with the transcript:

import { useMessageComposerController } from "stream-chat-react";
import { useSpeechToText } from "stream-chat-react/ai-components";

export const DictationButton = () => {
  const messageComposer = useMessageComposerController();
  const { isListening, isSupported, startListening, stopListening } =
    useSpeechToText({
      onTranscript: (text) => messageComposer.textComposer.setText(text),
    });

  if (!isSupported) return null;

  return (
    <button
      aria-pressed={isListening}
      onClick={isListening ? stopListening : startListening}
      type="button"
    >
      {isListening ? "Stop dictation" : "Dictate"}
    </button>
  );
};

With server rendering, render the button only after the component has mounted, as SpeechToTextButton does: isSupported is false on the server and can be true in the browser.

Best practices

  • Pass the channel to useAIState. It doesn't read one from context.
  • Type out only the messages isMessageAIGenerated marks, as MessageUI does.
  • Give a component that types out text a new key when it shows another message. The hook keeps the old text when text gets shorter.
  • Call skipAnimation() when the response stops, as StreamedMessageText does on ai_indicator.stop.