Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
eda1e6c
docs: design system integration guide, custom components guide, YouTu…
Mar 12, 2026
53f1284
docs: cross-link custom components ↔ design system guides
Mar 12, 2026
9a83a2b
docs: clarify DEFAULT_CATALOG spread is optional
Mar 12, 2026
3001ea6
Apply suggestions from code review
zeroasterisk Mar 12, 2026
6702eba
docs: address PR #824 review comments
Mar 12, 2026
9497734
docs: add render_macros:false to prevent Jinja2 eval of Angular templ…
Mar 12, 2026
a52cc2b
feat(dojo): Add A2UI Dojo and Mock Scenarios
Mar 14, 2026
d776a73
feat(dojo): implement comprehensive visual design and layout polish f…
Mar 14, 2026
47cd0f9
fix(composer): remove edge runtime to fix Next.js build errors, prefe…
Mar 15, 2026
9a62825
feat(dojo): Add scenario harvest, mobile layout, and UX evaluations
Mar 15, 2026
63d3c37
fix(composer): remove opennextjs-cloudflare dependency for Vercel dep…
Mar 15, 2026
6e574a0
fix(dojo): fix progress timeline and start renderers in empty state
Mar 15, 2026
8b08a4b
feat(dojo): enable URL parameter deep linking for scenarios and timel…
Mar 15, 2026
aa0f6b7
feat: wire up A2UIViewer to dojo scrubber stream via useA2UISurface hook
Mar 15, 2026
4b76c95
feat(dojo): polish timeline scrubber, sync A2UI transcoder and adjust…
Mar 15, 2026
952d1a1
feat(dojo): fix A2UISurface hook compatibility for React renderer and…
Mar 15, 2026
f634dcf
feat(dojo): improve visual feedback for timeline and scrubber messages
Mar 15, 2026
e64b6d0
feat(dojo): integrate northstar-tour scenario, remove CopilotKit, and…
Mar 15, 2026
e6e81af
fix(dojo): use real v0.8 sample scenarios and fix renderer pipeline
Mar 16, 2026
3304271
fix(dojo): use @copilotkit/a2ui-renderer instead of local @a2ui/react
Mar 16, 2026
b3bee0a
feat(dojo): single renderer pane, step summaries, hide Discord mock
Mar 16, 2026
06c93b7
fix(dojo): remove broken v0.9 scenarios that crash renderer
Mar 16, 2026
d723af3
fix(dojo): use standard catalog for rizzcharts-chart scenario so it r…
Mar 16, 2026
7a5e52b
fix(dojo): collapse mobile header to single compact row
Mar 16, 2026
0b6409a
feat(dojo): add contact_multiple_surfaces and rizzcharts scenarios
Mar 16, 2026
0efc86d
fix(dojo): render markdown headings in A2UIViewer via context provider
Mar 16, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
428 changes: 378 additions & 50 deletions docs/guides/custom-components.md

Large diffs are not rendered by default.

184 changes: 184 additions & 0 deletions docs/guides/design-system-integration.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
---
render_macros: false
---

# Integrating A2UI into an Existing Design System

This guide walks through adding A2UI to an **existing** Angular application that already uses a component library (like Angular Material). Instead of using the A2UI basic catalog, you'll wrap your own Material components as A2UI components — so agents generate UI that matches your design system.

> **Prerequisites**: An Angular 19+ application with a component library installed (this guide uses Angular Material). Familiarity with Angular components and dependency injection.

## Overview

Adding A2UI to an existing app involves four steps:

1. **Install** the A2UI Angular renderer and web_core packages
2. **Wrap** your existing components as A2UI custom components
3. **Register** them in a custom catalog
4. **Connect** to an A2A-compatible agent

The key insight: A2UI doesn't replace your design system — it wraps it. Your existing components become the rendering targets for agent-generated UI. Agents compose your Material buttons, cards, and inputs — not generic A2UI ones.

## Step 1: Install A2UI Packages

```bash
npm install @a2ui/angular @a2ui/web_core
```

The `@a2ui/angular` package provides:

- `DynamicComponent` — base class for wrapping your components as A2UI-compatible
- `Catalog` injection token — for providing your catalog to the renderer
- `configureChatCanvasFeatures()` — helper for wiring everything together

## Step 2: Wrap Your Components

Create A2UI wrappers around your existing Material components. Each wrapper extends `DynamicComponent` and delegates rendering to your Material component:

