From eda1e6c9d087b79cd8393041f83d8f0995c6f121 Mon Sep 17 00:00:00 2001 From: Zaf Date: Thu, 12 Mar 2026 03:10:34 +0000 Subject: [PATCH 1/6] docs: design system integration guide, custom components guide, YouTube component MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - New guide: design-system-integration.md — step-by-step for adding A2UI to an existing Material Angular application - Rewritten guide: custom-components.md — complete walkthrough for YouTube, Maps, and Charts custom components (replaces TODO skeleton) - New sample component: YouTube embed for rizzcharts catalog - Updated rizzcharts catalog.ts to include YouTube component - Friction log documenting 8 friction points (P2/P3) encountered during development, with recommendations - Added Design System Integration to mkdocs nav --- docs/guides/custom-components.md | 427 +++++++++++++++--- docs/guides/design-system-integration.md | 147 ++++++ docs/guides/friction-log-custom-components.md | 121 +++++ mkdocs.yaml | 1 + .../rizzcharts/src/a2ui-catalog/catalog.ts | 8 + .../rizzcharts/src/a2ui-catalog/youtube.ts | 122 +++++ 6 files changed, 774 insertions(+), 52 deletions(-) create mode 100644 docs/guides/design-system-integration.md create mode 100644 docs/guides/friction-log-custom-components.md create mode 100644 samples/client/angular/projects/rizzcharts/src/a2ui-catalog/youtube.ts diff --git a/docs/guides/custom-components.md b/docs/guides/custom-components.md index 9915fa4000..afdd820eac 100644 --- a/docs/guides/custom-components.md +++ b/docs/guides/custom-components.md @@ -1,83 +1,406 @@ -# Custom Component Catalogs +# Custom Components -Extend A2UI by defining **custom catalogs** that include your own components alongside standard A2UI components. +Extend A2UI with your own components — maps, charts, video players, or anything your application needs. -## Why Custom Catalogs? +## Why Custom Components? -The A2UI Standard Catalog provides common UI elements (buttons, text fields, etc.), but your application might need specialized components: +The A2UI Standard Catalog covers common UI elements (text, buttons, inputs, layout), but real applications need specialized components: -- **Domain-specific widgets**: Stock tickers, medical charts, CAD viewers -- **Third-party integrations**: Google Maps, payment forms, chat widgets -- **Brand-specific components**: Custom date pickers, product cards, dashboards +- **Maps**: Google Maps, Mapbox, Leaflet +- **Charts**: Chart.js, D3, Recharts +- **Media**: YouTube embeds, audio visualizers, 3D viewers +- **Domain-specific**: Stock tickers, medical imaging, CAD viewers -**Custom catalogs** are collections of components that can include: -- Standard A2UI components (Text, Button, TextField, etc.) -- Your custom components (GoogleMap, StockTicker, etc.) -- Third-party components +Custom components let agents generate UI that includes **any** component your app supports — not just what's in the standard catalog. -You register entire catalogs with your client application, not individual components. This allows agents and clients to agree on a shared, extended set of components while maintaining security and type safety. +## How It Works -## How Custom Catalogs Work +``` +Agent ──generates──> A2UI JSON ──references──> "GoogleMap" component + │ +Client ──registers──> Catalog { GoogleMap: ... } ───┘ + │ +Angular ──renders──> <───┘ +``` -1. **Client Defines Catalog**: You create a catalog definition that lists both standard and custom components. -2. **Client Registers Catalog**: You register the catalog (and its component implementations) with your client app. -3. **Client Announces Support**: The client informs the agent which catalogs it supports. -4. **Agent Selects Catalog**: The agent chooses a catalog for a given UI surface. -5. **Agent Generates UI**: The agent generates component messages (`surfaceUpdate` in v0.8, `updateComponents` in v0.9) using components from that catalog by name. +1. You **implement** an Angular component that extends `DynamicComponent` +2. You **register** it in a catalog alongside standard components +3. The agent **references** it by name in `updateComponents` messages +4. The A2UI renderer **instantiates** your component with the agent's properties -## Defining Custom Catalogs +## Step-by-Step: Adding a YouTube Component -TODO: Add detailed guide for defining custom catalogs for each platform. +Let's add a YouTube video player as a custom A2UI component. -**Web (Lit / Angular):** +### 1. Create the Component -- How to define a catalog with both standard and custom components -- How to register the catalog with the A2UI client -- How to implement custom component classes +Custom components extend `DynamicComponent` from `@a2ui/angular`: -**Flutter:** +```typescript +// a2ui-catalog/youtube.ts +import { DynamicComponent } from '@a2ui/angular'; +import * as Primitives from '@a2ui/web_core/types/primitives'; +import * as Types from '@a2ui/web_core/types/types'; +import { + ChangeDetectionStrategy, + Component, + computed, + input, +} from '@angular/core'; +import { DomSanitizer } from '@angular/platform-browser'; -- How to define custom catalogs using GenUI -- How to register custom component renderers +@Component({ + selector: 'a2ui-youtube', + changeDetection: ChangeDetectionStrategy.Eager, + styles: ` + :host { + display: block; + flex: var(--weight); + padding: 8px; + } + .video-container { + position: relative; + width: 100%; + padding-bottom: 56.25%; /* 16:9 aspect ratio */ + border-radius: 8px; + overflow: hidden; + } + iframe { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + border: none; + } + h3 { + margin: 8px 0 4px; + color: var(--mat-sys-on-surface); + } + `, + template: ` + @if (resolvedVideoId()) { + @if (resolvedTitle()) { +

{{ resolvedTitle() }}

