Chapter 19 · Operator · Transcript surface

Conversation. The transcript.

The operator-facing chat surface. Six components compose the transcript end-to-end — message, transcript, composer, trace-back, summary banner, live tester. Together they’re how an operator inspects, replays, and tests an agent conversation.

19.1 ChatMessage

The atom of the conversation surface. One bubble — agent, user, or system. The from prop drives alignment, bubble fill, and the position of avatars and reactions; the optional status, confidence, timestamp, and actions slots layer on top without changing the basic shape.

ChatMessage

.chat-msg

Five variants side by side — agent with name and timestamp, user with timestamp, agent streaming, system pill, and a tool-call agent message with no meta and a trace-button in the actions slot. The same primitive shape carries the whole transcript.

agent · with meta
Charlie’s Wines agent 14:02
Hey Jay — looking for the secret deals?
user · with timestamp
14:02
Yes please.
agent · streaming
Looking that up for you
system · pill
Block transition · Hook → Align 14:03
agent · tool call + actions
Pulled three matching offers from the regional catalogue.
<!-- One row per variant. .is-from-* drives alignment + bubble fill; -->
<!-- the inner shape (avatar, stack, meta, bubble) stays the same.   -->
<div class="chat-msg is-from-agent">
  <div class="chat-msg-avatar" aria-hidden="true">CW</div>
  <div class="chat-msg-stack">
    <div class="chat-msg-meta">
      <span class="chat-msg-name">Charlie’s Wines agent</span>
      <span class="chat-msg-time">14:02</span>
    </div>
    <div class="chat-msg-bubble-wrap">
      <div class="chat-msg-bubble">
        <div class="chat-msg-body">Hey Jay…</div>
      </div>
    </div>
  </div>
</div>

<!-- Streaming adds .is-status-streaming and a <span.chat-msg-caret> -->
<div class="chat-msg is-from-agent is-status-streaming">…
  <div class="chat-msg-bubble">
    <div class="chat-msg-body">Looking that up for you</div>
    <span class="chat-msg-caret" aria-hidden="true"></span>
  </div>
</div>

<!-- System pill — centred note, role="status". -->
<div class="chat-msg is-from-system" role="status">
  <div class="chat-msg-system-line">
    Block transition · Hook → Align
    <span class="chat-msg-system-time">14:03</span>
  </div>
</div>
.chat-msg {
  display: grid;
  grid-template-columns: 32px 1fr;
  gap: var(--s-3);
  margin: 0;
  padding: 0 var(--s-4);
}

.chat-msg.is-compact {
  grid-template-columns: 24px 1fr;
  gap: var(--s-2);
}

.chat-msg + .chat-msg { margin-top: var(--s-4); }

.chat-msg.is-compact + .chat-msg.is-compact { margin-top: var(--s-2); }

.chat-msg.is-from-user {
  grid-template-columns: 1fr 32px;
}

.chat-msg.is-from-user.is-compact {
  grid-template-columns: 1fr 24px;
}

.chat-msg.is-from-user .chat-msg-avatar { grid-column: 2; }

.chat-msg.is-from-user .chat-msg-stack { grid-column: 1; align-items: flex-end; }

.chat-msg-avatar {
  width: 32px;
  height: 32px;
  border-radius: 999px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: var(--bg-warm);
  color: var(--fg);
  font: 600 12px/1 var(--f-mono);
  flex-shrink: 0;
  overflow: hidden;
}

.chat-msg.is-compact .chat-msg-avatar {
  width: 24px;
  height: 24px;
  font-size: 10.5px;
}

.chat-msg.is-from-agent .chat-msg-avatar {
  background: color-mix(in oklab, var(--accent) 18%, var(--bg-warm));
  color: var(--accent);
}

.chat-msg.is-from-user .chat-msg-avatar {
  background: var(--ink);
  color: var(--paper);
}

.chat-msg-stack {
  display: flex;
  flex-direction: column;
  gap: 4px;
  min-width: 0;
}

.chat-msg-meta {
  display: inline-flex;
  align-items: baseline;
  gap: 6px;
  color: var(--fg-soft);
  font: 400 11.5px/1 var(--f-body);
}

.chat-msg.is-from-user .chat-msg-meta { flex-direction: row-reverse; }

.chat-msg-name {
  font: 500 12.5px/1 var(--f-body);
  color: var(--fg);
}

.chat-msg-time {
  font: 400 11px/1 var(--f-mono);
  color: var(--fg-faint);
  font-variant-numeric: tabular-nums;
}

.chat-msg-confidence {
  width: 8px;
  height: 8px;
  border-radius: 999px;
  background: var(--fg-faint);
  display: inline-block;
}