```typescript
// a2ui-catalog/material-button.ts
import { DynamicComponent } from '@a2ui/angular';
import * as Types from '@a2ui/web_core/types/types';
import { Component, computed, input } from '@angular/core';
import { MatButton } from '@angular/material/button';

@Component({
selector: 'a2ui-mat-button',
imports: [MatButton],
template: `
<button mat-raised-button [color]="resolvedColor()">
{{ resolvedLabel() }}
</button>
`,
})
export class MaterialButton extends DynamicComponent<Types.CustomNode> {
readonly label = input.required<any>();
readonly color = input<any>();

protected resolvedLabel = computed(() => this.resolvePrimitive(this.label()));
protected resolvedColor = computed(() =>
this.resolvePrimitive(this.color() ?? null) || 'primary'
);
}
```

The wrapper is thin — it just maps A2UI properties to your Material component's API.

## Step 3: Register a Custom Catalog

Build a catalog from your wrapped components. You do **not** need to include the A2UI basic catalog — your design system provides the components:

```typescript
// a2ui-catalog/catalog.ts
import { Catalog } from '@a2ui/angular';
import { inputBinding } from '@angular/core';

// No DEFAULT_CATALOG spread — your Material components ARE the catalog
export const MATERIAL_CATALOG = {
Button: {
type: () => import('./material-button').then((r) => r.MaterialButton),
bindings: ({ properties }) => [
inputBinding('label', () => properties['label'] || ''),
inputBinding('color', () => properties['color'] || undefined),
],
},
Card: {
type: () => import('./material-card').then((r) => r.MaterialCard),
bindings: ({ properties }) => [
inputBinding('title', () => properties['title'] || undefined),
inputBinding('subtitle', () => properties['subtitle'] || undefined),
],
},
// ... wrap more of your Material components
} as Catalog;
```

You can also mix approaches — use some basic catalog components alongside your custom ones:

```typescript
import { DEFAULT_CATALOG } from '@a2ui/angular';

export const MIXED_CATALOG = {
...DEFAULT_CATALOG, // A2UI basic components as fallback
Button: /* your Material button overrides the basic one */,
Card: /* your Material card */,
} as Catalog;
```

The basic components are entirely optional. If your design system already covers what you need, expose only your own components.

## Step 4: Wire It Up

```typescript
// app.config.ts
import {
configureChatCanvasFeatures,
usingA2aService,
usingA2uiRenderers,
} from '@a2a_chat_canvas/config';
import { MATERIAL_CATALOG } from './a2ui-catalog/catalog';
import { theme } from './theme';

export const appConfig: ApplicationConfig = {
providers: [
// ... your existing providers (Material, Router, etc.)
configureChatCanvasFeatures(
usingA2aService(MyA2aService),
usingA2uiRenderers(MATERIAL_CATALOG, theme),
),
],
};
```

## Step 5: Add the Chat Canvas

The chat canvas is the container where A2UI surfaces are rendered. Add it alongside your existing layout:

```html
<!-- app.component.html -->
<div class="app-layout">
<!-- Your existing app content -->
<mat-sidenav-container>
<mat-sidenav>...</mat-sidenav>
<mat-sidenav-content>
<router-outlet />
</mat-sidenav-content>
</mat-sidenav-container>

<!-- A2UI chat canvas -->
<a2a-chat-canvas />
</div>
```

## What Changes, What Doesn't

| Aspect | Before A2UI | After A2UI |
|--------|------------|------------|
| Your existing pages | Material components | Material components (unchanged) |
| Agent-generated UI | Not possible | Rendered via your Material wrappers |
| Component library | Angular Material | Angular Material (unchanged) |
| Design consistency | Your theme | Your theme (agents use your components) |

Your existing app is untouched. A2UI adds a rendering layer where agents compose **your** components.

## Theming

Because agents render your Material components, theming is automatic — your existing Material theme applies. You can optionally map tokens for any A2UI basic components you include:

```typescript
// theme.ts
import { Theme } from '@a2ui/angular';

export const theme: Theme = {
// Map your Material design tokens to A2UI
// See the Theming guide for full details
};
```

See the [Theming Guide](theming.md) for complete theming documentation.

## Next Steps