+ } +
+ +
+ } + `, +}) +export class YouTube extends DynamicComponent { + readonly videoId = input.required(); + protected readonly resolvedVideoId = computed(() => + this.resolvePrimitive(this.videoId()), + ); -**See working examples:** + readonly title = input(); + protected readonly resolvedTitle = computed(() => + this.resolvePrimitive(this.title() ?? null), + ); -- [Lit samples](https://github.com/google/a2ui/tree/main/samples/client/lit) -- [Angular samples](https://github.com/google/a2ui/tree/main/samples/client/angular) -- [Flutter GenUI docs](https://docs.flutter.dev/ai/genui) + protected readonly safeUrl = computed(() => { + const id = this.resolvedVideoId(); + if (!id) return null; + const url = `https://www.youtube.com/embed/${id}`; + return this.sanitizer.bypassSecurityTrustResourceUrl(url); + }); -## Agent-Side: Using Components from a Custom Catalog + constructor(private sanitizer: DomSanitizer) { + super(); + } +} +``` -Once a catalog is registered on the client, agents can use components from it in `surfaceUpdate` messages. +**Key patterns:** -The agent specifies which catalog to use via the `catalogId` in the `beginRendering` message. +- Extend `DynamicComponent` for custom component types +- Use `input()` for properties the agent will set +- Use `resolvePrimitive()` to resolve values that may be literals or data-bound paths +- Use `computed()` for reactive derivations -TODO: Add examples of: +### 2. Register in the Catalog -- How agents select catalogs -- How agents reference custom components from catalogs -- How catalog versioning works +Add your component to the catalog alongside standard components: -## Data Binding and Actions +```typescript +// a2ui-catalog/catalog.ts +import { Catalog, DEFAULT_CATALOG } from '@a2ui/angular'; +import { inputBinding } from '@angular/core'; -Custom components support the same data binding and action mechanisms as standard components: +export const MY_CATALOG = { + ...DEFAULT_CATALOG, // Include all standard components -- **Data binding**: Custom components can bind properties to data model paths using JSON Pointer syntax -- **Actions**: Custom components can emit actions that the agent receives and handles + YouTube: { + type: () => import('./youtube').then((r) => r.YouTube), + bindings: ({ properties }) => [ + inputBinding('videoId', () => + ('videoId' in properties && properties['videoId']) || undefined + ), + inputBinding('title', () => + ('title' in properties && properties['title']) || undefined + ), + ], + }, +} as Catalog; +``` -## Security Considerations +**What's happening:** -When creating custom catalogs and components: +- `...DEFAULT_CATALOG` — spread the standard catalog so agents can use standard components too +- `type` — lazy-loaded import of your component class +- `bindings` — maps properties from the A2UI JSON to Angular `@Input()` values -1. **Allowlist components**: Only register components you trust in your catalogs -2. **Validate properties**: Always validate component properties from agent messages -3. **Sanitize user input**: If components accept user input, sanitize it before processing -4. **Limit API access**: Don't expose sensitive APIs or credentials to custom components +### 3. Use the Custom Catalog -TODO: Add detailed security best practices and code examples. +Update your app config to use your custom catalog instead of the default: + +```typescript +// app.config.ts +import { MY_CATALOG } from './a2ui-catalog/catalog'; + +export const appConfig: ApplicationConfig = { + providers: [ + configureChatCanvasFeatures( + usingA2aService(MyA2aService), + usingA2uiRenderers(MY_CATALOG, theme), + ), + ], +}; +``` + +### 4. Agent-Side: Using the Custom Component + +The agent references your component by name in `updateComponents`: + +```json +{ + "type": "updateComponents", + "surfaceId": "main", + "components": { + "root": { + "component": "Column", + "properties": {}, + "childIds": ["vid1"] + }, + "vid1": { + "component": "YouTube", + "properties": { + "videoId": "dQw4w9WgXcQ", + "title": "Check out this video" + } + } + } +} +``` + +The agent knows about your custom components through the catalog configuration in its prompt or system instructions. + +## More Examples + +### Google Maps Component + +A map component that displays pins from the agent's data model: + +```typescript +// a2ui-catalog/google-map.ts +@Component({ + selector: 'a2ui-map', + imports: [GoogleMapsModule], + template: ` + + @for (pin of resolvedPins(); track pin) { + + } + + `, +}) +export class GoogleMap extends DynamicComponent { + readonly zoom = input.required(); + readonly center = input.required<{ path: string } | null>(); + readonly pins = input<{ path: string }>(); + + protected resolvedZoom = computed(() => this.resolvePrimitive(this.zoom())); + protected resolvedCenter = computed(() => this.resolveLatLng(this.center())); + protected resolvedPins = computed(() => this.resolveLocations(this.pins())); + // ... (resolve helpers iterate over data model paths) +} +``` + +**Catalog entry:** + +```typescript +GoogleMap: { + type: () => import('./google-map').then((r) => r.GoogleMap), + bindings: ({ properties }) => [ + inputBinding('zoom', () => properties['zoom'] || 8), + inputBinding('center', () => properties['center'] || undefined), + inputBinding('pins', () => properties['pins'] || undefined), + inputBinding('title', () => properties['title'] || undefined), + ], +}, +``` + +**Agent JSON:** + +```json +{ + "component": "GoogleMap", + "properties": { + "zoom": 12, + "center": { "path": "/mapCenter" }, + "pins": { "path": "/restaurants" }, + "title": "Nearby Restaurants" + } +} +``` + +Maps uses **data binding** — the `center` and `pins` reference paths in the data model, so the agent can update locations dynamically via `updateDataModel`. + +### Chart Component + +A chart component using Chart.js: + +```typescript +// a2ui-catalog/chart.ts +@Component({ + selector: 'a2ui-chart', + imports: [BaseChartDirective], + template: ` +
+

{{ resolvedTitle() }}

