Skip to content

Components

AI surface

A provider-neutral set of AI surfaces — messages, streaming text, tool calls and agent state — with no model, no transport and no vendor SDK.

Examples

A conversation

`MessageList` is a `role="log"` region that follows new content only while the reader is already at the bottom — scrolling up to re-read an answer is never yanked back down by a new token.

You

09:41

Summarise BTCUSD's recent volatility and tell me whether the term structure is still upward sloping.

Assistant

09:41

System

The second tool call returned nothing. Retry it, or narrow the query.

The demo text is written out by a timer; no request is made.

Playground

Change the props and read the result: the preview and the code below it are built from the same values, so what you see is what you copy.

Controls

Presets

Sender

Who the turn came from. It is `from`, not `role`, so a consumer's jsx-a11y lint stays clean.

Message

Author

A display name. Empty keeps the component's own word for the role.

Status

`pending` recedes, `error` takes the negative surface, `streaming` marks an unfinished turn.

Avatar

Replaces the role glyph; the accessible name still comes from the author.

Advanced

Timestamp

Pre-formatted by you: a relative time is a product decision, and the component never guesses one.

Assistant

09:31 UTC

Three positions were rebalanced this morning.

A real turn: the body wraps, the timestamp is text you supply, and the status changes the surface.

Code
import { ChatMessage } from "@luc/ui";
import { Spinner } from "@luc/ui";

<ChatMessage from="assistant">
  Three positions were rebalanced this morning.
</ChatMessage>

Not controllable here

  • actions — Copy, regenerate and rate need your callbacks, and they are revealed on hover.
  • footer — Sources, tool calls and attachments are your data, rendered under the body.
  • className — Width and placement belong to the conversation layout that mounts the message.
Copy for an AI agentA specification an agent can act on: the import, the setup, the chosen props, the data contract and the accessibility expectation.
Implement `AI surfaces` from Luc UI. It is: The conversation surface: messages, streaming text, tool calls and agent activity.

Import it with: import { AgentActivity, ChatMessage, MessageList, PromptComposer, StreamingText, ToolCallTimeline } from "@luc/ui";
Requires: react ^19.0.0; react-dom ^19.0.0; react-hook-form ^7.0.0 (optional, for @luc/ui/forms/react-hook-form); tailwindcss ^4.0.0; node >= 22 (repository tools). Import the stylesheet once at the app root and wrap the tree in `LucProvider`. Both come from the package that publishes @luc.

Chosen configuration: `from="assistant"`, `author=""`, `children="Three positions were rebalanced this morning."`, `status="complete"`, `timestamp="09:31 UTC"`, `avatar="glyph"`.

Accessibility: keep the component's own semantics. Do not re-implement its ARIA wiring, do not add roles it does not declare, and make sure every interactive element is reachable and operable from the keyboard with a visible focus state.
Styling: use Luc UI design tokens and the component's own props. Do not restyle with raw colour literals and do not override the component's internal structure with page CSS.
Motion: respect `prefers-reduced-motion`. The component already does, so do not disable it.

Reproduce this state:
```tsx
<ChatMessage from="assistant">
  Three positions were rebalanced this morning.
</ChatMessage>
```

Agent state

`AgentActivity` renders the steps the application declares. It has no “show reasoning” affordance because there is nothing to reveal: the only text it can display is text its caller passed in.

Analysing

6.2s

  1. Read the brief
  2. Fetch metrics12 of 40 symbols
  3. Write the conclusion
  4. Deliver

Compact mode

1/2

Fetch metrics

Composer

Enter sends and Shift + Enter adds a line; while a response is in flight the send button becomes a stop button, and the toolbar slot is left for model pickers and attachments.

Model: local demo

Import

These are exports of one package, so there is nothing to install per component: install @luc/ui and import the names you use.

  • import { MessageList, ChatMessage, StreamingText, ToolCallTimeline } from "@luc/ui";
  • import { PromptComposer, AgentActivity } from "@luc/ui";

Package setup — peer requirements, the stylesheet and the provider — is on installation.

Public API

The prop set stays small on purpose: composition points matter more than boolean switches.

Component props
PropTypeDefaultNotes
ChatMessage.from"user" | "assistant" | "system" | "tool"—Who the turn came from. Deliberately not called `role`: on a component that renders a DOM element, `role` collides with the ARIA attribute and makes every consumer with `eslint-plugin-jsx-a11y` report a false error.
ChatMessage.status"complete" | "streaming" | "pending" | "error"—The render state of the turn; `error` also changes the border and surface.
StreamingText.speednumber—Characters per second; under `prefers-reduced-motion` the whole string appears at once.
PromptComposer.onSubmit(value: string) => void—Receives the trimmed text on submit; the component then clears the field.
ToolCallTimeline.callsToolCall[]—Tool records: name, status, input, result and duration, all supplied by the caller.

`StreamingText` always renders a single text node: a screen reader reads the sentence once instead of spelling it out.

Usage

import { MessageList, ChatMessage, PromptComposer } from "@luc/ui";
<MessageList>
  <ChatMessage from="user">Summarise the drawdown.</ChatMessage>
  <ChatMessage from="assistant" status="done">
    The -8.4% peak-to-trough came from two sessions.
  </ChatMessage>
</MessageList>

Guidelines

Do

  • Use `from` to say who is speaking and `status` for what the message is doing (`streaming`, `done`, `error`); the surface reads both.
  • Put a tool call in `ToolCallTimeline` or `AgentActivity` rather than in the prose: a reader needs to see what ran and for how long.
  • Keep `PromptComposer`'s `busy` state tied to the request, so send becomes stop while a response is in flight.

Don't

  • Fake a response with a timer in the documentation; the components present content, and a demo that pretends to think teaches the wrong contract.
  • Put an agent's plan in the message body when it can change — a plan the reader cannot interrupt is a promise the UI cannot keep.

Accessibility

New messages are announced politely rather than stealing focus, a streaming answer keeps its caret and its place, and the composer is a labelled form whose submit control becomes stop while busy. A code block inside a message stays a code block.

Luc UI v1.0AI surfacesdark theme