.chat-msg-confidence.is-confidence-high {
  background: var(--success-text, #1A6A3F);
}

.chat-msg-confidence.is-confidence-medium {
  background: var(--warning, #F9AD03);
}

.chat-msg-confidence.is-confidence-low {
  background: var(--error-text, #8B2417);
}

.chat-msg-bubble-wrap {
  position: relative;
  display: inline-flex;
  max-width: min(560px, 80%);
}

.chat-msg.is-compact .chat-msg-bubble-wrap {
  max-width: min(480px, 86%);
}

.chat-msg.is-from-user .chat-msg-bubble-wrap { align-self: flex-end; }

/* …additional rules trimmed for brevity — see _shared.css */
import { ChatMessage, MessageTraceButton } from '@magicblocksai/ui';

// agent — with name + timestamp
<ChatMessage from="agent" name="Charlie’s Wines agent" avatar="CW" timestamp="14:02">
  Hey Jay — looking for the secret deals?
</ChatMessage>

// user — timestamp only, no name
<ChatMessage from="user" avatar="JS" timestamp="14:02">
  Yes please.
</ChatMessage>

// agent — streaming. The blinking caret is rendered automatically.
<ChatMessage from="agent" avatar="CW" status="streaming">
  Looking that up for you
</ChatMessage>

// system — centred pill, no avatar, role="status"
<ChatMessage from="system" timestamp="14:03">
  Block transition · Hook → Align
</ChatMessage>

// agent — with a per-message trace button in the actions slot
<ChatMessage
  from="agent"
  avatar="SA"
  actions={<MessageTraceButton noteworthy count={3} onClick={openTrace} />}
>
  Pulled three matching offers from the regional catalogue.
</ChatMessage>

19.2 ChatTranscript

The conversation viewer shell — sticky header, scrollable message area, pinned footer. Wraps a stack of <ChatMessage> children. New messages auto-scroll the view to the bottom only when the operator is already there; if they’ve scrolled up to read earlier turns, new arrivals don’t yank the view.

ChatTranscript

.chat-transcript

A six-message exchange between an operator-side customer-support agent and a customer asking for a password reset. Header carries the agent name and a live badge; the message stack groups agent + user turns; the footer slot holds the composer.

Spark CRM · support Live
Support agent 09:14
Hi Hoang — what can I help with this morning?
09:14
Hi — my password reset email never arrived. I tried twice last night.
Support agent 09:15
Let me check the delivery log. One moment.
Tool call · mailgun.search 09:15
Support agent 09:16
Found it — both attempts hit a Gmail spam filter at your domain. I’ve resent through our backup route. It should arrive in under a minute.
09:17
Got it. Thank you!
<!-- The transcript is three slots: header, scroll, footer.        -->
<!-- The scroll area carries role="log" + aria-live="polite" so    -->
<!-- screen-reader users hear newly-arrived messages.              -->
<div class="chat-transcript">
  <div class="chat-transcript-header">
    <strong>Spark CRM · support</strong>
    <span class="badge tone-success">Live</span>
  </div>
  <div class="chat-transcript-scroll" role="log" aria-live="polite" aria-relevant="additions">
    <div class="chat-transcript-list">
      <div class="chat-msg is-from-agent">…</div>
      <div class="chat-msg is-from-user">…</div>
      <!-- …four more messages… -->
    </div>
  </div>
  <div class="chat-transcript-footer">
    <form class="chat-composer">…</form>
  </div>
</div>
.chat-transcript {
  display: grid;
  grid-template-rows: auto 1fr auto;
  height: 100%;
  min-height: 320px;
  background: var(--bg-paper);
  border: 1px solid var(--hair);
  border-radius: var(--r-md);
  overflow: hidden;
}

.chat-transcript.is-compact { font-size: 12.5px; }

.chat-transcript-header {
  padding: var(--s-3) var(--s-4);
  border-bottom: 1px solid var(--hair-soft);
  background: var(--bg-paper);
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s-3);
}

.chat-transcript-scroll {
  overflow-y: auto;
  overflow-x: hidden;
  padding: var(--s-4) 0;
  scroll-behavior: smooth;
}

@media (prefers-reduced-motion: reduce) {
  .chat-transcript-scroll { scroll-behavior: auto; }
}

.chat-transcript-list {
  display: flex;
  flex-direction: column;
  gap: 0;
}

.chat-transcript-footer {
  border-top: 1px solid var(--hair-soft);
  padding: var(--s-3) var(--s-4);
  background: var(--bg-paper);
}

.chat-transcript-skeleton {
  display: flex;
  flex-direction: column;
  gap: var(--s-3);
  padding: 0 var(--s-4);
}

.chat-transcript-skeleton-row {
  height: 36px;
  border-radius: 14px;
  background: color-mix(in oklab, var(--bg-warm) 50%, var(--bg-paper));
  max-width: 60%;
  animation: chat-transcript-skel-pulse 1.5s ease-in-out infinite;
}

.chat-transcript-skeleton-row.is-user {
  align-self: flex-end;
  max-width: 50%;
}

.chat-transcript-skeleton-row.is-short { max-width: 40%; }

@media (prefers-reduced-motion: reduce) {
  .chat-transcript-skeleton-row { animation: none; opacity: 0.7; }
}

.chat-transcript-empty {
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--s-6) var(--s-4);
  color: var(--fg-soft);
  font: 400 13px/1.5 var(--f-body);
  text-align: center;
}

.live-chat-tester .chat-transcript {
  height: 100%;
  min-height: 360px;
}
import {
  ChatTranscript,
  ChatMessage,
  ChatComposer,
  Badge,
} from '@magicblocksai/ui';

<ChatTranscript
  header={
    <>
      <strong>Spark CRM · support</strong>
      <Badge tone="success">Live</Badge>
    </>
  }
  footer={<ChatComposer placeholder="Reply to Hoang…" onSubmit={send} />}
>
  <ChatMessage from="agent" name="Support agent" avatar="SP" timestamp="09:14">
    Hi Hoang — what can I help with this morning?
  </ChatMessage>
  <ChatMessage from="user" avatar="HT" timestamp="09:14">
    Hi — my password reset email never arrived. I tried twice last night.
  </ChatMessage>
  <ChatMessage from="agent" name="Support agent" avatar="SP" timestamp="09:15">
    Let me check the delivery log. One moment.
  </ChatMessage>
  <ChatMessage from="system" timestamp="09:15">
    Tool call · mailgun.search
  </ChatMessage>
  <ChatMessage from="agent" name="Support agent" avatar="SP" timestamp="09:16">
    Found it — both attempts hit a Gmail spam filter at your domain.
    I've resent through our backup route. It should arrive in under a minute.
  </ChatMessage>
  <ChatMessage from="user" avatar="HT" timestamp="09:17">
    Got it. Thank you!
  </ChatMessage>
</ChatTranscript>

19.3 ChatComposer

An autogrowing textarea + Send button, designed to live inside <ChatTranscript footer={…}> but works standalone. Submits on Enter; Shift+Enter inserts a newline. The composer does not clear itself — the consumer owns the value and decides when to clear (typically after the send resolves).

ChatComposer

.chat-composer

Three states side by side: default (empty, Send disabled), with attachment chips visible in the attachments slot, and the sending busy state (textarea disabled, Send button shows a spinner).

default · empty
with attachments
policy.pdf screenshot.png
sending · busy
<!-- Default — Send disabled until the textarea is non-empty. -->
<form class="chat-composer">
  <div class="chat-composer-row">
    <textarea class="chat-composer-input" rows="1" placeholder="Type a message…" aria-label="Message"></textarea>
    <button type="submit" class="chat-composer-send" disabled aria-label="Send">
      <span class="chat-composer-send-label">Send</span>
    </button>
  </div>
</form>

<!-- With attachments — chips in the left-side slot. -->
<form class="chat-composer">
  <div class="chat-composer-row">
    <div class="chat-composer-attachments">
      <span class="chat-attach-chip">policy.pdf</span>
      <span class="chat-attach-chip">screenshot.png</span>
    </div>
    <textarea class="chat-composer-input" rows="1" placeholder="Add a note…" aria-label="Message"></textarea>
    <button type="submit" class="chat-composer-send" disabled aria-label="Send">…</button>
  </div>
</form>

<!-- Sending — textarea disabled, Send shows spinner. -->
<form class="chat-composer is-sending">
  <div class="chat-composer-row">
    <textarea class="chat-composer-input" rows="1" disabled>Pulling that record now…</textarea>
    <button type="submit" class="chat-composer-send" disabled aria-label="Send">
      <span class="chat-composer-spinner" aria-hidden="true"></span>
    </button>
  </div>
</form>
.chat-composer { display: flex; flex-direction: column; gap: 4px; }

.chat-composer.is-disabled { opacity: 0.55; pointer-events: none; }

.chat-composer-row {
  display: flex;
  align-items: flex-end;
  gap: 6px;
  padding: 6px 6px 6px 10px;
  border: 1px solid var(--hair);
  border-radius: var(--r-md);
  background: var(--bg-paper);
  transition: border-color var(--dur-2) var(--ease),
              box-shadow var(--dur-2) var(--ease);
}

.chat-composer-row:focus-within {
  border-color: var(--accent);
  box-shadow: 0 0 0 2px color-mix(in oklab, var(--accent) 25%, transparent);
}

.chat-composer-attachments,
.chat-composer-actions {
  display: inline-flex;
  align-items: center;
  gap: 2px;
}

.chat-composer-input {
  flex: 1;
  appearance: none;
  background: transparent;
  border: 0;
  outline: none;
  resize: none;
  padding: 6px 0;
  min-height: 24px;
  font: 400 14px/1.4 var(--f-body);
  color: var(--fg);
  overflow-y: hidden;
}

.chat-composer-input::placeholder { color: var(--fg-faint); }

.chat-composer-send {
  appearance: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 4px;
  height: 32px;
  padding: 0 12px;
  border: 0;
  border-radius: var(--r-sm);
  background: var(--accent);
  color: var(--paper);
  font: 500 13px/1 var(--f-body);
  cursor: pointer;
  transition: filter var(--dur-2) var(--ease);
}

.chat-composer-send:hover:not(:disabled) { filter: brightness(0.95); }

.chat-composer-send:disabled {
  background: var(--bg-warm);
  color: var(--fg-faint);
  cursor: not-allowed;
}

.chat-composer-send-label { line-height: 1; }

.chat-composer-spinner {
  width: 14px;
  height: 14px;
  border-radius: 999px;
  border: 1.5px solid currentColor;
  border-right-color: transparent;
  animation: chat-composer-spin 0.7s linear infinite;
}

@media (prefers-reduced-motion: reduce) {
  .chat-composer-spinner { animation: none; opacity: 0.5; }
}

.chat-composer-helper {
  font: 400 11.5px/1.4 var(--f-body);
  color: var(--fg-faint);
  padding: 0 var(--s-2);
}
import { ChatComposer, IconButton } from '@magicblocksai/ui';

// default — empty, Send disabled until input is non-empty
<ChatComposer placeholder="Type a message…" onSubmit={send} />

// with attachment chips — pass anything into the `attachments` slot
<ChatComposer
  placeholder="Add a note for these files…"
  attachments={
    <>
      <span className="chat-attach-chip">policy.pdf</span>
      <span className="chat-attach-chip">screenshot.png</span>
    </>
  }
  onSubmit={send}
/>

// sending — textarea disabled, Send shows spinner
<ChatComposer
  value="Pulling that record now…"
  sending
  disabled
  onSubmit={send}
/>

19.4 MessageTraceButton

The per-message robot-head icon. Consumers drop it into a <ChatMessage actions={…}> slot to open the trace timeline for that specific turn. The optional dot makes noteworthy messages announce themselves — a block transition, an action firing, a guardrail hitting. Operators learn to hunt for the dot first.

MessageTraceButton

.message-trace-button

The button on its own, plus an inline-context view showing where it lives in practice — on the top-right of an agent message, accompanied by the dot indicator for a turn that fired an action.

standalone · with count + dot
in context · agent message
Sales Agent 14:05
Sure — we have a 15% off code for new customers: WELCOME15. Want me to text it?
<!-- Stand-alone. Add .is-noteworthy to render the indicator dot.   -->
<!-- Optional .message-trace-button-count renders a number badge.   -->
<button type="button" class="message-trace-button is-size-sm is-noteworthy"
        aria-label="View trace" title="View trace">
  <!-- inline 14×14 robot-head SVG -->
  <span class="message-trace-button-count" aria-label="3 events">3</span>
  <span class="message-trace-button-dot" aria-hidden="true"></span>
</button>

<!-- Drop it into a ChatMessage actions slot for inline context. -->
<div class="chat-msg is-from-agent">
  <!-- …avatar + stack… -->
  <div class="chat-msg-bubble-wrap">
    <div class="chat-msg-bubble"><!-- message body --></div>
    <div class="chat-msg-actions" role="group" aria-label="Message actions">
      <button class="message-trace-button is-size-sm is-noteworthy">…</button>
    </div>
  </div>
</div>
.message-trace-button {
  appearance: none;
  position: relative;
  display: inline-flex;
  align-items: center;
  gap: 4px;
  padding: 4px 6px;
  background: transparent;
  border: 0;
  border-radius: var(--r-xs);
  cursor: pointer;
  color: var(--fg-soft);
  transition: color var(--dur-2) var(--ease),
              background var(--dur-2) var(--ease);
}

.message-trace-button:hover {
  color: var(--fg);
  background: var(--bg-warm);
}

.message-trace-button:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 1px;
}