+ +
+ `, +}) +export class Chart extends DynamicComponent { + readonly type = input.required(); + readonly title = input(); + readonly chartData = input.required(); + + protected chartType = computed(() => this.type() as ChartType); + protected resolvedTitle = computed(() => this.resolvePrimitive(this.title() ?? null)); + // ... (resolve chart data from data model paths) +} +``` + +**Catalog entry:** + +```typescript +Chart: { + type: () => import('./chart').then((r) => r.Chart), + bindings: ({ properties }) => [ + inputBinding('type', () => properties['type'] || undefined), + inputBinding('title', () => properties['title'] || undefined), + inputBinding('chartData', () => properties['chartData'] || undefined), + ], +}, +``` + +### Any Component + +The same pattern works for **any Angular component**. If you can build it as an Angular component, you can make it an A2UI custom component: + +- **Carousel**: Wrap your carousel library, bind slides via data model paths +- **Code editor**: Monaco editor with syntax highlighting +- **3D viewer**: Three.js scene driven by agent data +- **Payment form**: Stripe Elements with A2UI event callbacks +- **PDF viewer**: Display documents the agent references + +The pattern is always: + +1. Extend `DynamicComponent` +2. Declare `input()` properties +3. Use `resolvePrimitive()` for data binding +4. Register in catalog with `inputBinding()` mappings + +## Data Binding with Custom Components + +Custom components can use A2UI's data binding system. Instead of literal values, properties can reference paths in the data model: + +```json +{ + "component": "Chart", + "properties": { + "type": "pie", + "title": "Sales by Region", + "chartData": { "path": "/salesData" } + } +} +``` + +The agent updates data separately via `updateDataModel`: + +```json +{ + "type": "updateDataModel", + "surfaceId": "main", + "data": { + "salesData": [ + { "label": "North America", "value": 45 }, + { "label": "Europe", "value": 30 }, + { "label": "Asia", "value": 25 } + ] + } +} +``` + +This separation means the agent can update chart data without re-sending the entire component tree. + +## Agent Configuration + +For agents to use your custom components, include the component definitions in the agent's prompt or catalog configuration: + +```python +# Agent-side catalog config +catalog = CatalogConfig( + catalog_id="my-custom-catalog", + components={ + # Standard components are inherited + "YouTube": { + "description": "Embedded YouTube video player", + "properties": { + "videoId": "YouTube video ID (e.g., 'dQw4w9WgXcQ')", + "title": "Optional title displayed above the video", + }, + }, + "GoogleMap": { + "description": "Interactive Google Map with pins", + "properties": { + "zoom": "Map zoom level (1-20)", + "center": "Center coordinates (data model path)", + "pins": "Array of pin locations (data model path)", + }, + }, + "Chart": { + "description": "Chart.js chart (pie, bar, line, doughnut)", + "properties": { + "type": "Chart type: pie, bar, line, doughnut", + "title": "Chart title", + "chartData": "Chart data (data model path)", + }, + }, + }, +) +``` + +## Working Examples + +- [**rizzcharts**](https://github.com/google/a2ui/tree/main/samples/client/angular/projects/rizzcharts) — Chart.js + Google Maps custom components +- [**custom-components**](https://github.com/google/a2ui/tree/main/samples/client/angular/projects/custom-components) — YouTube + Maps + Charts starter sample ## Next Steps -- **[Theming & Styling](theming.md)**: Customize the look and feel of components -- **[Component Reference](../reference/components.md)**: See all standard components -- **[Agent Development](agent-development.md)**: Build agents that use custom components +- [Design System Integration](design-system-integration.md) — Add A2UI to an existing Material app +- [Theming Guide](theming.md) — Style custom components with your design system +- [Agent Development](agent-development.md) — Build agents that use custom components diff --git a/docs/guides/design-system-integration.md b/docs/guides/design-system-integration.md new file mode 100644 index 0000000000..1b4412631c --- /dev/null +++ b/docs/guides/design-system-integration.md @@ -0,0 +1,147 @@ +# 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). By the end, your app will render agent-generated UI alongside your existing components. + +> **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. **Register** a component catalog (standard or custom) +3. **Wire** the A2UI renderer into your app +4. **Connect** to an A2A-compatible agent + +The key insight: A2UI doesn't replace your design system — it extends it. Your existing components stay exactly as they are. A2UI adds a rendering layer that translates agent-generated JSON into Angular components from a registered catalog. + +## Step 1: Install A2UI Packages + +```bash +npm install @a2ui/angular @a2ui/web_core +``` + +The `@a2ui/angular` package provides: + +- `DynamicComponent` — base class for A2UI-compatible components +- `DEFAULT_CATALOG` — the standard catalog (Text, Button, TextField, etc.) +- `Catalog` injection token — for providing your catalog to the renderer +- `configureChatCanvasFeatures()` — helper for wiring everything together + +## Step 2: Register a Catalog + +A **catalog** maps component names (strings the agent uses) to Angular component classes. Start with the default catalog: + +```typescript +// app.config.ts +import { + configureChatCanvasFeatures, + usingA2aService, + usingA2uiRenderers, +} from '@a2a_chat_canvas/config'; +import { DEFAULT_CATALOG } from '@a2ui/angular'; +import { theme } from './theme'; + +export const appConfig: ApplicationConfig = { + providers: [ + // ... your existing providers (Material, Router, etc.) + configureChatCanvasFeatures( + usingA2aService(MyA2aService), + usingA2uiRenderers(DEFAULT_CATALOG, theme), + ), + ], +}; +``` + +The `DEFAULT_CATALOG` includes all standard A2UI components: Text, Button, TextField, Image, Card, Row, Column, Tabs, Modal, Slider, CheckBox, MultipleChoice, DateTimeInput, Divider, Icon, Video, and AudioPlayer. + +## Step 3: Add the Chat Canvas + +The chat canvas is the container where A2UI surfaces are rendered. Add it to your layout: + +```html + +
+ + + ... + + + + + + + +
+``` + +The chat canvas handles: + +- Displaying agent messages and A2UI surfaces +- User input and message sending +- Surface lifecycle (create, update, delete) + +## Step 4: Connect to an Agent + +Create a service that implements the A2A connection: + +```typescript +// services/a2a.service.ts +import { Injectable } from '@angular/core'; + +@Injectable({ providedIn: 'root' }) +export class MyA2aService { + private readonly agentUrl = 'http://localhost:8000'; + + async sendMessage(message: string, sessionId: string) { + // Send message to your A2A agent + // The agent responds with A2UI messages that the renderer handles + } +} +``` + +See the [A2A JavaScript SDK](https://github.com/a2aproject/a2a-js) for the full client implementation. + +## 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 A2UI catalog | +| Component library | Angular Material | Angular Material + A2UI standard catalog | +| Routing | Your routes | Your routes + chat canvas overlay | +| Theming | Material theme | Material theme + A2UI theme tokens | + +Your existing app is untouched. A2UI adds a parallel rendering path for agent-generated content. + +## Theming + +A2UI components respect your Material theme through CSS custom properties. Create a theme that maps your Material tokens to A2UI: + +```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. + +## Working Example + +The [design-system-upgrade sample](https://github.com/google/a2ui/tree/main/samples/client/angular/projects/design-system-upgrade) demonstrates this integration end-to-end: + +- Angular Material app with navigation, cards, and a carousel +- A2UI added alongside existing components +- Custom theme mapping Material tokens to A2UI +- Connected to a sample A2A agent + +## Next Steps + +- [Custom Components](custom-components.md) — Add your own components to the catalog (Maps, Charts, YouTube, etc.) +- [Theming Guide](theming.md) — Deep dive into theming A2UI with your design system +- [Agent Development](agent-development.md) — Build agents that generate A2UI +- [Renderer Development](renderer-development.md) — Understand the rendering architecture diff --git a/docs/guides/friction-log-custom-components.md b/docs/guides/friction-log-custom-components.md new file mode 100644 index 0000000000..6e6aa5848d --- /dev/null +++ b/docs/guides/friction-log-custom-components.md @@ -0,0 +1,121 @@ +# Friction Log: Adding Custom Components to A2UI + +> **Author**: @zeroasterisk | **Date**: 2026-03-12 | **Goal**: Document friction points encountered while building custom A2UI components (YouTube, Maps, Charts) and integrating A2UI into an existing Material Angular app. + +## Summary + +Overall: the custom component pattern **works well** once understood. The main friction is in **discovery and documentation** — knowing what to extend, how bindings work, and how the agent learns about custom components. + +## Friction Points + +### 🟡 F1: No clear "Getting Started" for custom components + +**What happened**: The `custom-components.md` guide was a skeleton with TODOs. A developer wanting to add a custom component had to reverse-engineer the rizzcharts sample. + +**Expected**: A step-by-step guide with a simple example (like adding a YouTube embed). + +**Severity**: P2 — Blocks community adoption of custom components + +**Recommendation**: The updated `custom-components.md` guide (this PR) addresses this. Keep it maintained as the pattern evolves. + +--- + +### 🟡 F2: Catalog registration pattern is non-obvious + +**What happened**: The `inputBinding()` pattern for mapping A2UI JSON properties to Angular `@Input()` values requires specific knowledge of `@angular/core` internals. The `bindings` function receives `{ properties }` but the type is `Types.AnyComponentNode`, which doesn't self-document which properties are available. + +**Expected**: A typed helper or code-gen tool that creates catalog entries from component metadata. + +**Severity**: P2 + +**Recommendation**: Consider a decorator-based approach: +```typescript +@A2UIComponent({ name: 'YouTube' }) +export class YouTube extends DynamicComponent { + @A2UIInput() videoId: string; + @A2UIInput() title?: string; +} +``` +This would auto-generate catalog entries and reduce boilerplate. + +--- + +### 🟡 F3: Agent-side catalog configuration is manual + +**What happened**: For agents to use custom components, you must manually describe each component and its properties in the agent's prompt or catalog config. There's no way to auto-generate this from the client-side catalog definition. + +**Expected**: A shared schema that both client and agent can consume — define once, use on both sides. + +**Severity**: P2 + +**Recommendation**: Consider a `catalog.json` schema file that describes components, properties, and types. The client uses it for registration, the agent uses it for prompt construction. This aligns with the v0.9 catalog concept but needs tooling. + +--- + +### 🟢 F4: `resolvePrimitive()` works well but isn't well-documented + +**What happened**: The `resolvePrimitive()` method on `DynamicComponent` correctly handles both literal values and data-bound paths (`{ path: "/foo" }`). However, its behavior and return types aren't documented — I had to read the source to understand what it does. + +**Expected**: JSDoc on `resolvePrimitive()` explaining: input types, return types, null handling, and when to use it vs. direct input access. + +**Severity**: P3 + +--- + +### 🟢 F5: No validation that agent-generated JSON matches catalog + +**What happened**: If the agent generates `{ "component": "YouTub" }` (typo), the renderer silently fails to render anything. No error in console, no fallback. + +**Expected**: A warning or error when a component name isn't found in the registered catalog, and ideally a fallback component showing "Unknown component: YouTub". + +**Severity**: P2 — Debugging agent output is painful without this + +--- + +### 🟢 F6: DomSanitizer injection in DynamicComponent subclass + +**What happened**: The YouTube component needs `DomSanitizer` for iframe URLs. Injecting it via constructor works but feels wrong in the `DynamicComponent` pattern where the base class handles DI differently. + +**Expected**: A pattern or utility for safe URL handling in custom components. + +**Severity**: P3 + +--- + +### 🟡 F7: No guide for "upgrade existing app to A2UI" + +**What happened**: There's no documentation for the most common use case — adding A2UI to an app that already exists. The existing guides assume you're building from scratch. + +**Expected**: A guide that starts with "you have a Material Angular app" and walks through adding A2UI. + +**Severity**: P2 — This is the primary onboarding path for most teams + +**Recommendation**: The new `design-system-integration.md` guide (this PR) addresses this. + +--- + +### 🟢 F8: Theming custom components + +**What happened**: Custom components need to use CSS custom properties from the A2UI theme (e.g., `var(--mat-sys-surface-container)`). The theming guide doesn't cover how custom components should consume theme tokens. + +**Expected**: Documentation on which CSS custom properties are available and how to use them in custom components. + +**Severity**: P3 + +--- + +## What Worked Well + +- **`DynamicComponent` base class** is well-designed — clean inheritance, reactive signals, data binding just works +- **Lazy-loaded catalog entries** are smart — no bundle size impact for unused components +- **Data binding via paths** is powerful — agents can update data independently of the component tree +- **`DEFAULT_CATALOG` spread** makes it trivial to add custom components alongside standard ones +- **Angular Material integration** was seamless — A2UI components live alongside Material components without conflict + +## Recommendations + +1. **P2**: Add unknown component warnings/fallback in renderer (#F5) +2. **P2**: Explore decorator-based catalog registration (#F2) +3. **P2**: Define a shared catalog schema for client + agent (#F3) +4. **P3**: Document `resolvePrimitive()` and theme tokens (#F4, #F8) +5. **P3**: Add DI patterns guide for custom components (#F6) diff --git a/mkdocs.yaml b/mkdocs.yaml index 3b076e6d75..6e886fd97b 100644 --- a/mkdocs.yaml +++ b/mkdocs.yaml @@ -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: diff --git a/samples/client/angular/projects/rizzcharts/src/a2ui-catalog/catalog.ts b/samples/client/angular/projects/rizzcharts/src/a2ui-catalog/catalog.ts index e7cc9f0454..a4acf32340 100644 --- a/samples/client/angular/projects/rizzcharts/src/a2ui-catalog/catalog.ts +++ b/samples/client/angular/projects/rizzcharts/src/a2ui-catalog/catalog.ts @@ -40,4 +40,12 @@ export const RIZZ_CHARTS_CATALOG = { inputBinding('title', () => ('title' in properties && properties['title']) || undefined), ], }, + YouTube: { + type: () => import('./youtube').then((r) => r.YouTube), + bindings: ({ properties }) => [ + inputBinding('videoId', () => ('videoId' in properties && properties['videoId']) || undefined), + inputBinding('title', () => ('title' in properties && properties['title']) || undefined), + inputBinding('autoplay', () => ('autoplay' in properties && properties['autoplay']) || undefined), + ], + }, } as Catalog; diff --git a/samples/client/angular/projects/rizzcharts/src/a2ui-catalog/youtube.ts b/samples/client/angular/projects/rizzcharts/src/a2ui-catalog/youtube.ts new file mode 100644 index 0000000000..7f698486ea --- /dev/null +++ b/samples/client/angular/projects/rizzcharts/src/a2ui-catalog/youtube.ts @@ -0,0 +1,122 @@ +/* + Copyright 2025 Google LLC + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +import { DynamicComponent } from '@a2ui/angular'; +import * as Primitives from '@a2ui/web_core/types/primitives'; +import * as Types from '@a2ui/web_core/types/types'; +import { + ChangeDetectionStrategy, + Component, + computed, + input, +} from '@angular/core'; +import { DomSanitizer, SafeResourceUrl } from '@angular/platform-browser'; + +@Component({ + selector: 'a2ui-youtube', + changeDetection: ChangeDetectionStrategy.Eager, + styles: ` + :host { + display: block; + flex: var(--weight); + padding: 8px; + } + + .youtube-container { + background-color: var(--mat-sys-surface-container); + border-radius: 8px; + border: 1px solid var(--mat-sys-surface-container-high); + padding: 16px; + max-width: 800px; + } + + .youtube-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 12px; + } + + .youtube-header h3 { + margin: 0; + font-size: 18px; + color: var(--mat-sys-on-surface); + } + + .video-wrapper { + position: relative; + width: 100%; + padding-bottom: 56.25%; /* 16:9 aspect ratio */ + border-radius: 8px; + overflow: hidden; + } + + iframe { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + border: none; + } + `, + template: ` + @if (resolvedVideoId()) { +
+ @if (resolvedTitle()) { +
+

{{ resolvedTitle() }}

+
+ } +
+ +
+
+ } + `, +}) +export class YouTube extends DynamicComponent { + readonly videoId = input.required(); + protected readonly resolvedVideoId = computed(() => + this.resolvePrimitive(this.videoId()), + ); + + readonly title = input(); + protected readonly resolvedTitle = computed(() => + this.resolvePrimitive(this.title() ?? null), + ); + + readonly autoplay = input(); + protected readonly resolvedAutoplay = computed(() => + this.resolvePrimitive(this.autoplay() ?? null), + ); + + protected readonly safeUrl = computed((): SafeResourceUrl | null => { + const id = this.resolvedVideoId(); + if (!id) return null; + const autoplay = this.resolvedAutoplay() ? '1' : '0'; + const url = `https://www.youtube.com/embed/${encodeURIComponent(id)}?autoplay=${autoplay}&rel=0`; + return this.sanitizer.bypassSecurityTrustResourceUrl(url); + }); + + constructor(private sanitizer: DomSanitizer) { + super(); + } +} From 53f12846aea4260b128b31c4efa371615ccf6218 Mon Sep 17 00:00:00 2001 From: Zaf Date: Thu, 12 Mar 2026 13:19:18 +0000 Subject: [PATCH 2/6] =?UTF-8?q?docs:=20cross-link=20custom=20components=20?= =?UTF-8?q?=E2=86=94=20design=20system=20guides?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/guides/custom-components.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/guides/custom-components.md b/docs/guides/custom-components.md index afdd820eac..ea40074c27 100644 --- a/docs/guides/custom-components.md +++ b/docs/guides/custom-components.md @@ -13,6 +13,9 @@ The A2UI Standard Catalog covers common UI elements (text, buttons, inputs, layo Custom components let agents generate UI that includes **any** component your app supports — not just what's in the standard catalog. +!!! tip "Already have a component library?" + If you're adding A2UI to an existing app with its own design system (Material, Ant Design, PrimeNG, etc.), start with the [Design System Integration](design-system-integration.md) guide first — it walks through wiring A2UI into your app before adding custom components. + ## How It Works ``` From 9a83a2b509aaf938d84bc2ad13c97f5052f20a3c Mon Sep 17 00:00:00 2001 From: Zaf Date: Thu, 12 Mar 2026 13:20:50 +0000 Subject: [PATCH 3/6] docs: clarify DEFAULT_CATALOG spread is optional --- docs/guides/custom-components.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/guides/custom-components.md b/docs/guides/custom-components.md index ea40074c27..df1fe1e32c 100644 --- a/docs/guides/custom-components.md +++ b/docs/guides/custom-components.md @@ -137,7 +137,7 @@ import { Catalog, DEFAULT_CATALOG } from '@a2ui/angular'; import { inputBinding } from '@angular/core'; export const MY_CATALOG = { - ...DEFAULT_CATALOG, // Include all standard components + ...DEFAULT_CATALOG, // Optionally include A2UI basic catalog components YouTube: { type: () => import('./youtube').then((r) => r.YouTube), From 3001ea6432a12e3609ca5b9c1094128e062d6d08 Mon Sep 17 00:00:00 2001 From: alan blount Date: Thu, 12 Mar 2026 09:36:29 -0400 Subject: [PATCH 4/6] Apply suggestions from code review Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- docs/guides/custom-components.md | 2 +- docs/guides/design-system-integration.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/guides/custom-components.md b/docs/guides/custom-components.md index df1fe1e32c..89e8123887 100644 --- a/docs/guides/custom-components.md +++ b/docs/guides/custom-components.md @@ -110,7 +110,7 @@ export class YouTube extends DynamicComponent { protected readonly safeUrl = computed(() => { const id = this.resolvedVideoId(); if (!id) return null; - const url = `https://www.youtube.com/embed/${id}`; + const url = `https://www.youtube.com/embed/${encodeURIComponent(id)}`; return this.sanitizer.bypassSecurityTrustResourceUrl(url); }); diff --git a/docs/guides/design-system-integration.md b/docs/guides/design-system-integration.md index 1b4412631c..b7aaaf5422 100644 --- a/docs/guides/design-system-integration.md +++ b/docs/guides/design-system-integration.md @@ -53,7 +53,7 @@ export const appConfig: ApplicationConfig = { }; ``` -The `DEFAULT_CATALOG` includes all standard A2UI components: Text, Button, TextField, Image, Card, Row, Column, Tabs, Modal, Slider, CheckBox, MultipleChoice, DateTimeInput, Divider, Icon, Video, and AudioPlayer. +The `DEFAULT_CATALOG` includes all standard A2UI components: Text, Button, TextField, Image, Card, Row, Column, List, Tabs, Modal, Slider, CheckBox, MultipleChoice, DateTimeInput, Divider, Icon, Video, and AudioPlayer. ## Step 3: Add the Chat Canvas From 6702ebaee484499d23efd92af267163fd6273108 Mon Sep 17 00:00:00 2001 From: Zaf Date: Thu, 12 Mar 2026 13:46:15 +0000 Subject: [PATCH 5/6] docs: address PR #824 review comments - Remove friction log file (content already in issue #825) - YouTube component: add video ID regex validation (security) - custom-components.md: rename to 'Custom Component Catalogs', reorder examples (media first), clarify basic catalog is optional, remove redundant heading, fix Maps input.required consistency, add encodeURIComponent to docs example - design-system-integration.md: rewrite to focus on wrapping Material components as A2UI components (not using DEFAULT_CATALOG), show custom catalog without basic components, add mixed catalog example - s/standard/basic/ throughout --- docs/guides/custom-components.md | 72 ++++----- docs/guides/design-system-integration.md | 153 +++++++++++------- docs/guides/friction-log-custom-components.md | 121 -------------- .../rizzcharts/src/a2ui-catalog/youtube.ts | 9 ++ 4 files changed, 137 insertions(+), 218 deletions(-) delete mode 100644 docs/guides/friction-log-custom-components.md diff --git a/docs/guides/custom-components.md b/docs/guides/custom-components.md index 89e8123887..0059a90108 100644 --- a/docs/guides/custom-components.md +++ b/docs/guides/custom-components.md @@ -1,20 +1,20 @@ -# Custom Components +# Custom Component Catalogs -Extend A2UI with your own components — maps, charts, video players, or anything your application needs. +Extend A2UI with a catalog of your own components — video players, maps, charts, or anything your application needs. -## Why Custom Components? +## Why Custom Catalogs? -The A2UI Standard Catalog covers common UI elements (text, buttons, inputs, layout), but real applications need specialized components: +The A2UI Basic Catalog covers common UI elements (text, buttons, inputs, layout), but your application might need specialized components: +- **Media**: YouTube embeds, audio visualizers, 3D viewers - **Maps**: Google Maps, Mapbox, Leaflet - **Charts**: Chart.js, D3, Recharts -- **Media**: YouTube embeds, audio visualizers, 3D viewers - **Domain-specific**: Stock tickers, medical imaging, CAD viewers -Custom components let agents generate UI that includes **any** component your app supports — not just what's in the standard catalog. +Custom catalogs let agents generate UI that includes **any** component your app supports — not just what's in the basic catalog. !!! tip "Already have a component library?" - If you're adding A2UI to an existing app with its own design system (Material, Ant Design, PrimeNG, etc.), start with the [Design System Integration](design-system-integration.md) guide first — it walks through wiring A2UI into your app before adding custom components. + If you're adding A2UI to an existing app with its own design system (Material, Ant Design, PrimeNG, etc.), start with the [Design System Integration](design-system-integration.md) guide first — it walks through wrapping your existing components as A2UI components. ## How It Works @@ -27,11 +27,11 @@ Angular ──renders──> <───┘ ``` 1. You **implement** an Angular component that extends `DynamicComponent` -2. You **register** it in a catalog alongside standard components +2. You **register** it in a catalog 3. The agent **references** it by name in `updateComponents` messages 4. The A2UI renderer **instantiates** your component with the agent's properties -## Step-by-Step: Adding a YouTube Component +## Adding a Custom Component: YouTube Example Let's add a YouTube video player as a custom A2UI component. @@ -56,11 +56,7 @@ import { DomSanitizer } from '@angular/platform-browser'; selector: 'a2ui-youtube', changeDetection: ChangeDetectionStrategy.Eager, styles: ` - :host { - display: block; - flex: var(--weight); - padding: 8px; - } + :host { display: block; flex: var(--weight); padding: 8px; } .video-container { position: relative; width: 100%; @@ -70,16 +66,10 @@ import { DomSanitizer } from '@angular/platform-browser'; } iframe { position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; + top: 0; left: 0; + width: 100%; height: 100%; border: none; } - h3 { - margin: 8px 0 4px; - color: var(--mat-sys-on-surface); - } `, template: ` @if (resolvedVideoId()) { @@ -97,6 +87,8 @@ import { DomSanitizer } from '@angular/platform-browser'; `, }) export class YouTube extends DynamicComponent { + private static readonly YOUTUBE_ID_REGEX = /^[a-zA-Z0-9_-]{11}$/; + readonly videoId = input.required(); protected readonly resolvedVideoId = computed(() => this.resolvePrimitive(this.videoId()), @@ -110,6 +102,13 @@ export class YouTube extends DynamicComponent { protected readonly safeUrl = computed(() => { const id = this.resolvedVideoId(); if (!id) return null; + + // Validate video ID format before constructing URL + if (!YouTube.YOUTUBE_ID_REGEX.test(id)) { + console.error('Invalid YouTube video ID received from agent:', id); + return null; + } + const url = `https://www.youtube.com/embed/${encodeURIComponent(id)}`; return this.sanitizer.bypassSecurityTrustResourceUrl(url); }); @@ -126,10 +125,11 @@ export class YouTube extends DynamicComponent { - Use `input()` for properties the agent will set - Use `resolvePrimitive()` to resolve values that may be literals or data-bound paths - Use `computed()` for reactive derivations +- Validate agent-provided data before use (e.g., video ID format check) -### 2. Register in the Catalog +### 2. Register in a Catalog -Add your component to the catalog alongside standard components: +Add your component to a catalog: ```typescript // a2ui-catalog/catalog.ts @@ -153,15 +153,17 @@ export const MY_CATALOG = { } as Catalog; ``` +A2UI ships with a basic catalog to get you started quickly, but you do **not** need to use it. If your design system already provides the components you want to render, you can expose only your own components, or a mix of basic and custom components. + **What's happening:** -- `...DEFAULT_CATALOG` — spread the standard catalog so agents can use standard components too +- `...DEFAULT_CATALOG` — optionally spread the basic catalog (Text, Button, etc.) - `type` — lazy-loaded import of your component class - `bindings` — maps properties from the A2UI JSON to Angular `@Input()` values ### 3. Use the Custom Catalog -Update your app config to use your custom catalog instead of the default: +Update your app config to use your catalog: ```typescript // app.config.ts @@ -204,9 +206,7 @@ The agent references your component by name in `updateComponents`: The agent knows about your custom components through the catalog configuration in its prompt or system instructions. -## More Examples - -### Google Maps Component +## Google Maps Example A map component that displays pins from the agent's data model: @@ -224,8 +224,8 @@ A map component that displays pins from the agent's data model: `, }) export class GoogleMap extends DynamicComponent { - readonly zoom = input.required(); - readonly center = input.required<{ path: string } | null>(); + readonly zoom = input(); + readonly center = input<{ path: string } | null>(); readonly pins = input<{ path: string }>(); protected resolvedZoom = computed(() => this.resolvePrimitive(this.zoom())); @@ -265,7 +265,7 @@ GoogleMap: { Maps uses **data binding** — the `center` and `pins` reference paths in the data model, so the agent can update locations dynamically via `updateDataModel`. -### Chart Component +## Chart Example A chart component using Chart.js: @@ -310,7 +310,7 @@ Chart: { }, ``` -### Any Component +## Any Component The same pattern works for **any Angular component**. If you can build it as an Angular component, you can make it an A2UI custom component: @@ -369,7 +369,6 @@ For agents to use your custom components, include the component definitions in t catalog = CatalogConfig( catalog_id="my-custom-catalog", components={ - # Standard components are inherited "YouTube": { "description": "Embedded YouTube video player", "properties": { @@ -399,11 +398,10 @@ catalog = CatalogConfig( ## Working Examples -- [**rizzcharts**](https://github.com/google/a2ui/tree/main/samples/client/angular/projects/rizzcharts) — Chart.js + Google Maps custom components -- [**custom-components**](https://github.com/google/a2ui/tree/main/samples/client/angular/projects/custom-components) — YouTube + Maps + Charts starter sample +- [**rizzcharts**](https://github.com/google/a2ui/tree/main/samples/client/angular/projects/rizzcharts) — Chart.js + Google Maps + YouTube custom components ## Next Steps -- [Design System Integration](design-system-integration.md) — Add A2UI to an existing Material app +- [Design System Integration](design-system-integration.md) — Wrap your existing design system components as A2UI components - [Theming Guide](theming.md) — Style custom components with your design system - [Agent Development](agent-development.md) — Build agents that use custom components diff --git a/docs/guides/design-system-integration.md b/docs/guides/design-system-integration.md index b7aaaf5422..c73f608981 100644 --- a/docs/guides/design-system-integration.md +++ b/docs/guides/design-system-integration.md @@ -1,6 +1,6 @@ # 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). By the end, your app will render agent-generated UI alongside your existing components. +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. @@ -9,11 +9,11 @@ This guide walks through adding A2UI to an **existing** Angular application that Adding A2UI to an existing app involves four steps: 1. **Install** the A2UI Angular renderer and web_core packages -2. **Register** a component catalog (standard or custom) -3. **Wire** the A2UI renderer into your app +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 extends it. Your existing components stay exactly as they are. A2UI adds a rendering layer that translates agent-generated JSON into Angular components from a registered catalog. +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 @@ -23,14 +23,87 @@ npm install @a2ui/angular @a2ui/web_core The `@a2ui/angular` package provides: -- `DynamicComponent` — base class for A2UI-compatible components -- `DEFAULT_CATALOG` — the standard catalog (Text, Button, TextField, etc.) +- `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: Register a Catalog +## Step 2: Wrap Your Components -A **catalog** maps component names (strings the agent uses) to Angular component classes. Start with the default catalog: +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: ` + + `, +}) +export class MaterialButton extends DynamicComponent { + readonly label = input.required(); + readonly color = input(); + + 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 @@ -39,7 +112,7 @@ import { usingA2aService, usingA2uiRenderers, } from '@a2a_chat_canvas/config'; -import { DEFAULT_CATALOG } from '@a2ui/angular'; +import { MATERIAL_CATALOG } from './a2ui-catalog/catalog'; import { theme } from './theme'; export const appConfig: ApplicationConfig = { @@ -47,17 +120,15 @@ export const appConfig: ApplicationConfig = { // ... your existing providers (Material, Router, etc.) configureChatCanvasFeatures( usingA2aService(MyA2aService), - usingA2uiRenderers(DEFAULT_CATALOG, theme), + usingA2uiRenderers(MATERIAL_CATALOG, theme), ), ], }; ``` -The `DEFAULT_CATALOG` includes all standard A2UI components: Text, Button, TextField, Image, Card, Row, Column, List, Tabs, Modal, Slider, CheckBox, MultipleChoice, DateTimeInput, Divider, Icon, Video, and AudioPlayer. - -## Step 3: Add the Chat Canvas +## Step 5: Add the Chat Canvas -The chat canvas is the container where A2UI surfaces are rendered. Add it to your layout: +The chat canvas is the container where A2UI surfaces are rendered. Add it alongside your existing layout: ```html @@ -75,48 +146,20 @@ The chat canvas is the container where A2UI surfaces are rendered. Add it to you ``` -The chat canvas handles: - -- Displaying agent messages and A2UI surfaces -- User input and message sending -- Surface lifecycle (create, update, delete) - -## Step 4: Connect to an Agent - -Create a service that implements the A2A connection: - -```typescript -// services/a2a.service.ts -import { Injectable } from '@angular/core'; - -@Injectable({ providedIn: 'root' }) -export class MyA2aService { - private readonly agentUrl = 'http://localhost:8000'; - - async sendMessage(message: string, sessionId: string) { - // Send message to your A2A agent - // The agent responds with A2UI messages that the renderer handles - } -} -``` - -See the [A2A JavaScript SDK](https://github.com/a2aproject/a2a-js) for the full client implementation. - ## 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 A2UI catalog | -| Component library | Angular Material | Angular Material + A2UI standard catalog | -| Routing | Your routes | Your routes + chat canvas overlay | -| Theming | Material theme | Material theme + A2UI theme tokens | +| 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 parallel rendering path for agent-generated content. +Your existing app is untouched. A2UI adds a rendering layer where agents compose **your** components. ## Theming -A2UI components respect your Material theme through CSS custom properties. Create a theme that maps your Material tokens to A2UI: +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 @@ -130,18 +173,8 @@ export const theme: Theme = { See the [Theming Guide](theming.md) for complete theming documentation. -## Working Example - -The [design-system-upgrade sample](https://github.com/google/a2ui/tree/main/samples/client/angular/projects/design-system-upgrade) demonstrates this integration end-to-end: - -- Angular Material app with navigation, cards, and a carousel -- A2UI added alongside existing components -- Custom theme mapping Material tokens to A2UI -- Connected to a sample A2A agent - ## Next Steps -- [Custom Components](custom-components.md) — Add your own components to the catalog (Maps, Charts, YouTube, etc.) -- [Theming Guide](theming.md) — Deep dive into theming A2UI with your design system -- [Agent Development](agent-development.md) — Build agents that generate A2UI -- [Renderer Development](renderer-development.md) — Understand the rendering architecture +- [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 diff --git a/docs/guides/friction-log-custom-components.md b/docs/guides/friction-log-custom-components.md deleted file mode 100644 index 6e6aa5848d..0000000000 --- a/docs/guides/friction-log-custom-components.md +++ /dev/null @@ -1,121 +0,0 @@ -# Friction Log: Adding Custom Components to A2UI - -> **Author**: @zeroasterisk | **Date**: 2026-03-12 | **Goal**: Document friction points encountered while building custom A2UI components (YouTube, Maps, Charts) and integrating A2UI into an existing Material Angular app. - -## Summary - -Overall: the custom component pattern **works well** once understood. The main friction is in **discovery and documentation** — knowing what to extend, how bindings work, and how the agent learns about custom components. - -## Friction Points - -### 🟡 F1: No clear "Getting Started" for custom components - -**What happened**: The `custom-components.md` guide was a skeleton with TODOs. A developer wanting to add a custom component had to reverse-engineer the rizzcharts sample. - -**Expected**: A step-by-step guide with a simple example (like adding a YouTube embed). - -**Severity**: P2 — Blocks community adoption of custom components - -**Recommendation**: The updated `custom-components.md` guide (this PR) addresses this. Keep it maintained as the pattern evolves. - ---- - -### 🟡 F2: Catalog registration pattern is non-obvious - -**What happened**: The `inputBinding()` pattern for mapping A2UI JSON properties to Angular `@Input()` values requires specific knowledge of `@angular/core` internals. The `bindings` function receives `{ properties }` but the type is `Types.AnyComponentNode`, which doesn't self-document which properties are available. - -**Expected**: A typed helper or code-gen tool that creates catalog entries from component metadata. - -**Severity**: P2 - -**Recommendation**: Consider a decorator-based approach: -```typescript -@A2UIComponent({ name: 'YouTube' }) -export class YouTube extends DynamicComponent { - @A2UIInput() videoId: string; - @A2UIInput() title?: string; -} -``` -This would auto-generate catalog entries and reduce boilerplate. - ---- - -### 🟡 F3: Agent-side catalog configuration is manual - -**What happened**: For agents to use custom components, you must manually describe each component and its properties in the agent's prompt or catalog config. There's no way to auto-generate this from the client-side catalog definition. - -**Expected**: A shared schema that both client and agent can consume — define once, use on both sides. - -**Severity**: P2 - -**Recommendation**: Consider a `catalog.json` schema file that describes components, properties, and types. The client uses it for registration, the agent uses it for prompt construction. This aligns with the v0.9 catalog concept but needs tooling. - ---- - -### 🟢 F4: `resolvePrimitive()` works well but isn't well-documented - -**What happened**: The `resolvePrimitive()` method on `DynamicComponent` correctly handles both literal values and data-bound paths (`{ path: "/foo" }`). However, its behavior and return types aren't documented — I had to read the source to understand what it does. - -**Expected**: JSDoc on `resolvePrimitive()` explaining: input types, return types, null handling, and when to use it vs. direct input access. - -**Severity**: P3 - ---- - -### 🟢 F5: No validation that agent-generated JSON matches catalog - -**What happened**: If the agent generates `{ "component": "YouTub" }` (typo), the renderer silently fails to render anything. No error in console, no fallback. - -**Expected**: A warning or error when a component name isn't found in the registered catalog, and ideally a fallback component showing "Unknown component: YouTub". - -**Severity**: P2 — Debugging agent output is painful without this - ---- - -### 🟢 F6: DomSanitizer injection in DynamicComponent subclass - -**What happened**: The YouTube component needs `DomSanitizer` for iframe URLs. Injecting it via constructor works but feels wrong in the `DynamicComponent` pattern where the base class handles DI differently. - -**Expected**: A pattern or utility for safe URL handling in custom components. - -**Severity**: P3 - ---- - -### 🟡 F7: No guide for "upgrade existing app to A2UI" - -**What happened**: There's no documentation for the most common use case — adding A2UI to an app that already exists. The existing guides assume you're building from scratch. - -**Expected**: A guide that starts with "you have a Material Angular app" and walks through adding A2UI. - -**Severity**: P2 — This is the primary onboarding path for most teams - -**Recommendation**: The new `design-system-integration.md` guide (this PR) addresses this. - ---- - -### 🟢 F8: Theming custom components - -**What happened**: Custom components need to use CSS custom properties from the A2UI theme (e.g., `var(--mat-sys-surface-container)`). The theming guide doesn't cover how custom components should consume theme tokens. - -**Expected**: Documentation on which CSS custom properties are available and how to use them in custom components. - -**Severity**: P3 - ---- - -## What Worked Well - -- **`DynamicComponent` base class** is well-designed — clean inheritance, reactive signals, data binding just works -- **Lazy-loaded catalog entries** are smart — no bundle size impact for unused components -- **Data binding via paths** is powerful — agents can update data independently of the component tree -- **`DEFAULT_CATALOG` spread** makes it trivial to add custom components alongside standard ones -- **Angular Material integration** was seamless — A2UI components live alongside Material components without conflict - -## Recommendations - -1. **P2**: Add unknown component warnings/fallback in renderer (#F5) -2. **P2**: Explore decorator-based catalog registration (#F2) -3. **P2**: Define a shared catalog schema for client + agent (#F3) -4. **P3**: Document `resolvePrimitive()` and theme tokens (#F4, #F8) -5. **P3**: Add DI patterns guide for custom components (#F6) diff --git a/samples/client/angular/projects/rizzcharts/src/a2ui-catalog/youtube.ts b/samples/client/angular/projects/rizzcharts/src/a2ui-catalog/youtube.ts index 7f698486ea..914ea18586 100644 --- a/samples/client/angular/projects/rizzcharts/src/a2ui-catalog/youtube.ts +++ b/samples/client/angular/projects/rizzcharts/src/a2ui-catalog/youtube.ts @@ -108,9 +108,18 @@ export class YouTube extends DynamicComponent { this.resolvePrimitive(this.autoplay() ?? null), ); + private static readonly YOUTUBE_ID_REGEX = /^[a-zA-Z0-9_-]{11}$/; + protected readonly safeUrl = computed((): SafeResourceUrl | null => { const id = this.resolvedVideoId(); if (!id) return null; + + // Validate video ID format before constructing URL + if (!YouTube.YOUTUBE_ID_REGEX.test(id)) { + console.error('Invalid YouTube video ID received from agent:', id); + return null; + } + const autoplay = this.resolvedAutoplay() ? '1' : '0'; const url = `https://www.youtube.com/embed/${encodeURIComponent(id)}?autoplay=${autoplay}&rel=0`; return this.sanitizer.bypassSecurityTrustResourceUrl(url); From 9497734e11833db502d8ee009020a4829bf21a4e Mon Sep 17 00:00:00 2001 From: Zaf Date: Thu, 12 Mar 2026 14:43:26 +0000 Subject: [PATCH 6/6] docs: add render_macros:false to prevent Jinja2 eval of Angular template syntax --- docs/guides/custom-components.md | 4 ++++ docs/guides/design-system-integration.md | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/docs/guides/custom-components.md b/docs/guides/custom-components.md index 0059a90108..059db43d43 100644 --- a/docs/guides/custom-components.md +++ b/docs/guides/custom-components.md @@ -1,3 +1,7 @@ +--- +render_macros: false +--- + # Custom Component Catalogs Extend A2UI with a catalog of your own components — video players, maps, charts, or anything your application needs. diff --git a/docs/guides/design-system-integration.md b/docs/guides/design-system-integration.md index c73f608981..bfe8d0573c 100644 --- a/docs/guides/design-system-integration.md +++ b/docs/guides/design-system-integration.md @@ -1,3 +1,7 @@ +--- +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.