Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 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
1dd1c86
fix(dojo): remove h-full overflow-hidden from renderer frame so conte…
Mar 16, 2026
0d8c1ca
fix(dojo): add missing confirmation-column component to restaurant-co…
Mar 16, 2026
5275e40
dojo: hide mock browser chrome traffic light dots on mobile viewport
Mar 16, 2026
2298730
fix(dojo): set copilotkit route to force-dynamic to fix 500 on page load
Mar 16, 2026
a220864
feat(dojo): URL state, config panel, nav link, curated scenarios
Mar 16, 2026
f6820ac
fix: use npm lockfile for Vercel compatibility
Mar 16, 2026
5951304
fix: gitignore pnpm-lock.yaml to prevent Vercel from using pnpm
Mar 16, 2026
0f78d2a
feat(dojo): streaming simulation, 3-tab left pane, remove header scen…
Mar 16, 2026
1f43fd5
fix(dojo): correct JSONL streaming — chunk per message, not per line
Mar 16, 2026
887e754
feat(dojo): add user interaction to restaurant-booking scenario
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);

// 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, which makes the script non-portable for other developers. It's better to construct a path relative to the script's location.

You can achieve this using pathlib. You'll need to add from pathlib import Path at the top of the file. Also, consider ensuring the directory exists before writing, for example with out_path.parent.mkdir(parents=True, exist_ok=True) before the with open(...) block.

Suggested change
out_path = "/home/node/.openclaw/projects/A2UI/tools/composer/src/data/dojo/contact-lookup.json"
out_path = Path(__file__).resolve().parents[4] / "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
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 path to secrets.sh is hardcoded to a user-specific home directory (~/.openclaw/...). This makes the script non-portable. For a sample script, consider parameterizing this path or documenting that it needs to be changed by the user.

export GEMINI_API_KEY="$GEMINI_API_KEY_ALAN_WORK"
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"
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, which makes the script non-portable. It's better to construct a path relative to the script's location.

You can achieve this using pathlib. You'll need to add from pathlib import Path at the top of the file. Also, consider ensuring the directory exists before writing, for example with out_path.parent.mkdir(parents=True, exist_ok=True) before the with open(...) block.

Suggested change
out_path = "/home/node/.openclaw/projects/A2UI/tools/composer/src/data/dojo/restaurant-finder.json"
out_path = Path(__file__).resolve().parents[4] / "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
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 path to secrets.sh is hardcoded to a user-specific home directory (~/.openclaw/...). This makes the script non-portable. For a sample script, consider making this path configurable, for example, by reading it from an environment variable or a command-line argument.

export GEMINI_API_KEY="$GEMINI_API_KEY_ALAN_WORK"
uv run record_scenario.py
Loading