Chat · Components

ChatComponent

ChatComponent is the all-in-one composition that provides a complete, styled chat interface. It combines message rendering, text input, typing indicator, error display, interrupt handling, and an optional thread sidebar into a single component.

No Tailwind required

ChatComponent ships with its own design tokens and component-scoped styles. No PostCSS config, no Tailwind setup, and no global stylesheet import is needed.

Selector: chat

Import:

import { ChatComponent } from '@threadplane/chat';

#When to Use It

Use ChatComponent when you want a fully functional chat UI with minimal setup. It handles:

  • Rendering human, AI, tool, and system messages with appropriate templates
  • Markdown rendering for AI messages
  • Auto-scrolling to new messages and streaming content
  • Typing indicator while the agent is processing
  • Error banner for agent errors
  • Interrupt banner when the agent pauses
  • Optional thread sidebar for multi-conversation support
  • Enter to send, Shift+Enter for newlines

If you need to customize the message layout, add components between sections, or build a fundamentally different chat layout, use the primitives directly instead.

#Basic Usage

// app.config.ts
import { provideAgent } from '@threadplane/langgraph';
import { signal } from '@angular/core';
 
export const appConfig: ApplicationConfig = {
  providers: [
    provideAgent({
      apiUrl: 'http://localhost:2024',
      assistantId: 'chat',
      threadId: signal(null),
    }),
  ],
};
 
// chat-page.component.ts
import { Component } from '@angular/core';
import { injectAgent } from '@threadplane/langgraph';
import { ChatComponent } from '@threadplane/chat';
 
@Component({
  selector: 'app-chat-page',
  standalone: true,
  imports: [ChatComponent],
  template: `
    <div style="height: 100vh;">
      <chat [agent]="chatRef" />
    </div>
  `,
})
export class ChatPageComponent {
  protected readonly chatRef = injectAgent();
}
Height is required

ChatComponent uses height: 100% with flex layout. Make sure its parent container has an explicit height, otherwise the component will collapse to zero height.

#API

#Inputs

InputTypeDefaultDescription
agentAgentRequiredThe runtime-neutral agent providing streaming state. injectAgent() from @threadplane/langgraph returns a compatible LangGraphAgent.
viewsViewRegistry | undefinedundefinedView registry for generative UI. Maps spec type names to Angular components. Created with views() from @threadplane/chat.
storeStateStore | undefinedundefinedOptional state store for interactive generative UI specs.
handlersRecord<string, (params: Record<string, unknown>) => unknown | Promise<unknown>>{}Event handlers for generative UI specs and consumer-owned A2UI local actions. Handlers run in Angular injection context — inject() is available inside handler functions.
threadsThread[][]List of threads to display in the sidebar. Each thread must have an id property.
activeThreadIdstring''The ID of the currently active thread, used for highlighting in the sidebar.
welcomeDisabledbooleanfalseWhen true, suppresses the welcome screen shown for an empty conversation.
modelOptionsreadonly ChatSelectOption[][]When non-empty, renders a <chat-select> model picker inside the input pill, wired to the two-way selectedModel. Leave empty to project your own <chat-select chatInputModelSelect>.
showModelPickerbooleantrueWhen false, hides the auto-rendered model picker even when modelOptions is non-empty. No effect on a consumer-projected <chat-select>.
selectedModelstring (two-way)''The selected model value, bound to the auto-rendered model picker. Use with [(selectedModel)].
modelPickerPlaceholderstring'Choose a model'Placeholder shown in the auto-rendered model picker when no option matches.
genuiToolNamesreadonly string[]['generate_a2ui_schema', 'generate_json_render_spec', 'render_spec']Tool names whose calls produce a rendered GenUI surface rather than visible text. Used to filter <chat-tool-calls> and detect GenUI turns.

#Outputs

OutputTypeDescription
threadSelectedstringEmits when a thread is clicked in the sidebar. Payload is the thread ID.
renderEventChatRenderEventEmits render lifecycle events from generative UI surfaces.
regeneratevoidEmits when the user clicks the regenerate button on an assistant message.
rate{ messageIndex: number; rating: 'up' | 'down' }Emits when the user rates an assistant message.
messageCopy{ messageIndex: number; content: string }Emits when the user copies an assistant message.

#Thread Type

type Thread = { id: string; [key: string]: unknown };

#Thread Sidebar

When you pass a non-empty threads array, a sidebar appears on the left (hidden on mobile by default):

@Component({
  template: `
    <chat
      [agent]="chatRef"
      [threads]="threads()"
      [activeThreadId]="currentThreadId()"
      (threadSelected)="onThreadSelected($event)"
    />
  `,
})
export class ChatWithThreadsComponent {
  protected readonly chatRef = injectAgent();
 
  threads = signal([
    { id: 'thread-1' },
    { id: 'thread-2' },
    { id: 'thread-3' },
  ]);
 
  currentThreadId = signal('thread-1');
 
  onThreadSelected(threadId: string) {
    this.currentThreadId.set(threadId);
    this.chatRef.switchThread(threadId);
  }
}

#Message Feedback Outputs

Beyond threadSelected, <chat> emits three outputs for assistant-message feedback and regeneration. Each fires from the hover controls on an assistant message:

@Component({
  template: `
    <chat
      [agent]="chatRef"
      (regenerate)="onRegenerate()"
      (rate)="onRate($event)"
      (messageCopy)="onCopy($event)"
    />
  `,
})
export class ChatWithFeedbackComponent {
  protected readonly chatRef = injectAgent();
 
