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