Chat · Guides

Generative UI

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

The detection and rendering are adapter-neutral: the same [views] path works whether the agent is driven by the LangGraph adapter or the AG-UI adapter. The examples below use @threadplane/langgraph, but only the provideAgent/injectAgent imports change for AG-UI.

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 { injectAgent, provideAgent } from '@threadplane/langgraph';
import { ChatComponent, views } from '@threadplane/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],
  providers: [
    provideAgent({
      apiUrl: 'http://localhost:2024',
      assistantId: 'gen_ui_agent',
      threadId: signal(null),
    }),
  ],
  template: `
    <div style="height: 100vh;">
      <chat [agent]="chatRef" [views]="myViews" />
    </div>
  `,
})
export class ChatPageComponent {
  chatRef = injectAgent();
 
  myViews = myViews;
}

That's it. When the agent returns a JSON spec as a message, ChatComponent detects it and renders it 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 '@threadplane/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.

Event Handlers

When a spec wires an on event binding (for example a button's on: { click: 'submitForm' }), the named handler is looked up in the [handlers] map you pass to <chat>. The map is Record<string, (params) => unknown | Promise<unknown>> — each key is a handler name a spec can reference, and the value runs when the event fires.

import { Component, signal } from '@angular/core';
import { injectAgent, provideAgent } from '@threadplane/langgraph';
import { ChatComponent, views } from '@threadplane/chat';
import { ContactFormComponent } from './contact-form.component';
 
@Component({
  selector: 'app-chat',
  standalone: true,
  imports: [ChatComponent],
  providers: [
    provideAgent({
      apiUrl: 'http://localhost:2024',
      assistantId: 'gen_ui_agent',
      threadId: signal(null),
    }),
  ],
  template: `
    <div style="height: 100vh;">
      <chat [agent]="chatRef" [views]="myViews" [handlers]="handlers" />
    </div>
  `,
})
export class ChatPageComponent {
  protected readonly chatRef = injectAgent();
 
  protected readonly myViews = views({ contact_form: ContactFormComponent });
 
  // A spec's `on` binding referencing "submitForm" invokes this function,
  // passing the element's resolved params.
  protected readonly handlers = {
    submitForm: (params: Record<string, unknown>) => {
      console.log('form submitted', params);
    },
  };
}

Handlers can return a value or a Promise — async handlers let a spec await a result (such as a network call) before the UI advances.

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