-
Notifications
You must be signed in to change notification settings - Fork 1.1k
feat(composer): A2UI Dojo — interactive JSONL playback and streaming viewer #857
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 7 commits
eda1e6c
53f1284
9a83a2b
3001ea6
6702eba
9497734
a52cc2b
d776a73
47cd0f9
9a62825
63d3c37
6e574a0
8b08a4b
aa0f6b7
4b76c95
952d1a1
f634dcf
e64b6d0
e6e81af
3304271
b3bee0a
06c93b7
d723af3
1dd1c86
0d8c1ca
5275e40
2298730
a220864
f6820ac
5951304
0f78d2a
1f43fd5
887e754
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
| 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 |
| 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" | ||
| 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()) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| #!/bin/bash | ||
| source ~/.openclaw/credentials/secrets.sh | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| export GEMINI_API_KEY="$GEMINI_API_KEY_ALAN_WORK" | ||
| uv run 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" | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 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
Suggested change
|
||||||
| 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()) | ||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| #!/bin/bash | ||
| source ~/.openclaw/credentials/secrets.sh | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| export GEMINI_API_KEY="$GEMINI_API_KEY_ALAN_WORK" | ||
| uv run record_scenario.py | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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 addfrom pathlib import Pathat the top of the file. Also, consider ensuring the directory exists before writing, for example without_path.parent.mkdir(parents=True, exist_ok=True)before thewith open(...)block.