.message-trace-button.is-size-xs { padding: 2px 4px; }

.message-trace-button.is-size-md {
  padding: 6px 10px;
  font: 500 12.5px/1 var(--f-body);
}

.message-trace-button-label {
  font: 500 12px/1 var(--f-body);
  line-height: 1.1;
}

.message-trace-button-count {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 16px;
  padding: 0 5px;
  height: 16px;
  border-radius: 999px;
  background: var(--bg-warm);
  color: var(--fg);
  font: 600 10.5px/1 var(--f-mono);
  font-variant-numeric: tabular-nums;
}

.message-trace-button.is-noteworthy { color: var(--accent); }

.message-trace-button-dot {
  position: absolute;
  top: 3px;
  right: 3px;
  width: 7px;
  height: 7px;
  border-radius: 999px;
  background: var(--accent);
  border: 1.5px solid var(--bg-paper);
}
import { ChatMessage, MessageTraceButton } from '@magicblocksai/ui';

// Stand-alone — three events fired, dot indicator on.
<MessageTraceButton noteworthy count={3} tooltipLabel="View trace" onClick={openTrace} />

// In context — the canonical use, dropped into ChatMessage actions slot.
<ChatMessage
  from="agent"
  name="Sales Agent"
  avatar="SA"
  timestamp="14:05"
  actions={
    <MessageTraceButton
      noteworthy
      count={3}
      tooltipLabel="3 events fired — view trace"
      onClick={() => openTraceFor('msg_42')}
    />
  }
