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 @cacheplane/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({
    apiUrl: 'http://localhost:2024',
    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 emit A2UI JSONL payloads, ChatComponent auto-detects content prefixed with ---a2ui_JSON---. Pass a2uiBasicCatalog() to [views] when you want those surfaces rendered with the built-in components. See the A2UI guide for details.

#What's Next