Skip to content

Components / Sections

Chat Demo

A conversation with an AI agent, replayed in the page: the reader's message, the agent's reply with its tool calls folded into <details> (the call and its result in the monospace face), a request for approval that is approved, and the final answer, typed, with a button that copies it; a prompt box at the foot as a demo form. The server HTML is the end state. The replay is CSS on one clock, started when the chat comes into view, held by a native checkbox drawn as a Pause button and by a pointer, and done in 8 to 12 s. Several conversations can be keyed by slug, and a small internal script picks one from the address (?task=refactor); the default renders without it.

Live demo

Sample conversation

Ask, watch it work, approve

An imaginary agent on an imaginary repository. Open a call to read what it returned.

Sample agent

Done

  1. You

    Fix the failing checkout test in apps/web

  2. Sample agent

    One assertion fails: the total keeps a fraction of a cent. I will find where it is computed.

    • read_log(job 4182)0.4 s
      FAIL apps/web/cart.test.ts
        total > rounds to cents
        expected 23.99, received 23.988
    • grep("* 1.2" apps/web/src)0.2 s
      apps/web/src/cart/total.ts:3  return sum * 1.2;
    • run_tests(apps/web)38 s
      Tests  48 passed (48)
      Time   37.9 s
  3. Sample agent

    Push the change to fix/checkout-rounding and open a pull request?

    Approved

  4. Sample agent

    Done. The total now rounds to cents after tax, and all 48 tests in apps/web pass. The pull request is open for review.

Live, from the library itself, at full size: scroll, hover, navigate away and back. The demo is never gated.

Variants · each row writes the attribute the prop writes; the section's own CSS answers

tone
density

Measured

JavaScript of its own

0 B

Uses Reveal and the shared runtime (1.9 kB raw, once per site) and CopyButton and PromptInputSubmitOnEnter. With those included: 4.1 kB raw.

With tasks: 314 B raw of its own on Reveal and the runtime and CopyButton and PromptInputSubmitOnEnter, measured on its own fixture. The default above ships none of it.

CSS 32.6 kB raw including the base tokens. Measured from a production build, and again in CI for every change that can move it.

Browser support

Limited availability

Chrome 116 · not Firefox · Safari 18; needs has, display-animation, clipboard. Elsewhere: without :has() the chat plays once and cannot be held; where display does not animate (Firefox) the answer still types in turn through the font-size in the same keyframes; it ends on the whole conversation either way.

Install Pro

The demo above is never gated; the source is. With a key in components.json (see account), the registry serves it like any other item:

terminal
npx shadcn@latest add @moonarc-pro/chat-demo
src/pages/index.astro
---
import ChatDemo from '@/components/moonarc/pro/ChatDemo.astro';
import chats from '../content/chats.json';
---
<!-- /demo/?task=update-deps shows that conversation; the first is the default, and what renders without JavaScript -->
<ChatDemo
  level={1}
  headline="Watch the agent work"
  names={{ user: 'You', agent: 'Agent' }}
  chats={chats}
  action="/demo/"
/>

Without a key the registry answers 401 with an upgrade message. Everything this section composes is free: reveal, spinner, typewriter, copy-button, prompt-input.

Props

PropTypeDefaultDescription
chats{ slug, prompt, reply, calls, ask, answer }[]noneThe conversations, up to six. prompt: the reader's message. reply: the agent's first words. calls: one to four { name, args?, took?, result }, each folded into a <details> with its result. ask: the request for approval. answer: the final answer, typed, with a copy button. slug keys it in the address.
activestringnoneThe slug of the conversation shown by default; the first unless set.
paramstring'task'The query parameter that picks a conversation (/demo/?task=refactor). With more than one conversation this adds a small internal script, measured on its own; an unknown slug keeps the default.
names{ user?, agent? }You · AgentWho is speaking: every message is a list item that starts with its speaker's name for screen readers, and the agent's name is shown in the chat's bar.
markImageMetadata | stringnoneThe agent's mark beside its name. Unset, its initial on the accent.
headlinestringnoneThe headline above the chat. The headline slot wins over it.
eyebrowstringnoneSmall label above the headline. The eyebrow slot wins over it.
copystringnoneThe paragraph under the headline. The copy slot wins over it.
labels{ log?, working?, running?, done?, approve?, approved?, copy?, copied?, placeholder?, prompt?, send? }Conversation · Working · Running · Done · Approve · Approved · Copy the answer · Copied · Ask a follow-upThe words the chat prints around the conversation: the list's name, the working line, the status, the approve before and after it is pressed, the copy button's name (it says what it copies) and its done face, and the prompt box's placeholder, name and send button.
pauseLabelstring'Pause'The pause control's label: a checkbox drawn as a small button, checked while the chat is held.
inputbooleantrueThe prompt box at the foot.
actionstring'/demo/'Where the foot's form goes: a demo page by default.
method'get' | 'post''get'The foot's form method.
namestringnoneThe foot's textarea name. Unset, it has none and the form sends no text.
level1 | 22Heading level of the headline: 2 under the page's h1, 1 when the section opens the page.
tone'surface' | 'muted' | 'ink' | 'brand'unset (paints nothing)The ground: the page surface, a muted band, an always-dark ink panel, or a deep brand panel. With theme.css the chat's panel takes the tone's card; on brand what is live is marked in the text colour.
density'compact' | 'cozy' | 'roomy''cozy'Block padding and the gap above the chat ×0.8 / ×1 / ×1.25 through --ma-density.

