Docsโ€บChatโ€บGuidesโ€บGenerative UI

Generative UI

Generative UI lets your LangGraph agent return structured JSON specs that render as Angular components in the chat. The ChatComponent auto-detects JSON specs in AI messages and renders them โ€” no manual wiring needed.

How It Works

When AI messages stream token-by-token, the ChatComponent classifies each message's content automatically:

AI message content (token by token)
  โ†’ ContentClassifier (auto-detect per message)
    โ†’ First non-whitespace is { โ†’ JSON spec path
    โ†’ Anything else โ†’ Markdown path
  โ†’ ChatComponent template renders both:
    โ†’ Markdown prose via renderMarkdown()
    โ†’ JSON specs via RenderSpecComponent + your view registry

The JSON path uses @ngaf/partial-json to parse incomplete JSON character-by-character, producing a live Spec signal with structural sharing โ€” unchanged elements keep the same object reference so Angular skips re-rendering them.

Setup

Pass a ViewRegistry via the [views] input on ChatComponent:

import { Component, signal } from '@angular/core';
import { agent } from '@ngaf/langgraph';
import { ChatComponent, views } from '@ngaf/chat';
import { WeatherCardComponent } from './weather-card.component';
import { ChartComponent } from './chart.component';
 
const myViews = views({
  weather_card: WeatherCardComponent,
  chart: ChartComponent,
});
 
@Component({
  selector: 'app-chat',
  standalone: true,
  imports: [ChatComponent],
  template: `
    <div style="height: 100vh;">
      <chat [agent]="chatRef" [views]="myViews" />
    </div>
  `,
})
export class ChatPageComponent {
  chatRef = agent({
    assistantId: 'gen_ui_agent',
    threadId: signal(null),
  });
 
  myViews = myViews;
}

That's it. When the agent returns a JSON spec as a message, ChatComponent detects it and renders through your view registry.

Creating View Components

Each view component receives its props as Angular inputs. The component name in the spec's type field maps to the key in your views() call.

// weather-card.component.ts
import { Component, input } from '@angular/core';
 
@Component({
  selector: 'app-weather-card',
  standalone: true,
  template: `
    <div class="p-4 rounded-lg border">
      <h3 class="font-bold">{{ city() }}</h3>
      <p>{{ temperature() }}ยฐF โ€” {{ condition() }}</p>
    </div>
  `,
})
export class WeatherCardComponent {
  readonly city = input.required<string>();
  readonly temperature = input.required<number>();
  readonly condition = input.required<string>();
}

When the agent returns:

{
  "root": "r1",
  "elements": {
    "r1": {
      "type": "weather_card",
      "props": {
        "city": "Seattle",
        "temperature": 62,
        "condition": "Cloudy"
      }
    }
  }
}

The render pipeline instantiates WeatherCardComponent with those props.

Streaming Behavior

Because the JSON is parsed character-by-character as tokens arrive:

  • Components render as soon as enough of the spec is available
  • String props grow visibly as tokens stream (e.g., a title filling in letter by letter)
  • Completed elements keep their object reference โ€” only the currently-streaming element triggers re-renders
  • The loading input is true while the agent is still streaming

State Store

For interactive generative UI (forms, selections), pass a StateStore via the [store] input:

import { signalStateStore } from '@ngaf/render';
 
@Component({
  template: `
    <chat [agent]="chatRef" [views]="myViews" [store]="store" />
  `,
})
export class InteractiveChatComponent {
  store = signalStateStore({ selectedItem: null });
  // ...
}

The store enables two-way data binding between generative UI components and your application via $state and $bindState prop expressions in specs.

A2UI Protocol

For agents that implement Google's A2UI v0.9 protocol, ChatComponent auto-detects A2UI payloads (prefixed with ---a2ui_JSON---) and renders them using the built-in A2UI catalog. See the A2UI guide for details.

What's Next