Skip to main content

Overview

@akapulu/react-ui is the higher-level React UI package in the Akapulu Web SDK. It is built on top of @akapulu/react and provides a prebuilt conversation interface for React apps.

Installation

npm install @akapulu/react-ui

Main export

The main export is AkapuluConversation.
type AkapuluConversationProps = {
  title?: string;
  transcriptFilter?: (entry: TranscriptEntry) => boolean;
  renderTranscriptEntry?: (entry: TranscriptEntry) => ReactNode;
  onToolEvent?: (tool: NormalizedToolEvent) => void;
  renderToolEvent?: (tool: NormalizedToolEvent) => ReactNode;
  toolEventTimeoutMs?: number | null;
  className?: string;
  classes?: Partial<AkapuluConversationClasses>;
  styles?: Partial<AkapuluConversationStyles>;
}

Main responsibility

@akapulu/react-ui is responsible for rendering a ready-made conversation UI with:
  • video layout
  • transcript UI
  • loading and error states
  • current node display
  • tool event surfaces
  • built-in call controls

How it fits in your app

In a typical React app:
  1. AkapuluProvider from @akapulu/react points at your local routes.
  2. Your routes call Akapulu through @akapulu/server.
  3. AkapuluConversation renders the higher-level conversation UI on top of that session state.
See Customize Conversation UI for end-to-end setup (local connect/updates routes, connectBody, custom UI without this package, and post-call APIs).

When to use this package

Use @akapulu/react-ui when you want:
  • the fastest React integration
  • a prebuilt conversation UI
  • styling and rendering customization without building every surface from scratch
If you want complete control over layout and rendering, use @akapulu/react directly.

Minimal example

import { AkapuluProvider } from "@akapulu/react";
import { AkapuluConversation } from "@akapulu/react-ui";

export function App() {
  return (
    <AkapuluProvider
      config={{
        endpoints: {
          connectPath: "/api/akapulu/connect",
          updatesPath: "/api/akapulu/updates",
        },
      }}
    >
      <AkapuluConversation
        title="Akapulu demo"
        transcriptFilter={(entry) => entry.text.trim() !== ""}
        onToolEvent={(tool) => {
          console.log(tool.messageType, tool.functionName);
        }}
      />
    </AkapuluProvider>
  );
}

Styling AkapuluConversation

@akapulu/react-ui supports slot-level customization via:
  • className for the root
  • classes for slot class names
  • styles for slot inline style overrides

Example 1: Slot key overrides (classes + styles)

Use slot keys when you want to target specific UI parts directly from React props.
<AkapuluConversation
  title="Akapulu demo"
  classes={{
    transcriptContainer: "myTranscriptContainer",
    controlEnd: "myLeaveButton",
  }}
  styles={{
    videoPane: { borderRadius: 20 },
    connectedLayout: { gap: "1.5rem" },
  }}
/>

Example 2: Default class overrides (global CSS)

Use built-in default classes when you want to apply theme-like global styles from CSS.
.akapulu-transcript-container {
  border: 1px solid #334155;
  border-radius: 14px;
}

.akapulu-control-end {
  background: rgba(185, 28, 28, 0.9);
}

Example 3: data-slot overrides (stable CSS selectors)

Use data-slot selectors when you want explicit, inspectable selectors in DevTools.
[data-slot="transcript-header"] {
  backdrop-filter: blur(2px);
}

[data-slot="pip"] {
  width: 28%;
  border-color: #60a5fa;
}

Slot map

Each slot has both a default class and a data-slot marker so users can inspect and target it in DevTools without guessing.

Layout

Slot keyWhat it targetsDefault classdata-slot value
containerOuter wrapper for the whole conversation UIakapulu-conversationcontainer
titleTop heading textakapulu-titletitle
connectedLayoutMain connected-state grid wrapperakapulu-connected-layoutconnected-layout
startButtonIdle/error state start-call buttonakapulu-start-buttonstart-button

Loading