>
  Sure — we have a 15% off code for new customers: WELCOME15. Want me to text it?
</ChatMessage>

19.5 SummaryBanner

The collapsible AI-generated TL;DR card — the kit’s standard “here’s a summary of this conversation” banner that sits above a transcript. Defaults to expanded; the chevron in the header toggles. Pair with <TraceTimeline> below to give operators a one-glance read-out before they dive into the per-turn detail.

SummaryBanner

.summary-banner

Three side-by-side variants — the default neutral treatment, the accented pink-rail treatment used on AI-content surfaces, and the static hideToggle variant that always shows the body. Each carries a short headline and a one-line body.

default · expanded
Conversation summary Updated 3 minutes ago.

Hoang reported a missing password-reset email. The agent ran a Mailgun search, found both attempts in a Gmail spam route, and resent via the backup channel.

accent · AI surfaces
Today’s agent changes Three edits since yesterday.

Switched the Hook persona to “Friendly mortgage rep”. Added two key facts to Qualify. Removed the Embed action.

static · hideToggle
Pending review Awaiting approval from the duty manager.

Two flagged turns from the overnight queue need a human eye before the agent ships its weekly digest.

<!-- Default — chevron toggles open/closed. .is-open keeps body visible. -->
<section class="summary-banner is-open" aria-labelledby="sb-1">
  <header class="summary-banner-head">
    <span class="summary-banner-icon" aria-hidden="true"><!-- sparkle SVG --></span>
    <div class="summary-banner-title-block">
      <span id="sb-1" class="summary-banner-title">Conversation summary</span>
      <span class="summary-banner-caption">Updated 3 minutes ago.</span>
    </div>
    <button type="button" class="summary-banner-toggle" aria-expanded="true"
            aria-controls="sb-1-body" aria-label="Collapse summary">
      <span class="summary-banner-chevron" aria-hidden="true"></span>
    </button>
  </header>
  <div id="sb-1-body" class="summary-banner-body">
    <p>Hoang reported a missing password-reset email…</p>
  </div>
</section>

<!-- Accent — pink left rail, the AI-content treatment.       -->
<section class="summary-banner is-accent is-open">…</section>

<!-- Static — no chevron; body always visible.                -->
<section class="summary-banner is-open is-static">…</section>
.summary-banner {
  display: flex;
  flex-direction: column;
  background: var(--bg-paper);
  border: 1px solid var(--hair);
  border-radius: var(--r-md);
  overflow: hidden;
  position: relative;
}

.summary-banner.is-accent {
  border-left: 3px solid var(--accent);
}

.summary-banner-head {
  display: grid;
  grid-template-columns: auto 1fr auto auto;
  align-items: center;
  gap: var(--s-3);
  padding: var(--s-3) var(--s-4);
}

.summary-banner-icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 24px;
  height: 24px;
  border-radius: var(--r-xs);
  background: color-mix(in oklab, var(--accent) 12%, var(--bg-paper));
  color: var(--accent);
}

.summary-banner-title-block {
  display: flex;
  flex-direction: column;
  gap: 1px;
  min-width: 0;
}

.summary-banner-title {
  font: 500 13.5px/1.3 var(--f-body);
  color: var(--fg);
}

.summary-banner-caption {
  font: 400 12px/1.4 var(--f-body);
  color: var(--fg-soft);
}

.summary-banner-meta {
  display: inline-flex;
  align-items: center;
  gap: var(--s-2);
}

.summary-banner-toggle {
  appearance: none;
  background: transparent;
  border: 0;
  width: 28px;
  height: 28px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  color: var(--fg-soft);
  cursor: pointer;
  border-radius: var(--r-xs);
}

.summary-banner-toggle:hover {
  color: var(--fg);
  background: var(--bg-warm);
}

.summary-banner-chevron {
  width: 10px;
  height: 10px;
  position: relative;
  transition: transform var(--dur-2) var(--ease);
}

.summary-banner-chevron::before {
  content: "";
  position: absolute;
  inset: 0;
  border-right: 1.5px solid currentColor;
  border-bottom: 1.5px solid currentColor;
  transform: translate(-1px, -1px) rotate(45deg);
}

.summary-banner.is-open .summary-banner-chevron {
  transform: rotate(180deg);
}

@media (prefers-reduced-motion: reduce) {
  .summary-banner-chevron { transition: none; }
}

.summary-banner-body {
  padding: 0 var(--s-4) var(--s-4);
  border-top: 1px solid var(--hair-soft);
  padding-top: var(--s-3);
  font: 400 13.5px/1.55 var(--f-body);
  color: var(--fg);
}

.summary-banner.is-static .summary-banner-body { border-top: 0; padding-top: 0; }

.summary-banner-body > *:first-child { margin-top: 0; }

.summary-banner-body > *:last-child { margin-bottom: 0; }

.summary-banner-body p { margin: 0 0 8px; }

/* …additional rules trimmed for brevity — see _shared.css */
import { SummaryBanner } from '@magicblocksai/ui';

// default — neutral treatment
<SummaryBanner
  title="Conversation summary"
  caption="Updated 3 minutes ago."
>
  <p>Hoang reported a missing password-reset email. The agent ran a Mailgun
  search, found both attempts in a Gmail spam route, and resent via the
  backup channel.</p>
</SummaryBanner>

// accent — pink left rail, AI-content surfaces
<SummaryBanner accent title="Today's agent changes" caption="Three edits since yesterday.">
  <p>Switched the Hook persona to "Friendly mortgage rep". Added two key
  facts to Qualify. Removed the Embed action.</p>
</SummaryBanner>

// static — no chevron, body always visible
<SummaryBanner hideToggle title="Pending review" caption="Awaiting approval from the duty manager.">
  <p>Two flagged turns from the overnight queue need a human eye before
  the agent ships its weekly digest.</p>
</SummaryBanner>

19.6 LiveChatTester

The composed “try my agent” shell. Wraps <ChatTranscript> + <ChatComposer> with a sticky agent-identity header and an optional reset affordance. Used for sandboxes, channel previews, and any place an operator wants to chat with their own agent without leaving the builder.

LiveChatTester

.live-chat-tester