  // (regenerate) emits void — the agent already re-ran the last assistant turn.
  onRegenerate() {
    console.log('user requested regeneration');
  }
 
  // (rate) payload: { messageIndex, rating }
  onRate(event: { messageIndex: number; rating: 'up' | 'down' }) {
    console.log(`message ${event.messageIndex} rated ${event.rating}`);
  }
 
  // (messageCopy) payload: { messageIndex, content }
  onCopy(event: { messageIndex: number; content: string }) {
    console.log(`copied message ${event.messageIndex}`, event.content);
  }
}

#Message Templates

ChatComponent ships with four built-in message templates:

TypeLayout
humanRight-aligned bubble with user background and border radius
aiLeft-aligned with avatar badge, markdown-rendered content
toolFull-width monospace card with alt background
systemCentered, italic, muted text

#Generative UI

When you pass a [views] registry, the component auto-detects JSON specs in AI messages and renders them as Angular components:

<chat [agent]="chatRef" [views]="myViews" />
import { views } from '@threadplane/chat';
import { WeatherCardComponent } from './weather-card.component';
 
const myViews = views({
  weather_card: WeatherCardComponent,
});

AI messages containing JSON are parsed character-by-character as tokens stream. Components render incrementally — string props grow visibly as tokens arrive. See the Generative UI guide for full setup.

#Interactive specs and [store]

A StateStore is the shared reactive state that interactive generative-UI specs read from and write to. When a spec uses $state / $bindState prop expressions — for forms, selections, toggles — those bindings resolve against the store you pass via [store]. Read-only specs (a weather card, a chart) don't need one; reach for [store] only when a spec is interactive. Create it with signalStateStore() from @threadplane/render:

import { Component } from '@angular/core';
import { injectAgent } from '@threadplane/langgraph';
import { ChatComponent } from '@threadplane/chat';
import { signalStateStore } from '@threadplane/render';
 
@Component({
  selector: 'app-interactive-chat',
  standalone: true,
  imports: [ChatComponent],
  template: `<chat [agent]="chatRef" [views]="myViews" [store]="store" />`,
})
export class InteractiveChatComponent {
  protected readonly chatRef = injectAgent();
  protected readonly myViews = myViews;
  protected readonly store = signalStateStore({ selectedItem: null });
}

See State Store in the Generative UI guide for the full binding model.

#A2UI Rendering

When AI messages contain A2UI content (prefixed with ---a2ui_JSON---), the component auto-detects A2UI mode. Pass a2uiBasicCatalog() to [views] to render the built-in catalog:

import { ChatComponent, a2uiBasicCatalog } from '@threadplane/chat';
 
views = a2uiBasicCatalog();
<chat [agent]="chatRef" [views]="views" />

The catalog currently maps 18 component types: Text, Image, Icon, Divider, Row, Column, Card, List, Button, TextField, CheckBox, MultipleChoice, DateTimeInput, Slider, Tabs, Modal, Video, and AudioPlayer.

A2UI surfaces support two-way data binding, button actions, template expansion over collections, and validation. See the A2UI guide for details.

#Auto-Scroll Behavior

The component tracks message count and loading state to auto-scroll:

  • New message: Scrolls to bottom immediately (using behavior: 'instant')
  • Streaming updates: Scrolls smoothly only if the user is near the bottom (within 150px)
  • User scrolled up: Does not auto-scroll during streaming to avoid disrupting reading

#Message Pattern

ChatComponent renders messages using an asymmetric pattern:

  • User messages — right-aligned filled bubble using --tplane-chat-primary background and --tplane-chat-on-primary text
  • Assistant messages — inline (no bubble), left-aligned, with hover controls (copy, thumbs up/down) that appear on pointer-over

#Styling

Override any CSS custom property on the chat element or a parent. See the Theming guide for the full --tplane-chat-* token reference.

/* Example: brand color override */
chat {
  --tplane-chat-primary: #2563eb;
  --tplane-chat-on-primary: #ffffff;
}

#Included Primitives

Under the hood, ChatComponent composes these primitives:

  • ChatMessageListComponent with MessageTemplateDirective for message rendering
  • ChatInputComponent for text input (with submitOnEnter and a placeholder)
  • ChatTypingIndicatorComponent for the animated typing dots
  • ChatErrorComponent for error display
  • ChatInterruptComponent for the interrupt banner
  • ChatThreadListComponent for the sidebar

#Reasoning

When a model emits reasoning content (gpt-5 / o-series with reasoning blocks, Anthropic with thinking blocks, or any AG-UI agent emitting REASONING_MESSAGE_* events), the adapter populates Message.reasoning and Message.reasoningDurationMs. The <chat> composition automatically renders <chat-reasoning> above the assistant response. No configuration required.

While reasoning is streaming, the pill shows "Thinking…" with a pulse dot and the body auto-expands so the user sees content arrive in real time. Once response text begins, the pill collapses to "Thought for Ns" (e.g. "Thought for 4s").

#Tool-call templates

Project a <ng-template chatToolCallTemplate="…"> directly into <chat> to replace the default card UX for a specific tool name. The composition forwards the template into the inner <chat-tool-calls>.

<chat [agent]="agent">
  <ng-template chatToolCallTemplate="generate_image" let-call let-status="status">
    <my-image-card
      [prompt]="call.args.prompt"
      [imageUrl]="call.result"
      [status]="status"
    />
  </ng-template>
</chat>

A chatToolCallTemplate="*" wildcard catches any unmapped tool name. See chatToolCallTemplate for the directive reference.