- [Custom Components](custom-components.md) — Add specialized components to your catalog (Maps, Charts, YouTube, etc.)
- [Theming Guide](theming.md) — Deep dive into theming
- [Agent Development](agent-development.md) — Build agents that generate A2UI using your catalog
1 change: 1 addition & 0 deletions mkdocs.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ nav:
- Client Setup: guides/client-setup.md
- Agent Development: guides/agent-development.md
- Renderer Development: guides/renderer-development.md
- Design System Integration: guides/design-system-integration.md
- Custom Components: guides/custom-components.md
- Theming & Styling: guides/theming.md
- Reference:
Expand Down
1 change: 1 addition & 0 deletions renderers/react/src/core/A2UIRenderer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ export const A2UIRenderer = memo(function A2UIRenderer({

// Get surface - this will re-render when version changes
const surface = getSurface(surfaceId);
console.log('A2UIRenderer: surfaceId=', surfaceId, 'surface=', surface, 'version=', version);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Debugging console.log should be removed from production code to maintain clean logs and avoid potential performance overhead in the rendering loop.


// Memoize surface styles to prevent object recreation
// Matches Lit renderer's transformation logic in surface.ts
Expand Down
4 changes: 3 additions & 1 deletion renderers/web_core/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion renderers/web_core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@
"license": "Apache-2.0",
"devDependencies": {
"@types/node": "^24.11.0",
"typescript": "^5.8.3",
"typescript": "^5.9.3",
"wireit": "^0.15.0-pre.2",
"zod-to-json-schema": "^3.25.1"
},
Expand Down
37 changes: 37 additions & 0 deletions samples/agent/adk/contact_lookup/record_scenario.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import asyncio
import json
import logging
from agent import ContactAgent

logging.basicConfig(level=logging.INFO)

async def main():
agent = ContactAgent(base_url="http://localhost:10006", use_ui=True)
query = "Find Alex in Marketing"
session_id = "test_session_2"

print(f"Running agent with query: {query}")

messages = []

async for event in agent.stream(query, session_id):
if event.get("is_task_complete"):
parts = event.get("parts", [])
for p in parts:
if p.root.metadata and p.root.metadata.get("mimeType") == "application/json+a2ui":
# Some payloads are already a list, some are dicts
if isinstance(p.root.data, list):
messages.extend(p.root.data)
else:
messages.append(p.root.data)

if messages:
out_path = "/home/node/.openclaw/projects/A2UI/tools/composer/src/data/dojo/contact-lookup.json"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The output path is hardcoded to an absolute path specific to the author's local environment (/home/node/...). This will cause the script to fail for other users. Please use a relative path or an environment variable instead.

Suggested change
out_path = "/home/node/.openclaw/projects/A2UI/tools/composer/src/data/dojo/contact-lookup.json"
import os
base_dir = os.path.dirname(os.path.abspath(__file__))
out_path = os.path.join(base_dir, "../../../../tools/composer/src/data/dojo/contact-lookup.json")

with open(out_path, "w") as f:
json.dump(messages, f, indent=2)
print(f"Recorded {len(messages)} A2UI message parts to {out_path}")
else:
print("No A2UI messages produced.")

if __name__ == "__main__":
asyncio.run(main())
4 changes: 4 additions & 0 deletions samples/agent/adk/contact_lookup/record_scenario.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
#!/bin/bash
source ~/.openclaw/credentials/secrets.sh
export GEMINI_API_KEY="$GEMINI_API_KEY_ALAN_WORK"
Comment on lines +2 to +3
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The script relies on a specific local file structure (~/.openclaw/...) and a custom environment variable (GEMINI_API_KEY_ALAN_WORK). This limits the portability of the sample. Consider using standard environment variables and providing instructions for setup.

uv run record_scenario.py
37 changes: 37 additions & 0 deletions samples/agent/adk/restaurant_finder/record_scenario.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import asyncio
import json
import logging
from agent import RestaurantAgent

logging.basicConfig(level=logging.INFO)

async def main():
agent = RestaurantAgent(base_url="http://localhost:10007", use_ui=True)
query = "Find me Szechuan restaurants in New York"
session_id = "test_session_3"

print(f"Running agent with query: {query}")

messages = []

async for event in agent.stream(query, session_id):
if event.get("is_task_complete"):
parts = event.get("parts", [])
for p in parts:
if p.root.metadata and p.root.metadata.get("mimeType") == "application/json+a2ui":
# Some payloads are already a list, some are dicts
if isinstance(p.root.data, list):
messages.extend(p.root.data)
else:
messages.append(p.root.data)

if messages:
out_path = "/home/node/.openclaw/projects/A2UI/tools/composer/src/data/dojo/restaurant-finder.json"
with open(out_path, "w") as f:
json.dump(messages, f, indent=2)
print(f"Recorded {len(messages)} A2UI message parts to {out_path}")
else:
print("No A2UI messages produced.")

if __name__ == "__main__":
asyncio.run(main())
4 changes: 4 additions & 0 deletions samples/agent/adk/restaurant_finder/record_scenario.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
#!/bin/bash
source ~/.openclaw/credentials/secrets.sh
export GEMINI_API_KEY="$GEMINI_API_KEY_ALAN_WORK"
uv run record_scenario.py
Loading