The full tester. Sticky agent header carries an avatar, the agent name, a version pill, a status dot, and a restart affordance; the transcript renders a four-message canned exchange; the footer composer is empty and idle.

Charlie’s Wines agent draft v15
Online Powered by gpt-4.1
Hi! Try asking me anything about your agent.
What’s on the secret-deals page right now?
Three offers — a 2021 Aglianico at 22% off, a 2020 Riesling at 18%, and a magnum bundle at 15%. Want the SMS code?
Yes — text me the Aglianico one.
<!-- LiveChatTester wraps a chat-transcript with a custom header -->
<!-- that carries the agent identity card. The composer is the   -->
<!-- standard chat-composer pinned to the transcript footer.     -->
<div class="live-chat-tester">
  <div class="chat-transcript">
    <div class="chat-transcript-header">
      <header class="live-chat-tester-head">
        <div class="live-chat-tester-avatar" aria-hidden="true">CW</div>
        <div class="live-chat-tester-title-block">
          <div class="live-chat-tester-name-row">
            <span class="live-chat-tester-name">Charlie’s Wines agent</span>
            <span class="live-chat-tester-version">
              <span class="badge tone-warning">draft v15</span>
            </span>
          </div>
          <div class="live-chat-tester-status-row">
            <span class="live-chat-tester-status-dot is-online" aria-hidden="true"></span>
            <span class="live-chat-tester-status-label">Online</span>
            <span class="live-chat-tester-sep" aria-hidden="true">·</span>
            <span class="live-chat-tester-caption">Powered by gpt-4.1</span>
          </div>
        </div>
        <button type="button" class="live-chat-tester-reset" aria-label="Restart">
          <!-- inline 14×14 restart icon SVG -->
          <span>Restart</span>
        </button>
      </header>
    </div>
    <div class="chat-transcript-scroll" role="log" aria-live="polite" aria-relevant="additions">
      <div class="chat-transcript-list">
        <!-- …four chat-msg rows… -->
      </div>
    </div>
    <div class="chat-transcript-footer">
      <form class="chat-composer">…</form>
    </div>
  </div>
</div>
.live-chat-tester {
  display: flex;
  flex-direction: column;
  height: 100%;
  min-height: 360px;
}

.live-chat-tester.is-disabled { opacity: 0.55; pointer-events: none; }

.live-chat-tester .chat-transcript {
  height: 100%;
  min-height: 360px;
}

.live-chat-tester-head {
  display: grid;
  grid-template-columns: auto 1fr auto auto;
  align-items: center;
  gap: var(--s-3);
  width: 100%;
}

.live-chat-tester-avatar {
  width: 36px;
  height: 36px;
  border-radius: 999px;
  background: color-mix(in oklab, var(--accent) 18%, var(--bg-warm));
  color: var(--accent);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font: 600 13px/1 var(--f-mono);
  flex-shrink: 0;
}

.live-chat-tester-title-block {
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 0;
}

.live-chat-tester-name-row {
  display: inline-flex;
  align-items: baseline;
  gap: 6px;
}

.live-chat-tester-name {
  font: 600 14px/1.3 var(--f-display);
  color: var(--fg);
}

.live-chat-tester-version {
  font: 500 11px/1 var(--f-mono);
  padding: 2px 6px;
  border-radius: 999px;
  background: var(--bg-warm);
  color: var(--fg-soft);
}

.live-chat-tester-status-row {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font: 400 12px/1.3 var(--f-body);
  color: var(--fg-soft);
}

.live-chat-tester-status-dot {
  width: 7px;
  height: 7px;
  border-radius: 999px;
  background: var(--fg-faint);
}

