Voice UI Kit

usePipecatConversation

Hook that derives a clean, ordered conversation stream from RTVI events.

Overview

usePipecatConversation provides access to conversation state managed by the ConversationProvider. The provider is automatically included when using PipecatAppBase and handles all Pipecat RTVI events to produce a clean message list suitable for rendering a chat transcript. It handles streaming text, placeholder messages, finalization, and merging of consecutive messages from the same role.

Setup

The ConversationProvider is automatically included when you use PipecatAppBase, so no additional setup is required:

import { PipecatAppBase, usePipecatConversation } from "@pipecat-ai/voice-ui-kit";

export default function App() {
  return (
    <PipecatAppBase
      transportType="smallwebrtc"
      connectParams={{ webrtcUrl: "/api/offer" }}
    >
      <YourApp />
    </PipecatAppBase>
  );
}

Usage

Basic

import { usePipecatConversation } from "@pipecat-ai/voice-ui-kit";

export function ConversationList() {
  const { messages } = usePipecatConversation();
  return (
    <ul>
      {messages.map((m, i) => (
        <li key={i}>
          <strong>{m.role}:</strong>{" "}
          {m.parts.map((part, idx) => {
            if (part.text && typeof part.text === "object" && "spoken" in part.text) {
              return <span key={idx}>{part.text.spoken + part.text.unspoken}</span>;
            }
            return <span key={idx}>{String(part.text)}</span>;
          })}
        </li>
      ))}
    </ul>
  );
}

With callback

const { messages } = usePipecatConversation({
  onMessageAdded: (message) => console.log("New message:", message),
});

Custom aggregation metadata (inline vs block, spoken vs not spoken)

Only "word" and "sentence" are considered default aggregation types. Any other aggregatedBy value is custom and arbitrary, and should be configured through aggregationMetadata (and optionally botOutputRenderers in your UI components).

This metadata influences:

  • Rendering: whether an aggregation should be treated as inline text or a block (e.g. code blocks)
  • Karaoke-style highlighting: whether the aggregation is expected to be spoken (isSpoken)
const { messages } = usePipecatConversation({
  aggregationMetadata: {
    code: { displayMode: "block", isSpoken: false },
    credit_card: { displayMode: "inline", isSpoken: false },
  },
});

Notes:

  • displayMode: "block" parts are rendered on their own line by MessageContent.
  • For cleaner rendering, non-spoken block parts are trimmed of leading/trailing whitespace before display (the stored text is not modified).

Full example

import React, { useEffect, useRef } from "react";
import { usePipecatConversation } from "@pipecat-ai/voice-ui-kit";

export function ConversationExample() {
  const { messages } = usePipecatConversation();
  const endRef = useRef<HTMLDivElement>(null);

  useEffect(() => {
    endRef.current?.scrollIntoView({ behavior: "smooth" });
  }, [messages]);

  return (
    <div style={{ height: 300, overflow: "auto", fontFamily: "monospace", fontSize: 12 }}>
      {messages.map((m, i) => (
        <div key={i}>
          <strong>[{m.role}]</strong>{" "}
          {m.parts.map((part, idx) => {
            if (part.text && typeof part.text === "object" && "spoken" in part.text) {
              return <span key={idx}>{part.text.spoken + part.text.unspoken}</span>;
            }
            return <span key={idx}>{String(part.text)}</span>;
          })}
          <span style={{ opacity: 0.6, marginLeft: 8 }}>
            {new Date(m.createdAt).toLocaleTimeString()}
          </span>
        </div>
      ))}
      <div ref={endRef} />
    </div>
  );
}

Injecting messages

You can inject messages directly via the hook, or by using ConsoleTemplate callbacks.

import React from "react";
import { usePipecatConversation } from "@pipecat-ai/voice-ui-kit";

export function InjectViaHook() {
  const { injectMessage } = usePipecatConversation();
  return (
    <button
      onClick={() =>
        injectMessage({
          role: "system",
          parts: [{ text: "Hello from the system", final: true, createdAt: new Date().toISOString() }],
          createdAt: new Date().toISOString()
        })
      }
    >
      Inject message
    </button>
  );
}