Theming and overrides

The section reads the theme contract (shadcn's variable names with the base tokens as fallback), so a pasted theme or one --ma-brand recolours it. Its class names are the override surface and stay stable; a named slot replaces the matching prop with your own markup.

Classes

.ma-chat-demo.ma-chat-demo__agent.ma-chat-demo__answer.ma-chat-demo__approve.ma-chat-demo__approved.ma-chat-demo__args.ma-chat-demo__ask.ma-chat-demo__ask-text.ma-chat-demo__bar.ma-chat-demo__bars.ma-chat-demo__beat.ma-chat-demo__bubble.ma-chat-demo__call.ma-chat-demo__calls.ma-chat-demo__chat.ma-chat-demo__chev.ma-chat-demo__copy.ma-chat-demo__copy-wrap.ma-chat-demo__cue.ma-chat-demo__decision.ma-chat-demo__details.ma-chat-demo__done.ma-chat-demo__eyebrow.ma-chat-demo__fn.ma-chat-demo__foot.ma-chat-demo__head.ma-chat-demo__headline.ma-chat-demo__inner.ma-chat-demo__log.ma-chat-demo__mark.ma-chat-demo__msg.ma-chat-demo__out.ma-chat-demo__panel.ma-chat-demo__pause.ma-chat-demo__pause-control.ma-chat-demo__pause-icon.ma-chat-demo__pip.ma-chat-demo__play.ma-chat-demo__room.ma-chat-demo__running.ma-chat-demo__spinner.ma-chat-demo__stack.ma-chat-demo__status.ma-chat-demo__still.ma-chat-demo__summary.ma-chat-demo__text.ma-chat-demo__took.ma-chat-demo__typed.ma-chat-demo__working.ma-in.ma-type__char

Slots

slot="eyebrow"slot="headline"slot="copy"

Reduced motion

The whole conversation at once: every message there, the calls folded, the approval given, the answer whole and its copy button shown. Nothing travels and the Pause control is not drawn.

With ClientRouter

CSS only; the copy button, the prompt box's Enter and the conversation picker rebind through the shared runtime. A new page replays the chat when it comes into view.

Why it is built this way

Replaces: Chat demo (Vercel AI Elements) · AI chat mockup (Magic UI). See the migration table.

npx astro add moonarc
click · check · back

A button that copies text to the clipboard: the icon turns into a check, the label says "Copied", and both come back after the ambient duration.

every browserclick

grows with the text · Enter sends · ⇧Enter breaks the line

The AI prompt box: a form with a textarea that grows with its content through field-sizing, a model select, an attachment button that is a real file input, and a send button that dims until something is typed and spins while the form carries data-pending.

newly · Chrome 123 · Firefox 152 · Safari 26.2click

Types a string one character at a time in any font, with a caret that stays solid while typing and blinks once idle.

Chrome 116 · not Firefox · Safari 18load

Sample run

One task, from the failing test to the merge

An imaginary agent on an imaginary repository. The lanes fill in order; under reduced motion, or without JavaScript, they are filled from the start.

Task Fix the failing checkout test in apps/web

Done

  1. Plan

    • Read the failing job
    • Reproduce it locally
    • Round the total to cents
    • Run the suite again
  2. Tools

    • read_log(job 4182)done0.4 s
    • grep("toBe(19.99)" apps/web)done0.2 s
    • run_tests(apps/web)done38 s
  3. Changes

    apps/web/src/cart/total.ts
    export function total(items: Item[]) { const sum = items.reduce(add, 0); return sum * 1.2; const gross = sum * 1.2; return Math.round(gross * 100) / 100;}
  4. Review

    Maya Kim

    Ready to merge. The suite passes and the change is two lines.

    Approved

Checkout totals round to cents

Files
1 changed
Tests
48 passed
Time
2 min 41 s
Tokens
18.2k

An AI agent's run, replayed in the page: the task at the top, then four lanes that fill in order (the plan ticks off, tool calls spin and finish with their time, a diff writes in line by line, a person is asked to approve and the approve reads Approved), then the result with its numbers.

every browserscrollhoverclick

A hero whose entry point is a prompt box: a small label, a short headline, a lead, a large PromptInput with Enter to send, a status line and example tasks.

newly · Chrome 123 · Firefox 152 · Safari 26.2load

Also: view transitions · how costs are measured · browser support · accessibility policy · five-minute setup · MCP for agents · this page as Markdown