.live-chat-tester-status-dot.is-online {
  background: #1A8754;
  box-shadow: 0 0 0 2px color-mix(in oklab, #1A8754 24%, transparent);
}

.live-chat-tester-status-dot.is-offline { background: var(--fg-faint); }

.live-chat-tester-status-dot.is-thinking {
  background: #F9AD03;
  animation: live-chat-status-blink 1.4s ease-in-out infinite;
}

.live-chat-tester-status-dot.is-error {
  background: #C0392B;
}

@media (prefers-reduced-motion: reduce) {
  .live-chat-tester-status-dot.is-thinking { animation: none; }
}

.live-chat-tester-status-label { font-weight: 500; color: var(--fg); }

.live-chat-tester-sep { opacity: 0.5; }

.live-chat-tester-caption { color: var(--fg-soft); }

.live-chat-tester-actions { display: inline-flex; align-items: center; gap: 4px; }

/* …additional rules trimmed for brevity — see _shared.css */
import { useState } from 'react';
import { LiveChatTester, ChatMessage, Badge } from '@magicblocksai/ui';

function Example() {
  const [input, setInput] = useState('');
  return (
    <LiveChatTester
      agentName="Charlie’s Wines agent"
      agentAvatar="CW"
      agentVersion={<Badge tone="warning">draft v15</Badge>}
      status="online"
      caption="Powered by gpt-4.1"
      composerValue={input}
      onComposerValueChange={setInput}
      onSubmit={(text) => sendToAgent(text)}
      onReset={() => resetTranscript()}
    >
      <ChatMessage from="agent" avatar="CW">Hi! Try asking me anything about your agent.</ChatMessage>
      <ChatMessage from="user" avatar="JS">What's on the secret-deals page right now?</ChatMessage>
      <ChatMessage from="agent" avatar="CW">
        Three offers — a 2021 Aglianico at 22% off, a 2020 Riesling at 18%,
        and a magnum bundle at 15%. Want the SMS code?
      </ChatMessage>
      <ChatMessage from="user" avatar="JS">Yes — text me the Aglianico one.</ChatMessage>
    </LiveChatTester>
  );
}

19.7 Sessions list

The conversation hub — every session the agents have handled, in one searchable, sortable, filterable table. Saved views (Latest · With goal · Negative) live at the top of the table; the row chips encode status, sentiment, channel, and goal in one glance. Click any row to open the session detail (composition 19.8).

All conversations — filter, sort, scan

.cv-list · .cv-table

Table-first composition. Top bar carries the title + sub-line + search + Filter + Sort + Export. Saved-view tabs (Latest / With goal / Negative / Flagged) below; sync indicator on the right. Each row composes a contact cell (avatar + name + sentiment dot + masked id), a status chip, a channel chip with icon, a one-line preview, message + event counts, goal pill, and a timestamp. Clickable rows show a hover state; the third row demos the selected state.

All conversations 724

Your conversation hub — monitor how users interact with your AI across every session, channel, and outcome.

Synced 12m ago
Contact Status Channel Last message Goal Msgs Started
NM
Nectar Mint
s_AUacy1ZX…Uvca
Live Web Chat That's a great question, Hạ Vi! While I don't have specific details about support availability… Meeting booked 12 2m ago
BC
Bessie Cooper
s_PpKmt8…dQwf
Active Email Hello! I'm here to help. To better assist you, please provide me with the following information… Qualified 8 3d ago
AF
Albert Flores
s_Lk89tR…eYzW
Inactive Web Chat Hello! I'm here to help. To better assist you, please provide me with the following information… No goal 3 10m ago
RR
Ronald Richards
s_Vk7Mq3…Bfrx
Flagged SMS I asked for a refund and you keep transferring me. Get me a real person… No goal 14 3d ago
LA
Leslie Alexander
s_Mn4Pq8…Cdef
Closed WhatsApp Booked. Looking forward to the demo on Tuesday at 2pm. Thanks for the help! Demo booked 17 5h ago
JW
Jenny Wilson
s_Qw8Rt2…Ghij
Active Email Looking at your pricing page—the Scale tier looks closest. Can you confirm what the AI message cap is? Pricing intent 6 35m ago
JJ
Jacob Jones
s_Yu7Wq1…Klmn
Closed Web Chat Got the discount code, thanks. Will probably order in the next day or two. Quote sent 9 1d ago
1–7 of 724
<div class="cv-list">
  <header class="cv-list-head">
    <div class="cv-list-head-title">
      <h2>All conversations <span class="cv-count">724</span></h2>
      <p class="cv-sub">Your conversation hub…</p>
    </div>
    <div class="cv-list-head-actions">
      <span class="cv-search"><input type="search" placeholder="Search…" /></span>
      <button class="cv-pill">Filter</button>
      <button class="cv-pill">Sort: Latest</button>
      <button class="cv-pill">Export</button>
    </div>
  </header>

  <div class="cv-list-views">
    <div class="cv-list-views-tabs">
      <button class="cv-list-views-tab is-active">Latest <span>724</span></button>
      <button class="cv-list-views-tab">With goal <span>186</span></button>
      <button class="cv-list-views-tab">Negative <span>23</span></button>
      …
    </div>
    <div class="cv-list-sync">Synced 12m ago · Refresh</div>
  </div>

  <div class="tbl-wrap">
    <table class="tbl">
      <thead>
        <tr>
          <th class="tbl-check"><input type="checkbox" /></th>
          <th>Contact</th><th>Status</th><th>Channel</th>
          <th>Last message</th><th>Goal</th>
          <th class="tbl-right">Msgs</th>
          <th class="tbl-sort is-active">Started</th>
          <th class="tbl-right"></th>
        </tr>
      </thead>
      <tbody>
        <tr class="is-selected">
          <td class="tbl-check"><input type="checkbox" checked /></td>
          <td>
            <div class="tbl-person">
              <span class="av">NM</span>
              <div>
                <div class="tbl-name">Nectar Mint <span class="cv-sentiment-dot is-positive"></span></div>
                <div class="tbl-sub">s_AUacy…Uvca</div>
              </div>
            </div>
          </td>
          <td><span class="badge cv-chip-live"><span class="dot dot-green"></span> Live</span></td>
          <td><span class="cv-channel">Web Chat</span></td>
          <td><span class="cv-preview">That's a great question…</span></td>
          <td><span class="badge"><span class="dot dot-green"></span> Meeting booked</span></td>
          <td class="tbl-right tbl-num">12</td>
          <td><span class="mono tbl-mono">2m ago</span></td>
          <td class="tbl-right"><button class="tbl-icon">⋯</button></td>
        </tr>
        …
      </tbody>
    </table>
  </div>

  <footer class="cv-list-foot">…pagination…</footer>
</div>
/* Sentiment dot — the genuinely-new per-row signal. Four tones map
   to the platform's sentiment classifier output. */
.cv-sentiment-dot.is-positive { background: #0F8062; }
.cv-sentiment-dot.is-neutral  { background: var(--warm-7); }
.cv-sentiment-dot.is-negative { background: #C13449; }
.cv-sentiment-dot.is-mixed    {
  background: linear-gradient(90deg, #0F8062 50%, #C13449 50%);
}

/* Status chips compose the kit's `.badge` + `.dot` from chapter 7.4.
   Wrap a Live badge in `.cv-chip-live` to add the pulse on its dot. */
.cv-chip-live .dot-green {
  animation: cv-pulse 2.4s var(--ease) infinite;
}
@keyframes cv-pulse { 0%, 60%, 100% { opacity: 1; } 70% { opacity: 0.4; } }
@media (prefers-reduced-motion: reduce) {
  .cv-chip-live .dot-green { animation: none; }
}

/* Saved-view tab row composes the kit's `.tabs` underline-bar from
   chapter 6.3 — no chapter-private styling for the tabs themselves. */
/* PROVISIONAL — pending kit additions flagged in gap list.

   Pending kit exports:
     SessionsTable    — table primitive with chips + sentiment
     SessionRow       — single row
     SearchInput      — pill search field
     SortMenu         — sort selector
   Existing: FilterChipGroup, SavedViewsRail, SyncStatus, FilterPopover. */

export function SessionsListPage({ sessions }) {
  const [view, setView] = useState("latest");
  const [query, setQuery] = useState("");
  return (
    <PageShell title="All conversations" count={sessions.length}>
      <Toolbar>
        <SearchInput value={query} onChange={setQuery} />
        <FilterPopover />
        <SortMenu defaultValue="started_desc" />
      </Toolbar>
      <SavedViewsRail value={view} onValueChange={setView}
        views={["latest","with_goal","negative","flagged"]}
      />
      <SessionsTable
        sessions={visible(sessions, view, query)}
        onRowClick={s => navigate(`/sessions/${s.id}`)}
      />
    </PageShell>
  );
}

19.8 Session detail

Open a row from 19.7 and the table collapses to a compact left rail; the conversation takes the main pane. Each agent message carries a small “trace” button that expands the reasoning inline beneath it — knowledge retrieved, facts captured, goals triggered, tool calls, errors, guardrail and security events — colour-coded by kind. A “Show all traces” toggle at the top opens every trace at once for power review. No secondary right rail; the testing chat bar lives outside.

Conversation — Nectar Mint · meeting booking

.cv-detail · .cv-trace

Two-pane layout. Left rail (320px) shows the compact session list with the selected conversation highlighted; the rail keeps search + saved-view tabs so the operator can switch sessions without leaving the page. Main pane shows the agent profile + agent version + actions in the head, then the conversation thread with inline-expandable traces below the agent turns that did interesting work. Click Show all traces to open them all at once.

NM

Nectar Mint Live

Web Chat · Started Friday 2:18pm · Test error Agent · v15
3 of 4 agent turns produced reasoning events.
Test error Agent Friday 2:22pm
Hi there! I'm Agent. What brings you to MagicBlocks today?
Friday 2:22pm
Hi my name is Hạ Vi, thuht+251125002@magicblocks.ai
Test error Agent Friday 2:22pm
It's lovely to meet you, Hạ Vi! How can I help you get the most out of MagicBlocks today?
  1. step 1Capture
    Facts captured
    01:57:38
    NameHạ Vi
    Emailthuht+251125002@magicblocks.ai
  2. step 2Journey
    Block transition
    +18ms
    FromHook
    ToQualify
    CauseName + email captured
Friday 2:22pm
I live in Ho Chi Minh city Viet Nam, can you support on this location
Test error Agent Friday 2:22pm
That's a great question, Hạ Vi!

While I don't have specific details about support availability in Ho Chi Minh City, I can definitely help you with general information about MagicBlocks.

What kind of support are you looking for?
  1. step 1Knowledge
    2 sources retrieved from RAG
    01:57:38

    “For customers in the APAC region, including Vietnam, our local support team operates 9am–6pm Singapore Time (GMT+8). Outside these hours, follow the global escalation path documented in…”

    Ho Chi Minh City (Saigon) is the largest city in Vietnam. Region: APAC. Default time zone: Asia/Ho_Chi_Minh (GMT+7). Business hours: 8am–5pm local.”

    Match“Ho Chi Minh City” → APAC region
  2. step 2Capture
    Facts captured
    +18ms
    LocationHo Chi Minh City
  3. step 3Goal
    Goal triggered
    +34ms
    GoalBook a meeting
    TimeApr 18, 9:00 AM
  4. step 4Tool
    Tool call · Slack
    +142ms
    ActionChecked staging agent
    Result200 OK · 142ms
  5. step 5Form
    Form completed
    +86ms
    FormMeeting booking
    SubmittedApr 18, 8:58 AM
  6. step 6Form
    Form submitted to Calendar
    +44ms
    StatusConfirmed
  7. step 7Model
    Model error Retried on Haiku 4.5 fallback…
    +430ms
    CodeRATE_LIMIT on follow-up call
  8. step 8Form
    Form post error Suggested fix: add email address
    +72ms
    ReasonMissing required field
  9. step 9Security
    Jailbreak attempt intercepted Refused safely · conversation continued
    +12ms
    DetectionPrompt injection signature
  10. step 10Meta
    Message ID
    +8ms
    m_AUacy1ZXTWTzUuVca_Tm7SiC1Uiem2v
<div class="cv-detail">
  <aside class="cv-rail">
    <div class="cv-rail-head">
      <button class="cv-rail-back">‹ All conversations</button>
      <input type="search" placeholder="Find a session…" />
      <div class="cv-rail-tabs">
        <button class="is-active">Latest</button>
        <button>With goal</button>
        <button>Negative</button>
      </div>
    </div>
    <div class="cv-rail-list">
      <div class="cv-rail-row is-active">…Nectar Mint…</div>
      …other compact session rows…
    </div>
  </aside>

  <main class="cv-pane">
    <header class="cv-pane-head">
      <h3>Nectar Mint <span class="badge cv-chip-live"><span class="dot dot-green"></span> Live</span></h3>
      <span class="cv-pane-head-sub">Web Chat · Started Friday 2:18pm · v15</span>
    </header>

    <div class="cv-thread">
      <div class="cv-thread-controls">
        3 of 4 agent turns produced reasoning events.
        <button data-toggle-all-traces>Show all traces</button>
      </div>

      <!-- The trace toggle is the shipped .message-trace-button (chapter
           19.4): data-trace-toggle + aria-controls drive the inline
           .trace-timeline (chapter 20.2/20.3) disclosure below — one
           authoritative trace primitive, no bespoke pill. (Per the 19.4
           canonical pattern you can instead place the button in an agent
           message's actions slot; here it heads the inline trace.) -->
      <div class="cv-trace" id="cv-trace-3" data-open="true">
        <button class="message-trace-button is-size-sm is-noteworthy"
                data-trace-toggle aria-controls="cv-trace-3" aria-expanded="true">
          …robot-head SVG…
          <span class="message-trace-button-count">10</span>
          <span class="message-trace-button-dot"></span>
        </button>

        <div class="trace-timeline">
          <ol class="trace-timeline-list" role="list">
            <li class="trace-timeline-item">
              <div class="trace-event is-tone-success">
                …leading icon · eyebrow · title · time · bullet items…
              </div>
            </li>
            …step 2 (success) Facts captured…
            …step 3 (success) Goal triggered…
            <li class="trace-timeline-item is-section-break">
              <div class="trace-event is-tone-warn">…Tool call · Slack…</div>
            </li>
            …more warn / error / security / info events…
          </ol>
        </div>
      </div>
    </div>
  </main>
</div>
/* The ONLY chapter-private CSS the trace needs — the inline-anchoring
   wrapper that hangs the timeline beneath an agent message. The
   colour-coded step styling itself lives in chapter 20 (Explainability)
   and is already shipped + production-styled. */
.cv-trace {
  margin-left: 48px;
  border-left: 2px solid var(--accent);
  padding-left: var(--s-4);
  display: none;
}
.cv-trace[data-open="true"] { display: flex; flex-direction: column; }

/* "Show all traces" master toggle opens every trace at once for
   power review. The thread carries .is-all-traces-open. */
.cv-thread.is-all-traces-open .cv-trace { display: flex; }
.cv-thread.is-all-traces-open .message-trace-button {
  background: var(--accent); color: var(--paper); border-color: var(--accent);
}
// Per-message trace toggle + "Show all traces" master toggle.
// Idempotent and defensive — both work if the section is present.
document.addEventListener('DOMContentLoaded', () => {
  // Per-message trace
  document.querySelectorAll('[data-trace-toggle]').forEach((btn) => {
    btn.addEventListener('click', () => {
      const id = btn.getAttribute('aria-controls');
      const trace = id && document.getElementById(id);
      if (!trace) return;
      const open = trace.getAttribute('data-open') === 'true';
      trace.setAttribute('data-open', open ? 'false' : 'true');
      btn.setAttribute('aria-expanded', open ? 'false' : 'true');
    });
  });

  // Master toggle — opens / closes every trace at once
  document.querySelectorAll('[data-toggle-all-traces]').forEach((btn) => {
    btn.addEventListener('click', () => {
      const thread = btn.closest('.cv-thread');
      if (!thread) return;
      const on = thread.classList.toggle('is-all-traces-open');
      btn.setAttribute('aria-pressed', on ? 'true' : 'false');
      btn.textContent = on ? 'Hide all traces' : 'Show all traces';
      // sync per-message toggles
      thread.querySelectorAll('[data-trace-toggle]').forEach((t) =>
        t.setAttribute('aria-expanded', on ? 'true' : 'false'));
      thread.querySelectorAll('.cv-trace').forEach((t) =>
        t.setAttribute('data-open', on ? 'true' : 'false'));
    });
  });
});
import {
  ChatTranscript,
  ChatMessage,
  TraceTimeline,
  TraceItemCard,
  MessageTraceButton,
} from "@magicblocksai/ui";

/* Session detail composition. Page chrome (rail + pane + transcript)
   is consumer-owned; the kit ships the trace-rendering primitives:
     - <TraceTimeline> handles full timelines (steps + items)
     - <TraceItemCard> is also exported standalone for citation cards,
       retrieval-debug surfaces, or any "label · value · meta ·
       expandable details" shape outside of a timeline. */

export function SessionDetailPage({ session, sessions, onSelect }) {
  return (
    <div className="cv-screen-frame">
      <SessionsRail
        sessions={sessions}
        activeId={session.id}
        onSelect={onSelect}
      />
      <main>
        <ChatTranscript>
          {session.messages.map(m => (
            <ChatMessage key={m.id} {...m}
              actions={m.trace && <MessageTraceButton for={m.trace} />}
            />
          ))}
        </ChatTranscript>

        {/* Below the message: expandable RAG passages from the trace. */}
        <div className="rag-passages">
          {session.ragHits.map(hit => (
            <TraceItemCard
              key={hit.id}
              tone="success"
              label={hit.source}
              value={hit.heading}
              meta={`${hit.similarity.toFixed(2)} sim`}
              defaultOpen={hit.id === session.ragHits[0].id}
              details={<p>{hit.passage}</p>}
            />
          ))}
        </div>
      </main>
    </div>
  );
}

19.9 Live takeover console

When a conversation needs a person, the AI pauses and an operator replies as the brand on the contact's own channel — guardrails off. Handing back chooses where the AI resumes.

TakeoverConsole

.takeover-console

Two control states — AI driving (a Take-over CTA) and human in control (guardrails-off composer + a JourneyBlockPicker hand-back). Composes ChatTranscript, ChatComposer, JourneyBlockPicker and KeyFactGrid; the operator-context sidebar carries the contact, key facts so far, a “Sage suggests” draft, and the journey position. The operator's own replies use the accent .is-from-operator “human” role.

You're in control · AI paused SMS+1 (415) 555-0132
Marcus2:31
This still isn't working and I've tried twice now.
Sage2:31
I'm sorry about that — connecting you with a teammate now.
You took over · AI paused2:32
Youhuman2:32
Hi Marcus — I'll sort this out personally. What do you see on the final step?
<div class="takeover-console" data-state="human_in_control">
  <div class="tc-main">
    <div class="tc-controlbar">
      <span class="tc-state"><span class="tc-state-dot"></span> You're in control · AI paused</span>
      <span class="tc-channel"><span class="channel-chip" data-channel="sms">SMS</span> <span class="tc-channel-id">+1 (415) 555-0132</span></span>
      <div class="tc-controlbar-action">
        <button class="jbp-trigger">Hand back <span class="jbp-caret"></span></button>
      </div>
    </div>

    <div class="tc-transcript">
      <div class="chat-transcript">
        <!-- agent · system "took over · AI paused" · operator bubble (.is-from-operator + .chat-msg-human) -->
        …message stack…
        <div class="chat-transcript-footer">
          <form class="chat-composer">
            <textarea class="chat-composer-input" placeholder="Reply as Acme…"></textarea>
            <button class="chat-composer-send">Send as human</button>
            <div class="chat-composer-helper">
              <span class="tc-guard"><span class="tc-guard-dot"></span> Guardrails off · sending as a human</span>
            </div>
          </form>
        </div>
      </div>
    </div>
  </div>

  <aside class="tc-aside">
    <div class="tc-sage">…Sage suggests + Insert…</div>
    <div class="tc-facts">…<div class="key-fact-grid" data-layout="list">…</div></div>
    <div class="tc-journey">…Journey position…</div>
  </aside>
</div>
import {
  TakeoverConsole,
  JourneyBlockPicker,
  ChatTranscript,
  ChatMessage,
} from "@magicblocksai/ui";

/* SessionPage mode="live" renders this console under the shared hero.
   The AI pauses on takeover; the operator replies as the brand on the
   contact's own channel with guardrails OFF. Hand back picks the
   Journey Block the AI resumes from. */

export function LiveTakeover({ session, onHandBack }) {
  const [draft, setDraft] = useState("");
  return (
    <TakeoverConsole
      defaultState="ai_driving"            /* → human_in_control on "Take over" */
      channel={session.channel}
      channelLabel={session.channelLabel}  /* "+1 (415) 555-0132" */
      brandName="Acme"
      composerValue={draft}
      onComposerChange={setDraft}
      onSend={(text) => { sendAsHuman(session.id, text); setDraft(""); }}
      transcript={
        <ChatTranscript>
          {session.messages.map((m) => (
            <ChatMessage key={m.id} from={m.from} name={m.name}>{m.body}</ChatMessage>
          ))}
          {/* the operator's own replies use the accent "human" role: */}
          <ChatMessage from="operator" name="You">…your reply…</ChatMessage>
        </ChatTranscript>
      }
      handBackOptions={[
        { id: "resume", kind: "resume", label: "Resume where the AI paused", detail: "Recommended" },
        { id: "meeting", kind: "block", label: "Book a meeting" },
        { id: "end", kind: "end", label: "End — no further automation" },
      ]}
      onHandBack={onHandBack}
      keyFacts={session.keyFacts}
      sage={{ text: session.sageDraft, onInsert: () => setDraft(session.sageDraft) }}
      journeyPosition="Post-demo nurture · step 2"
    />
  );
}

/* The hand-back menu is also exported standalone: */
<JourneyBlockPicker
  options={handBackOptions}
  onSelect={onHandBack}
/>