Slot keyWhat it targetsDefault classdata-slot value
loadingContainerWrapper for loading state UIakapulu-loading-containerloading-container
loadingSpinnerLoading spinner elementakapulu-loading-spinnerloading-spinner
loadingLabelLoading headline text (e.g. Connecting…)akapulu-loading-labelloading-label
loadingProgressTrackProgress bar background trackakapulu-loading-progress-trackloading-progress-track
loadingProgressFillProgress bar filled portionakapulu-loading-progress-fillloading-progress-fill
loadingStatusTextDetailed status text under progress barakapulu-loading-status-textloading-status-text

Error modal

Slot keyWhat it targetsDefault classdata-slot value
errorModalBackdropFull-screen modal overlay behind error cardakapulu-error-modal-backdroperror-modal-backdrop
errorModalCardError modal content cardakapulu-error-modal-carderror-modal-card

Tool events

Slot keyWhat it targetsDefault classdata-slot value
toolToastFloating toast/card for tool activityakapulu-tool-toasttool-toast

Video + controls

Slot keyWhat it targetsDefault classdata-slot value
videoPaneVideo column containerakapulu-video-panevideo-pane
videoSurfacePrimary remote/bot video surfaceakapulu-video-surfacevideo-surface
botStateBadgeSpeaking/listening/idle badge on videoakapulu-bot-state-badgebot-state-badge
pipLocal picture-in-picture preview tileakapulu-pippip
waitingVideoPlaceholder shown before video is availableakapulu-waiting-videowaiting-video
controlMicIn-call mic button (video mode)akapulu-control-miccontrol-mic
controlCamIn-call camera button (video mode)akapulu-control-camcontrol-cam
controlEndIn-call hang-up button (video mode)akapulu-control-endcontrol-end

Transcript

Slot keyWhat it targetsDefault classdata-slot value
transcriptPaneTranscript column containerakapulu-transcript-panetranscript-pane
transcriptContainerScrollable transcript boxakapulu-transcript-containertranscript-container
transcriptHeaderTranscript header rowakapulu-transcript-headertranscript-header
nodeChipCurrent node chip in transcript headerakapulu-node-chipnode-chip
transcriptRowUserUser transcript row/bubbleakapulu-transcript-row-usertranscript-row-user
transcriptRowBotBot transcript row/bubbleakapulu-transcript-row-bottranscript-row-bot

Behavior customization (handlers + custom elements)

Beyond styles, @akapulu/react-ui lets you customize behavior and rendering for transcript and tool events directly on AkapuluConversation.

Built-in handler props on AkapuluConversation

  • transcriptFilter(entry) to hide transcript rows
  • renderTranscriptEntry(entry) to render transcript rows with your own JSX
  • onToolEvent(tool) to run side effects when a tool event arrives
  • renderToolEvent(tool) to replace the default tool toast element
  • toolEventTimeoutMs to control how long the tool toast stays visible
Callback signatures:
transcriptFilter?: (entry: TranscriptEntry) => boolean;
renderTranscriptEntry?: (entry: TranscriptEntry) => ReactNode;
onToolEvent?: (tool: NormalizedToolEvent) => void;
renderToolEvent?: (tool: NormalizedToolEvent) => ReactNode;
toolEventTimeoutMs?: number | null;
toolEventTimeoutMs behavior:
  • default: 4000
  • pass a number to customize the auto-hide timeout
  • pass null to disable auto-hide
TranscriptEntry shape (entry):
FieldTypeDescription
idstringStable transcript row id
speaker"user" | "bot"Who produced this transcript chunk
textstringTranscript text
timestampstringEvent timestamp
isFinalbooleanWhether this transcript row is finalized
NormalizedToolEvent shape (tool) by tool type:

RAG tool event

FieldTypeDescription
messageType"RAG"RAG tool event
functionNamestringRAG function/tool name
summarystring"RAG tool called"
querystring | undefinedQuery text

vision tool event

FieldTypeDescription
messageType"vision"Vision tool event
functionNamestringVision function/tool name
summarystring"Vision tool called"

http tool event

FieldTypeDescription
messageType"http"HTTP tool event
functionNamestringHTTP function/tool name
summarystring"HTTP endpoint called"
argsJsonstring | undefinedPretty JSON string of body
bodyRecord<string, unknown>Outgoing HTTP request payload fields
import { AkapuluProvider } from "@akapulu/react";
import { AkapuluConversation } from "@akapulu/react-ui";

