Elements

21 / 122 · Messages

Speaker identity

Who is talking, once a thread holds more than a user and one model.

YouFind out why the converter drops turns.
MaintaineropusSplitting this: one worker reads the converter, one reads the tests.
readerhaikuconvertMessages returns early when parts is empty.
read_file42mspackages/core/src/convertMessages.ts

Installation

npx shadcn@latest add "@assistant-ui/elements-speaker-identity"

Usage

import { SpeakerIdentity } from "@/components/elements/speaker-identity";

<SpeakerIdentity turns={turns} />

Props

SpeakerIdentity

turns*SpeakerTurn[]Turns in order, each naming who produced it.
classNamestringExtra classes merged onto the root.

SpeakerTurn

id*stringStable identity for the row key.
text*stringWhat this speaker said.
kind*"user" | "agent" | "subagent" | "tool"Chooses the badge shape and tone. Subagents get a round badge so they read as delegated.
name*stringDisplay name for the speaker.
detailstringSecondary fact next to the name: the model, a duration.

Source

speaker-identity.tsx
"use client";

import { BotIcon, UserIcon, WrenchIcon } from "lucide-react";
import { cn } from "@/lib/utils";
import { mono } from "./surfaces";

export type SpeakerKind = "user" | "agent" | "subagent" | "tool";

export interface SpeakerTurn {
  id: string;
  kind: SpeakerKind;
  name: string;
  detail?: string;
  text: string;
}

const TONE: Record<SpeakerKind, string> = {
  user: "bg-foreground/[0.06] text-foreground/55",
  agent: "bg-blue-500/12 text-blue-600 dark:bg-blue-400/15 dark:text-blue-400",
  subagent: "bg-foreground/[0.06] text-foreground/45",
  tool: "bg-foreground/[0.04] text-foreground/40",
};

export function SpeakerIdentity({
  turns,
  className,
}: {
  turns: readonly SpeakerTurn[];
  className?: string;
}) {
  return (
    <div className={cn("flex w-full max-w-sm flex-col gap-3.5", className)}>
      {turns.map((turn) => (
        <div key={turn.id} className="flex gap-2.5">
          <span
            className={cn(
              "flex size-6 shrink-0 items-center justify-center rounded-lg",
              TONE[turn.kind],
              turn.kind === "subagent" && "rounded-full",
            )}
          >
            {turn.kind === "user" ? (
              <UserIcon className="size-3" />
            ) : turn.kind === "tool" ? (
              <WrenchIcon className="size-3" />
            ) : (
              <BotIcon className="size-3" />
            )}
          </span>

          <div className="flex min-w-0 flex-1 flex-col gap-0.5">
            <span className="flex items-baseline gap-1.5">
              <span className="text-[13px] font-medium">{turn.name}</span>
              {turn.detail && (
                <span className={cn(mono, "text-foreground/30")}>
                  {turn.detail}
                </span>
              )}
            </span>
            <span className="text-foreground/65 text-[13.5px] leading-relaxed">
              {turn.text}
            </span>
          </div>
        </div>
      ))}
    </div>
  );
}