Skip to content
2 changes: 0 additions & 2 deletions cockpit/chat/generative-ui/angular/src/app/app.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,11 @@
import { ApplicationConfig } from '@angular/core';
import { provideAgent } from '@cacheplane/angular';
import { provideChat } from '@cacheplane/chat';
import { provideRender } from '@cacheplane/render';
import { environment } from '../environments/environment';

export const appConfig: ApplicationConfig = {
providers: [
provideAgent({ apiUrl: environment.langGraphApiUrl }),
provideChat({}),
provideRender({}),
],
};
Original file line number Diff line number Diff line change
@@ -1,42 +1,28 @@
// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0
import { Component } from '@angular/core';
import { ChatComponent, ChatGenerativeUiComponent } from '@cacheplane/chat';
import { ChatComponent, views } from '@cacheplane/chat';
import { agent } from '@cacheplane/angular';
import { environment } from '../environments/environment';
import { WeatherCardComponent } from './views/weather-card.component';
import { StatCardComponent } from './views/stat-card.component';
import { ContainerComponent } from './views/container.component';

const myViews = views({
weather_card: WeatherCardComponent,
stat_card: StatCardComponent,
container: ContainerComponent,
});

/**
* GenerativeUiComponent demonstrates dynamic UI generation within
* chat messages using ChatComponent and ChatGenerativeUiComponent.
* The agent embeds render specs that are rendered as live components.
*/
@Component({
selector: 'app-generative-ui',
standalone: true,
imports: [ChatComponent, ChatGenerativeUiComponent],
template: `
<div class="flex h-screen">
<chat [ref]="stream" class="flex-1 min-w-0" />
<aside class="w-80 shrink-0 border-l overflow-y-auto p-4 space-y-4"
style="border-color: var(--chat-border, #333); background: var(--chat-bg, #171717); color: var(--chat-text, #e0e0e0);">
<h3 class="text-xs font-semibold uppercase tracking-wide"
style="color: var(--chat-text-muted, #777);">Generative UI</h3>
<chat-generative-ui [ref]="stream" />
<div class="mt-4">
<h4 class="text-xs font-semibold uppercase tracking-wide mb-2"
style="color: var(--chat-text-muted, #777);">How It Works</h4>
<p class="text-xs" style="color: var(--chat-text-muted, #777);">
The agent embeds JSON render specs in chat messages.
These specs are detected and rendered as live Angular
components using the render registry.
</p>
</div>
</aside>
</div>
`,
imports: [ChatComponent],
template: `<chat [ref]="agentRef" [views]="myViews" class="block h-screen" />`,
})
export class GenerativeUiComponent {
protected readonly stream = agent({
protected readonly agentRef = agent({
apiUrl: environment.langGraphApiUrl,
assistantId: environment.streamingAssistantId,
assistantId: environment.generativeUiAssistantId,
});
protected readonly myViews = myViews;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0
import { Component, input } from '@angular/core';
import type { Spec } from '@json-render/core';
import { RenderElementComponent } from '@cacheplane/render';

@Component({
selector: 'app-container',
standalone: true,
imports: [RenderElementComponent],
template: `
<div class="flex flex-col gap-3">
@for (key of childKeys(); track key) {
<render-element [elementKey]="key" [spec]="spec()" />
}
</div>
`,
})
export class ContainerComponent {
readonly childKeys = input<string[]>([]);
readonly spec = input.required<Spec>();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0
import { Component, input } from '@angular/core';

@Component({
selector: 'app-stat-card',
standalone: true,
template: `
<div class="rounded-lg border border-white/10 bg-white/5 p-4 backdrop-blur-sm">
<div class="text-xs font-medium uppercase tracking-wider text-white/40 mb-1">{{ label() }}</div>
<div class="text-xl font-semibold text-white">{{ value() }}</div>
</div>
`,
})
export class StatCardComponent {
readonly label = input<string>('');
readonly value = input<string>('');
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0
import { Component, input } from '@angular/core';

@Component({
selector: 'app-weather-card',
standalone: true,
template: `
<div class="rounded-xl border border-white/10 bg-white/5 p-5 backdrop-blur-sm">
<div class="flex items-center justify-between mb-3">
<h3 class="text-lg font-semibold text-white">{{ city() }}</h3>
<span class="text-2xl">{{ weatherEmoji() }}</span>
</div>
<div class="text-4xl font-bold text-white mb-1">{{ temperature() }}°F</div>
<div class="text-sm text-white/60">{{ condition() }}</div>
</div>
`,
})
export class WeatherCardComponent {
readonly city = input<string>('');
readonly temperature = input<number>(0);
readonly condition = input<string>('');

weatherEmoji(): string {
const c = this.condition().toLowerCase();
if (c.includes('sun') || c.includes('clear')) return '☀️';
if (c.includes('cloud') || c.includes('overcast')) return '☁️';
if (c.includes('rain')) return '🌧️';
if (c.includes('snow')) return '❄️';
if (c.includes('storm') || c.includes('thunder')) return '⛈️';
return '🌤️';
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
export const environment = {
production: false,
langGraphApiUrl: 'http://localhost:4508/api',
streamingAssistantId: 'c-generative-ui',
langGraphApiUrl: 'http://localhost:4310/api',
generativeUiAssistantId: 'generative_ui',
};
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
export const environment = {
production: true,
langGraphApiUrl: '/api',
streamingAssistantId: 'c-generative-ui',
generativeUiAssistantId: 'generative_ui',
};
91 changes: 44 additions & 47 deletions cockpit/chat/generative-ui/python/docs/guide.md
Original file line number Diff line number Diff line change
@@ -1,76 +1,73 @@
# Chat Generative UI with @cacheplane/chat
# Generative UI with Streaming Auto-Detection

<Summary>
Render dynamic UI components within chat messages using
ChatGenerativeUiComponent. The agent embeds JSON render specs
in responses that are rendered as live Angular components.
Render dynamic UI components within chat messages using the streaming
auto-detection pipeline. As tokens stream in, the system detects JSON,
parses it incrementally, and renders Angular components in real time.
</Summary>

<Prompt>
Add generative UI to your chat interface using `ChatGenerativeUiComponent`
from `@cacheplane/chat` and `provideRender()` from `@cacheplane/render`.
Configure both providers to enable spec detection and rendering.
Add generative UI to your chat interface using `views()` from
`@cacheplane/chat`. Register view components and pass them to
`ChatComponent` via the `[views]` input.
</Prompt>

<Steps>
<Step title="Configure the render provider">
<Step title="Define view components">

Generative UI requires both `provideChat()` and `provideRender()`:

```typescript
import { provideRender } from '@cacheplane/render';
import { provideChat } from '@cacheplane/chat';

export const appConfig: ApplicationConfig = {
providers: [
provideStreamResource({ apiUrl: environment.langGraphApiUrl }),
provideChat({}),
provideRender({}),
],
};
```
Create Angular components for each UI type the agent can emit.
Each component uses `input()` signals to receive props from the
rendered spec.

</Step>
<Step title="Create a spec-emitting agent">
<Step title="Register views">

Configure the backend agent to include JSON render specs in its
responses using fenced code blocks with the `render-spec` tag.
Use the `views()` function to map spec type names to Angular components:

</Step>
<Step title="Detect specs in messages">
```typescript
import { views } from '@cacheplane/chat';

ChatGenerativeUiComponent automatically scans messages for render
spec code blocks and extracts them for rendering.
const myViews = views({
weather_card: WeatherCardComponent,
stat_card: StatCardComponent,
container: ContainerComponent,
});
```

</Step>
<Step title="Render with ChatGenerativeUiComponent">
<Step title="Bind views to ChatComponent">

Use the component in your template alongside ChatComponent:
Pass the view map to `ChatComponent` via the `[views]` input:

```html
<chat [ref]="stream" />
<chat-generative-ui [ref]="stream" />
<chat [ref]="agentRef" [views]="myViews" />
```

</Step>
<Step title="Customize the component registry">
<Step title="Configure the agent prompt">

Register custom Angular components to handle specific spec types:

```typescript
provideRender({
registry: defineAngularRegistry({
card: MyCardComponent,
chart: MyChartComponent,
}),
})
```
Instruct the LLM to respond with raw JSON following the Spec schema.
No code fences or markdown — just valid JSON so the streaming pipeline
can detect and parse it incrementally.

</Step>
</Steps>

## How Streaming Auto-Detection Works

1. **Token streaming** — The LLM streams response tokens to the client.
2. **ContentClassifier** — Inspects the incoming token buffer and detects
when the content is JSON rather than plain text or markdown.
3. **Partial JSON parser** — As JSON tokens arrive, a partial parser
builds an incremental parse tree without waiting for the full payload.
4. **ParseTreeStore** — Materializes the partial parse tree into a live
`Spec` object (elements map + root key) that updates on every chunk.
5. **Component rendering** — The `[views]` registry resolves each element
type to an Angular component, which renders incrementally as the spec
grows.

<Tip>
Generative UI bridges the gap between conversational AI and rich
interactive interfaces — the agent can create forms, dashboards,
and visualizations on the fly.
Because detection and parsing happen on every streamed chunk, the user
sees UI components materialize progressively — cards appear and fill in
as the LLM generates the JSON structure.
</Tip>
2 changes: 1 addition & 1 deletion cockpit/chat/generative-ui/python/langgraph.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"graphs": {
"c-generative-ui": "./src/graph.py:graph"
"generative_ui": "./src/graph.py:graph"
},
"dependencies": ["."],
"python_version": "3.12",
Expand Down
54 changes: 39 additions & 15 deletions cockpit/chat/generative-ui/python/prompts/generative-ui.md
Original file line number Diff line number Diff line change
@@ -1,22 +1,46 @@
# Chat Generative UI Assistant
# Generative UI Assistant

You are an assistant that demonstrates dynamic UI generation within
chat responses using render specs.
You are a generative-UI assistant. You MUST respond with **raw JSON only** — no markdown, no code fences, no explanation text. Your entire response must be a single valid JSON object following the Spec format below.

When the user asks you to create a UI element, include a JSON render spec
in your response using a fenced code block with the `render-spec` language tag.
For example:
## Spec Schema

```render-spec
A **Spec** is a JSON object with two required top-level keys:

```
{
"elements": { [key: string]: Element },
"rootKey": string
}
```

An **Element** has:

```
{
"type": "card",
"props": { "title": "Generated Card" },
"children": [
{ "type": "text", "props": { "content": "This card was generated by the AI" } }
]
"type": string, // component type name
"props": { ... }, // component-specific properties
"children?": string[] // ordered list of element keys (references into `elements`)
}
```

The frontend will detect these specs and render them as live Angular
components inline within the chat message. Explain what you are generating
and why in the surrounding text.
## Available Component Types

| Type | Props | Children |
|-----------------|--------------------------------------------------------------|----------|
| `container` | *(none)* | Yes |
| `weather_card` | `city` (string), `temperature` (number), `condition` (string)| No |
| `stat_card` | `label` (string), `value` (string) | No |

## Rules

1. Respond ONLY with valid JSON. No markdown. No code fences. No surrounding text.
2. Every element referenced in a `children` array must exist as a key in `elements`.
3. `rootKey` must reference a key that exists in `elements`.
4. Use `container` to group multiple cards together.
5. Choose component types that best match the user's request.

## Example Response

If the user asks "What's the weather in Chicago and New York?", respond exactly like:

{"elements":{"root":{"type":"container","props":{},"children":["chicago","nyc"]},"chicago":{"type":"weather_card","props":{"city":"Chicago","temperature":45,"condition":"Partly Cloudy"}},"nyc":{"type":"weather_card","props":{"city":"New York","temperature":52,"condition":"Sunny"}}},"rootKey":"root"}
Loading
Loading