export function App() {
  return (
    <AkapuluProvider
      config={{
        endpoints: {
          connectPath: "/api/akapulu/connect",
          updatesPath: "/api/akapulu/updates",
        },
      }}
    >
      <AkapuluConversation
        title="Akapulu demo"
        transcriptFilter={(entry) => {
          // Example: skip rows you do not want in the transcript list
          return entry.text.trim() !== "skip me";
        }}
        renderTranscriptEntry={(entry) => (
          <div>
            <strong>{entry.speaker === "user" ? "You" : "Bot"}:</strong> {entry.text}
          </div>
        )}
        onToolEvent={(tool) => {
          // Example: analytics side effect
          console.log("tool_event", tool.messageType, tool.functionName);
        }}
        renderToolEvent={(tool) => (
          <div style={{ border: "1px solid #334155", borderRadius: 8, padding: 12 }}>
            <strong>{tool.summary}</strong>
            <div style={{ marginTop: 6, opacity: 0.85 }}>{tool.functionName}</div>
          </div>
        )}
      />
    </AkapuluProvider>
  );
}

Handling all conversation events while keeping prebuilt UI

For full event handling (node changes, bot speaking state, transcript updates, tool calls, and timeout), add a small sibling listener component that uses useAkapuluEvents from @akapulu/react.
import { AkapuluProvider, useAkapuluEvents } from "@akapulu/react";
import { AkapuluConversation } from "@akapulu/react-ui";

function ConversationEventListener() {
  useAkapuluEvents((event) => {
    // … handle `event` (discriminated by `event.type`; see schema below)
  });
  return null;
}

export function App() {
  return (
    <AkapuluProvider
      config={{
        endpoints: {
          connectPath: "/api/akapulu/connect",
          updatesPath: "/api/akapulu/updates",
        },
      }}
    >
      <AkapuluConversation title="Akapulu demo" />
      <ConversationEventListener />
    </AkapuluProvider>
  );
}
Event schema by event.type:

status_changed

FieldTypeDescription
type"status_changed"Discriminator
status"idle" | "connecting" | "connected" | "disconnecting" | "ended" | "error"Session lifecycle state

bot_speaking_state_changed

FieldTypeDescription
type"bot_speaking_state_changed"Discriminator
speakingState"idle" | "speaking" | "listening"Bot speaking/listening state

node_changed

FieldTypeDescription
type"node_changed"Discriminator
node{ key: string; label: string } | nullCurrent flow node

tool_event

FieldTypeDescription
type"tool_event"Discriminator
toolNormalizedToolEventNormalized tool event payload

transcript_updated

FieldTypeDescription
type"transcript_updated"Discriminator
transcriptTranscriptEntryUpdated transcript row

call_ready

FieldTypeDescription
type"call_ready"Call reached ready state

timeout

FieldTypeDescription
type"timeout"Discriminator
reason"duration_limit_reached" | "participant_join_timeout"Timeout reason from backend
import { AkapuluProvider, useAkapuluEvents } from "@akapulu/react";
import { AkapuluConversation } from "@akapulu/react-ui";

function ConversationEventBridge() {
  useAkapuluEvents((event) => {
    if (event.type === "transcript_updated") {
      // event.transcript
      return;
    }
    if (event.type === "bot_speaking_state_changed") {
      // event.speakingState: "idle" | "speaking" | "listening"
      return;
    }
    if (event.type === "node_changed") {
      // event.node
      return;
    }
    if (event.type === "tool_event") {
      // event.tool
      return;
    }
    if (event.type === "timeout") {
      // event.reason
      return;
    }
  });

  return null;
}

export function App() {
  return (
    <AkapuluProvider
      config={{
        endpoints: {
          connectPath: "/api/akapulu/connect",
          updatesPath: "/api/akapulu/updates",
        },
      }}
    >
      <ConversationEventBridge />
      <AkapuluConversation title="Akapulu demo" />
    </AkapuluProvider>
  );
}
See Callbacks and events for more context on the event stream.