From 6766fb7c770b2706684827242b2c34b6f5043445 Mon Sep 17 00:00:00 2001 From: alan blount Date: Thu, 12 Mar 2026 03:10:34 +0000 Subject: [PATCH 01/46] 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/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 +++++++++++++++ 5 files changed, 399 insertions(+) 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/design-system-integration.md b/docs/guides/design-system-integration.md new file mode 100644 index 000000000..b3f9987ca --- /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 + +- [Defining Your Own Catalog](defining-your-own-catalog.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 000000000..6e6aa5848 --- /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 0f638cfa4..114df9f83 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 - Defining Your Own Catalog: guides/defining-your-own-catalog.md - Authoring Custom Components: guides/authoring-components.md - Theming & Styling: guides/theming.md 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 e7cc9f045..a4acf3234 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 000000000..7f698486e --- /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 b5141ae30c9f5d39a6185c33a704121408bca9f7 Mon Sep 17 00:00:00 2001 From: alan blount Date: Thu, 12 Mar 2026 09:36:29 -0400 Subject: [PATCH 02/46] Apply suggestions from code review Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- docs/guides/design-system-integration.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/guides/design-system-integration.md b/docs/guides/design-system-integration.md index b3f9987ca..a611f3788 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 63bd533e29a4f388ed86cbfc7d90e41bc77c9195 Mon Sep 17 00:00:00 2001 From: alan blount Date: Thu, 12 Mar 2026 13:46:15 +0000 Subject: [PATCH 03/46] 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/design-system-integration.md | 153 +++++++++++------- docs/guides/friction-log-custom-components.md | 121 -------------- .../rizzcharts/src/a2ui-catalog/youtube.ts | 9 ++ 3 files changed, 102 insertions(+), 181 deletions(-) delete mode 100644 docs/guides/friction-log-custom-components.md diff --git a/docs/guides/design-system-integration.md b/docs/guides/design-system-integration.md index a611f3788..01126321d 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 -- [Defining Your Own Catalog](defining-your-own-catalog.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 +- [Defining Your Own Catalog](defining-your-own-catalog.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 6e6aa5848..000000000 --- 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 7f698486e..914ea1858 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 d4e005ab7443037984da739dec48c18e5b6efef5 Mon Sep 17 00:00:00 2001 From: alan blount Date: Thu, 12 Mar 2026 14:43:26 +0000 Subject: [PATCH 04/46] docs: add render_macros:false to prevent Jinja2 eval of Angular template syntax --- docs/guides/design-system-integration.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/guides/design-system-integration.md b/docs/guides/design-system-integration.md index 01126321d..3313c7ff1 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. From 582f7e76f1a971046cb8ef90c8e55a90e03432ac Mon Sep 17 00:00:00 2001 From: alan blount Date: Sat, 14 Mar 2026 18:09:50 +0000 Subject: [PATCH 05/46] feat(dojo): Add A2UI Dojo and Mock Scenarios --- .../adk/contact_lookup/record_scenario.py | 37 + .../adk/contact_lookup/record_scenario.sh | 4 + .../adk/restaurant_finder/record_scenario.py | 37 + .../adk/restaurant_finder/record_scenario.sh | 4 + tools/composer/next.config.ts | 17 - tools/composer/pnpm-lock.yaml | 55 +- .../app/api/copilotkit/[[...slug]]/route.ts | 5 +- tools/composer/src/app/dojo/page.tsx | 233 +++ .../src/app/widget/[id]/ClientWidgetPage.tsx | 28 + tools/composer/src/app/widget/[id]/page.tsx | 50 +- .../src/components/dojo/useJsonlPlayer.ts | 82 ++ .../src/data/dojo/contact-lookup.json | 490 +++++++ tools/composer/src/data/dojo/index.ts | 11 + .../composer/src/data/dojo/kitchen-sink.json | 1285 +++++++++++++++++ .../src/data/dojo/restaurant-finder.json | 351 +++++ 15 files changed, 2611 insertions(+), 78 deletions(-) create mode 100644 samples/agent/adk/contact_lookup/record_scenario.py create mode 100755 samples/agent/adk/contact_lookup/record_scenario.sh create mode 100644 samples/agent/adk/restaurant_finder/record_scenario.py create mode 100755 samples/agent/adk/restaurant_finder/record_scenario.sh create mode 100644 tools/composer/src/app/dojo/page.tsx create mode 100644 tools/composer/src/app/widget/[id]/ClientWidgetPage.tsx create mode 100644 tools/composer/src/components/dojo/useJsonlPlayer.ts create mode 100644 tools/composer/src/data/dojo/contact-lookup.json create mode 100644 tools/composer/src/data/dojo/index.ts create mode 100644 tools/composer/src/data/dojo/kitchen-sink.json create mode 100644 tools/composer/src/data/dojo/restaurant-finder.json diff --git a/samples/agent/adk/contact_lookup/record_scenario.py b/samples/agent/adk/contact_lookup/record_scenario.py new file mode 100644 index 000000000..62b28dc07 --- /dev/null +++ b/samples/agent/adk/contact_lookup/record_scenario.py @@ -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()) diff --git a/samples/agent/adk/contact_lookup/record_scenario.sh b/samples/agent/adk/contact_lookup/record_scenario.sh new file mode 100755 index 000000000..0f8554505 --- /dev/null +++ b/samples/agent/adk/contact_lookup/record_scenario.sh @@ -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 diff --git a/samples/agent/adk/restaurant_finder/record_scenario.py b/samples/agent/adk/restaurant_finder/record_scenario.py new file mode 100644 index 000000000..df9d9d1c5 --- /dev/null +++ b/samples/agent/adk/restaurant_finder/record_scenario.py @@ -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()) diff --git a/samples/agent/adk/restaurant_finder/record_scenario.sh b/samples/agent/adk/restaurant_finder/record_scenario.sh new file mode 100755 index 000000000..0f8554505 --- /dev/null +++ b/samples/agent/adk/restaurant_finder/record_scenario.sh @@ -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 diff --git a/tools/composer/next.config.ts b/tools/composer/next.config.ts index eafcaa2a0..db0a37275 100644 --- a/tools/composer/next.config.ts +++ b/tools/composer/next.config.ts @@ -1,23 +1,6 @@ -/** - * Copyright 2026 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 - * - * http://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 type { NextConfig } from "next"; const nextConfig: NextConfig = { - /* config options here */ }; export default nextConfig; diff --git a/tools/composer/pnpm-lock.yaml b/tools/composer/pnpm-lock.yaml index bdff681fe..01908e4c2 100644 --- a/tools/composer/pnpm-lock.yaml +++ b/tools/composer/pnpm-lock.yaml @@ -1,17 +1,3 @@ -# Copyright 2026 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 -# -# http://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. - lockfileVersion: '9.0' settings: @@ -865,89 +851,105 @@ packages: resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==} cpu: [arm64] os: [linux] + libc: [glibc] '@img/sharp-libvips-linux-arm@1.2.4': resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==} cpu: [arm] os: [linux] + libc: [glibc] '@img/sharp-libvips-linux-ppc64@1.2.4': resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==} cpu: [ppc64] os: [linux] + libc: [glibc] '@img/sharp-libvips-linux-riscv64@1.2.4': resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==} cpu: [riscv64] os: [linux] + libc: [glibc] '@img/sharp-libvips-linux-s390x@1.2.4': resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==} cpu: [s390x] os: [linux] + libc: [glibc] '@img/sharp-libvips-linux-x64@1.2.4': resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==} cpu: [x64] os: [linux] + libc: [glibc] '@img/sharp-libvips-linuxmusl-arm64@1.2.4': resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==} cpu: [arm64] os: [linux] + libc: [musl] '@img/sharp-libvips-linuxmusl-x64@1.2.4': resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==} cpu: [x64] os: [linux] + libc: [musl] '@img/sharp-linux-arm64@0.34.5': resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] + libc: [glibc] '@img/sharp-linux-arm@0.34.5': resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm] os: [linux] + libc: [glibc] '@img/sharp-linux-ppc64@0.34.5': resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [ppc64] os: [linux] + libc: [glibc] '@img/sharp-linux-riscv64@0.34.5': resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [riscv64] os: [linux] + libc: [glibc] '@img/sharp-linux-s390x@0.34.5': resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [s390x] os: [linux] + libc: [glibc] '@img/sharp-linux-x64@0.34.5': resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] + libc: [glibc] '@img/sharp-linuxmusl-arm64@0.34.5': resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] + libc: [musl] '@img/sharp-linuxmusl-x64@0.34.5': resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] + libc: [musl] '@img/sharp-wasm32@0.34.5': resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==} @@ -1080,24 +1082,28 @@ packages: engines: {node: '>= 10'} cpu: [arm64] os: [linux] + libc: [glibc] '@next/swc-linux-arm64-musl@16.1.5': resolution: {integrity: sha512-U+kBxGUY1xMAzDTXmuVMfhaWUZQAwzRaHJ/I6ihtR5SbTVUEaDRiEU9YMjy1obBWpdOBuk1bcm+tsmifYSygfw==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] + libc: [musl] '@next/swc-linux-x64-gnu@16.1.5': resolution: {integrity: sha512-gq2UtoCpN7Ke/7tKaU7i/1L7eFLfhMbXjNghSv0MVGF1dmuoaPeEVDvkDuO/9LVa44h5gqpWeJ4mRRznjDv7LA==} engines: {node: '>= 10'} cpu: [x64] os: [linux] + libc: [glibc] '@next/swc-linux-x64-musl@16.1.5': resolution: {integrity: sha512-bQWSE729PbXT6mMklWLf8dotislPle2L70E9q6iwETYEOt092GDn0c+TTNj26AjmeceSsC4ndyGsK5nKqHYXjQ==} engines: {node: '>= 10'} cpu: [x64] os: [linux] + libc: [musl] '@next/swc-win32-arm64-msvc@16.1.5': resolution: {integrity: sha512-LZli0anutkIllMtTAWZlDqdfvjWX/ch8AFK5WgkNTvaqwlouiD1oHM+WW8RXMiL0+vAkAJyAGEzPPjO+hnrSNQ==} @@ -1564,66 +1570,79 @@ packages: resolution: {integrity: sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==} cpu: [arm] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm-musleabihf@4.59.0': resolution: {integrity: sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==} cpu: [arm] os: [linux] + libc: [musl] '@rollup/rollup-linux-arm64-gnu@4.59.0': resolution: {integrity: sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==} cpu: [arm64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm64-musl@4.59.0': resolution: {integrity: sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==} cpu: [arm64] os: [linux] + libc: [musl] '@rollup/rollup-linux-loong64-gnu@4.59.0': resolution: {integrity: sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==} cpu: [loong64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-loong64-musl@4.59.0': resolution: {integrity: sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==} cpu: [loong64] os: [linux] + libc: [musl] '@rollup/rollup-linux-ppc64-gnu@4.59.0': resolution: {integrity: sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==} cpu: [ppc64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-ppc64-musl@4.59.0': resolution: {integrity: sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==} cpu: [ppc64] os: [linux] + libc: [musl] '@rollup/rollup-linux-riscv64-gnu@4.59.0': resolution: {integrity: sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==} cpu: [riscv64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-riscv64-musl@4.59.0': resolution: {integrity: sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==} cpu: [riscv64] os: [linux] + libc: [musl] '@rollup/rollup-linux-s390x-gnu@4.59.0': resolution: {integrity: sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==} cpu: [s390x] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-gnu@4.59.0': resolution: {integrity: sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==} cpu: [x64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-musl@4.59.0': resolution: {integrity: sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==} cpu: [x64] os: [linux] + libc: [musl] '@rollup/rollup-openbsd-x64@4.59.0': resolution: {integrity: sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==} @@ -1736,24 +1755,28 @@ packages: engines: {node: '>= 10'} cpu: [arm64] os: [linux] + libc: [glibc] '@tailwindcss/oxide-linux-arm64-musl@4.1.16': resolution: {integrity: sha512-H81UXMa9hJhWhaAUca6bU2wm5RRFpuHImrwXBUvPbYb+3jo32I9VIwpOX6hms0fPmA6f2pGVlybO6qU8pF4fzQ==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] + libc: [musl] '@tailwindcss/oxide-linux-x64-gnu@4.1.16': resolution: {integrity: sha512-ZGHQxDtFC2/ruo7t99Qo2TTIvOERULPl5l0K1g0oK6b5PGqjYMga+FcY1wIUnrUxY56h28FxybtDEla+ICOyew==} engines: {node: '>= 10'} cpu: [x64] os: [linux] + libc: [glibc] '@tailwindcss/oxide-linux-x64-musl@4.1.16': resolution: {integrity: sha512-Oi1tAaa0rcKf1Og9MzKeINZzMLPbhxvm7rno5/zuP1WYmpiG0bEHq4AcRUiG2165/WUzvxkW4XDYCscZWbTLZw==} engines: {node: '>= 10'} cpu: [x64] os: [linux] + libc: [musl] '@tailwindcss/oxide-wasm32-wasi@4.1.16': resolution: {integrity: sha512-B01u/b8LteGRwucIBmCQ07FVXLzImWESAIMcUU6nvFt/tYsQ6IHz8DmZ5KtvmwxD+iTYBtM1xwoGXswnlu9v0Q==} @@ -3626,24 +3649,28 @@ packages: engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] + libc: [glibc] lightningcss-linux-arm64-musl@1.30.2: resolution: {integrity: sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] + libc: [musl] lightningcss-linux-x64-gnu@1.30.2: resolution: {integrity: sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] + libc: [glibc] lightningcss-linux-x64-musl@1.30.2: resolution: {integrity: sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] + libc: [musl] lightningcss-win32-arm64-msvc@1.30.2: resolution: {integrity: sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==} diff --git a/tools/composer/src/app/api/copilotkit/[[...slug]]/route.ts b/tools/composer/src/app/api/copilotkit/[[...slug]]/route.ts index 940673d30..0afb5cec1 100644 --- a/tools/composer/src/app/api/copilotkit/[[...slug]]/route.ts +++ b/tools/composer/src/app/api/copilotkit/[[...slug]]/route.ts @@ -1,3 +1,4 @@ +export const runtime = 'edge'; /** * Copyright 2026 Google LLC * @@ -54,13 +55,13 @@ const agent = new BuiltInAgent({ temperature: 0.7, }); -const runtime = new CopilotRuntime({ +const copilotRuntime = new CopilotRuntime({ agents: { default: agent }, runner: new InMemoryAgentRunner(), }); const app = createCopilotEndpoint({ - runtime, + runtime: copilotRuntime, basePath: "/api/copilotkit", }); diff --git a/tools/composer/src/app/dojo/page.tsx b/tools/composer/src/app/dojo/page.tsx new file mode 100644 index 000000000..0949efefd --- /dev/null +++ b/tools/composer/src/app/dojo/page.tsx @@ -0,0 +1,233 @@ +'use client'; + +import React, { useState } from 'react'; +import { Play, Pause, SkipBack, FastForward, Settings, FileJson, LayoutTemplate } from 'lucide-react'; +import { useJsonlPlayer } from '@/components/dojo/useJsonlPlayer'; +import { Button } from '@/components/ui/button'; +import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from '@/components/ui/resizable'; +import { Separator } from '@/components/ui/separator'; +import { scenarios, ScenarioId } from '@/data/dojo'; + +export default function DojoPage() { + const [activeTab, setActiveTab] = useState<'data' | 'config'>('data'); + const [renderers, setRenderers] = useState({ react: true, lit: false, discord: true }); + const [selectedScenario, setSelectedScenario] = useState('kitchen-sink'); + + const { + playbackState, + progress, + totalMessages, + speed, + activeMessages, + play, + pause, + stop, + seek, + setSpeed + } = useJsonlPlayer({ + messages: (scenarios[selectedScenario] as any) || [], + autoPlay: false, + baseIntervalMs: 1000 + }); + + const toggleTab = () => setActiveTab((prev) => (prev === 'data' ? 'config' : 'data')); + + return ( +
+ {/* Top Header / Command Center */} +
+ {/* Toggle Data / Config */} +
+ + +
+ + + + {/* Playback Scrubber & Controls */} +
+
+ + {playbackState === 'playing' ? ( + + ) : ( + + )} +
+ + {/* Timeline Scrubber */} +
+ {progress + 1} + seek(parseInt(e.target.value, 10))} + className="flex-1 h-2 bg-secondary rounded-lg appearance-none cursor-pointer" + /> + {totalMessages} +
+ + {/* Speed Toggle */} + +
+ + + + {/* Scenario Selection Placeholder */} +
+ +
+
+ + {/* Main Split Layout */} + + + {/* Left Pane: JSONL Source or Configuration */} + +
+

+ {activeTab === 'data' ? 'JSONL Stream (SSE)' : 'Renderer Configuration'} +

+ + {activeTab === 'data' ? ( +
+ {(scenarios[selectedScenario] as any).map((msg: any, index: number) => ( +
+
{JSON.stringify(msg, null, 2)}
+
+ ))} +
+ ) : ( +
+ {/* Mock Config Toggles */} +
+

Active Surfaces

+ + + +
+ +
+

Transport Configuration

+ +
+
+ )} +
+
+ + + + {/* Right Pane: Active Renderers */} + +
+
+ + {renderers.react && ( +
+
+ React Renderer +
+
+ {/* Placeholder for real React A2UI Renderer component */} +
+

{''}

+

Currently displaying {activeMessages.length} elements

+
+
+
+ )} + + {renderers.discord && ( +
+
+ Discord Renderer (Adapter) +
+
+ {/* Mock Discord UI */} +
+
+
+
+ Agent + Today at 4:20 PM +
+
+

Simulated Discord embed updating via {activeMessages.length} states.

+
+
+
+
+
+ )} + +
+
+ + + +
+ ); +} diff --git a/tools/composer/src/app/widget/[id]/ClientWidgetPage.tsx b/tools/composer/src/app/widget/[id]/ClientWidgetPage.tsx new file mode 100644 index 000000000..bc5018f88 --- /dev/null +++ b/tools/composer/src/app/widget/[id]/ClientWidgetPage.tsx @@ -0,0 +1,28 @@ +'use client'; + +import { WidgetEditor } from '@/components/editor/widget-editor'; +import { useWidgets } from '@/contexts/widgets-context'; + +export function ClientWidgetPage({ id }: { id: string }) { + const { loading, getWidget } = useWidgets(); + + if (loading) { + return ( +
+
Loading...
+
+ ); + } + + const widget = getWidget(id); + + if (!widget) { + return ( +
+
Widget not found
+
+ ); + } + + return ; +} diff --git a/tools/composer/src/app/widget/[id]/page.tsx b/tools/composer/src/app/widget/[id]/page.tsx index ec54c84d1..1bbe6c455 100644 --- a/tools/composer/src/app/widget/[id]/page.tsx +++ b/tools/composer/src/app/widget/[id]/page.tsx @@ -1,50 +1,10 @@ +export const runtime = 'edge'; /** * Copyright 2026 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 - * - * http://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 { ClientWidgetPage } from './ClientWidgetPage'; -'use client'; - -import { use } from 'react'; -import { WidgetEditor } from '@/components/editor/widget-editor'; -import { useWidgets } from '@/contexts/widgets-context'; - -interface WidgetPageProps { - params: Promise<{ id: string }>; -} - -export default function WidgetPage({ params }: WidgetPageProps) { - const { id } = use(params); - const { loading, getWidget } = useWidgets(); - - if (loading) { - return ( -
-
Loading...
-
- ); - } - - const widget = getWidget(id); - - if (!widget) { - return ( -
-
Widget not found
-
- ); - } - - return ; +export default async function WidgetPage({ params }: { params: Promise<{ id: string }> }) { + const { id } = await params; + return ; } diff --git a/tools/composer/src/components/dojo/useJsonlPlayer.ts b/tools/composer/src/components/dojo/useJsonlPlayer.ts new file mode 100644 index 000000000..7c8bc2213 --- /dev/null +++ b/tools/composer/src/components/dojo/useJsonlPlayer.ts @@ -0,0 +1,82 @@ +import { useState, useEffect, useCallback, useRef } from 'react'; + +export type PlaybackState = 'playing' | 'paused' | 'stopped'; + +export interface UseJsonlPlayerOptions { + messages: T[]; + autoPlay?: boolean; + baseIntervalMs?: number; +} + +export function useJsonlPlayer({ + messages, + autoPlay = false, + baseIntervalMs = 500, +}: UseJsonlPlayerOptions) { + const [playbackState, setPlaybackState] = useState( + autoPlay ? 'playing' : 'stopped' + ); + const [progress, setProgress] = useState(0); // Index of the last applied message + const [speed, setSpeed] = useState(1); + const timerRef = useRef | null>(null); + + const totalMessages = messages.length; + + const play = useCallback(() => { + if (progress >= totalMessages - 1) { + setProgress(0); // Loop or restart + } + setPlaybackState('playing'); + }, [progress, totalMessages]); + + const pause = useCallback(() => { + setPlaybackState('paused'); + }, []); + + const stop = useCallback(() => { + setPlaybackState('stopped'); + setProgress(0); + }, []); + + const seek = useCallback((index: number) => { + if (index >= 0 && index < totalMessages) { + setProgress(index); + } + }, [totalMessages]); + + useEffect(() => { + if (playbackState === 'playing') { + const ms = baseIntervalMs / speed; + timerRef.current = setInterval(() => { + setProgress((prev) => { + if (prev >= totalMessages - 1) { + setPlaybackState('paused'); + return prev; + } + return prev + 1; + }); + }, ms); + } else if (timerRef.current) { + clearInterval(timerRef.current); + } + + return () => { + if (timerRef.current) clearInterval(timerRef.current); + }; + }, [playbackState, speed, totalMessages, baseIntervalMs]); + + const activeMessages = messages.slice(0, progress + 1); + + return { + playbackState, + progress, + speed, + totalMessages, + activeMessages, + play, + pause, + stop, + seek, + setSpeed, + }; +} diff --git a/tools/composer/src/data/dojo/contact-lookup.json b/tools/composer/src/data/dojo/contact-lookup.json new file mode 100644 index 000000000..7f8da3cf4 --- /dev/null +++ b/tools/composer/src/data/dojo/contact-lookup.json @@ -0,0 +1,490 @@ +[ + { + "beginRendering": { + "surfaceId": "contact-card", + "root": "main_card" + } + }, + { + "surfaceUpdate": { + "surfaceId": "contact-card", + "components": [ + { + "id": "profile_image", + "component": { + "Image": { + "url": { + "path": "/imageUrl" + }, + "usageHint": "avatar", + "fit": "cover" + } + } + }, + { + "id": "user_heading", + "weight": 1, + "component": { + "Text": { + "text": { + "path": "/name" + }, + "usageHint": "h2" + } + } + }, + { + "id": "description_text_1", + "component": { + "Text": { + "text": { + "path": "/title" + } + } + } + }, + { + "id": "description_text_2", + "component": { + "Text": { + "text": { + "path": "/team" + } + } + } + }, + { + "id": "description_column", + "component": { + "Column": { + "children": { + "explicitList": [ + "user_heading", + "description_text_1", + "description_text_2" + ] + }, + "alignment": "center" + } + } + }, + { + "id": "calendar_icon", + "component": { + "Icon": { + "name": { + "literalString": "calendarToday" + } + } + } + }, + { + "id": "calendar_primary_text", + "component": { + "Text": { + "usageHint": "h5", + "text": { + "path": "/calendar" + } + } + } + }, + { + "id": "calendar_secondary_text", + "component": { + "Text": { + "text": { + "literalString": "Calendar" + } + } + } + }, + { + "id": "calendar_text_column", + "component": { + "Column": { + "children": { + "explicitList": [ + "calendar_primary_text", + "calendar_secondary_text" + ] + }, + "distribution": "start", + "alignment": "start" + } + } + }, + { + "id": "info_row_1", + "component": { + "Row": { + "children": { + "explicitList": [ + "calendar_icon", + "calendar_text_column" + ] + }, + "distribution": "start", + "alignment": "start" + } + } + }, + { + "id": "location_icon", + "component": { + "Icon": { + "name": { + "literalString": "locationOn" + } + } + } + }, + { + "id": "location_primary_text", + "component": { + "Text": { + "usageHint": "h5", + "text": { + "path": "/location" + } + } + } + }, + { + "id": "location_secondary_text", + "component": { + "Text": { + "text": { + "literalString": "Location" + } + } + } + }, + { + "id": "location_text_column", + "component": { + "Column": { + "children": { + "explicitList": [ + "location_primary_text", + "location_secondary_text" + ] + }, + "distribution": "start", + "alignment": "start" + } + } + }, + { + "id": "info_row_2", + "component": { + "Row": { + "children": { + "explicitList": [ + "location_icon", + "location_text_column" + ] + }, + "distribution": "start", + "alignment": "start" + } + } + }, + { + "id": "mail_icon", + "component": { + "Icon": { + "name": { + "literalString": "mail" + } + } + } + }, + { + "id": "mail_primary_text", + "component": { + "Text": { + "usageHint": "h5", + "text": { + "path": "/email" + } + } + } + }, + { + "id": "mail_secondary_text", + "component": { + "Text": { + "text": { + "literalString": "Email" + } + } + } + }, + { + "id": "mail_text_column", + "component": { + "Column": { + "children": { + "explicitList": [ + "mail_primary_text", + "mail_secondary_text" + ] + }, + "distribution": "start", + "alignment": "start" + } + } + }, + { + "id": "info_row_3", + "component": { + "Row": { + "children": { + "explicitList": [ + "mail_icon", + "mail_text_column" + ] + }, + "distribution": "start", + "alignment": "start" + } + } + }, + { + "id": "div", + "component": { + "Divider": {} + } + }, + { + "id": "call_icon", + "component": { + "Icon": { + "name": { + "literalString": "call" + } + } + } + }, + { + "id": "call_primary_text", + "component": { + "Text": { + "usageHint": "h5", + "text": { + "path": "/mobile" + } + } + } + }, + { + "id": "call_secondary_text", + "component": { + "Text": { + "text": { + "literalString": "Mobile" + } + } + } + }, + { + "id": "call_text_column", + "component": { + "Column": { + "children": { + "explicitList": [ + "call_primary_text", + "call_secondary_text" + ] + }, + "distribution": "start", + "alignment": "start" + } + } + }, + { + "id": "info_row_4", + "component": { + "Row": { + "children": { + "explicitList": [ + "call_icon", + "call_text_column" + ] + }, + "distribution": "start", + "alignment": "start" + } + } + }, + { + "id": "info_rows_column", + "weight": 1, + "component": { + "Column": { + "children": { + "explicitList": [ + "info_row_1", + "info_row_2", + "info_row_3", + "info_row_4" + ] + }, + "alignment": "stretch" + } + } + }, + { + "id": "button_1_text", + "component": { + "Text": { + "text": { + "literalString": "Follow" + } + } + } + }, + { + "id": "button_1", + "component": { + "Button": { + "child": "button_1_text", + "primary": true, + "action": { + "name": "follow_contact" + } + } + } + }, + { + "id": "button_2_text", + "component": { + "Text": { + "text": { + "literalString": "Message" + } + } + } + }, + { + "id": "button_2", + "component": { + "Button": { + "child": "button_2_text", + "primary": false, + "action": { + "name": "send_message" + } + } + } + }, + { + "id": "action_buttons_row", + "component": { + "Row": { + "children": { + "explicitList": [ + "button_1", + "button_2" + ] + }, + "distribution": "center", + "alignment": "center" + } + } + }, + { + "id": "link_text", + "component": { + "Text": { + "text": { + "literalString": "[View Full Profile](/profile)" + } + } + } + }, + { + "id": "link_text_wrapper", + "component": { + "Row": { + "children": { + "explicitList": [ + "link_text" + ] + }, + "distribution": "center", + "alignment": "center" + } + } + }, + { + "id": "main_column", + "component": { + "Column": { + "children": { + "explicitList": [ + "profile_image", + "description_column", + "div", + "info_rows_column", + "action_buttons_row", + "link_text_wrapper" + ] + }, + "alignment": "stretch" + } + } + }, + { + "id": "main_card", + "component": { + "Card": { + "child": "main_column" + } + } + } + ] + } + }, + { + "dataModelUpdate": { + "surfaceId": "contact-card", + "path": "/", + "contents": [ + { + "key": "name", + "valueString": "Alex Jordan" + }, + { + "key": "title", + "valueString": "Product Marketing Manager" + }, + { + "key": "team", + "valueString": "Team Macally" + }, + { + "key": "location", + "valueString": "New York" + }, + { + "key": "email", + "valueString": "alex.jordan@example.com" + }, + { + "key": "mobile", + "valueString": "+1 (415) 171-1080" + }, + { + "key": "calendar", + "valueString": "Free until 4:00 PM" + }, + { + "key": "imageUrl", + "valueString": "http://localhost:10006/static/profile1.png" + } + ] + } + } +] \ No newline at end of file diff --git a/tools/composer/src/data/dojo/index.ts b/tools/composer/src/data/dojo/index.ts new file mode 100644 index 000000000..862ce5b0c --- /dev/null +++ b/tools/composer/src/data/dojo/index.ts @@ -0,0 +1,11 @@ +import kitchenSink from './kitchen-sink.json'; +import contactLookup from './contact-lookup.json'; +import restaurantFinder from './restaurant-finder.json'; + +export const scenarios = { + 'kitchen-sink': kitchenSink, + 'contact-lookup': contactLookup, + 'restaurant-finder': restaurantFinder, +}; + +export type ScenarioId = keyof typeof scenarios; diff --git a/tools/composer/src/data/dojo/kitchen-sink.json b/tools/composer/src/data/dojo/kitchen-sink.json new file mode 100644 index 000000000..5a2209f2b --- /dev/null +++ b/tools/composer/src/data/dojo/kitchen-sink.json @@ -0,0 +1,1285 @@ +[ + { + "beginRendering": { + "surfaceId": "demo-text", + "root": "demo-text-root" + } + }, + { + "surfaceUpdate": { + "surfaceId": "demo-text", + "components": [ + { + "id": "demo-text-root", + "component": { + "TextField": { + "label": { + "literalString": "Enter some text" + }, + "text": { + "path": "galleryData/textField" + } + } + } + } + ] + } + }, + { + "dataModelUpdate": { + "surfaceId": "demo-text", + "contents": [ + { + "key": "galleryData", + "valueMap": [ + { + "key": "textField", + "valueString": "Hello World" + }, + { + "key": "checkbox", + "valueBoolean": false + }, + { + "key": "checkboxChecked", + "valueBoolean": true + }, + { + "key": "slider", + "valueNumber": 30 + }, + { + "key": "date", + "valueString": "2025-10-26" + }, + { + "key": "favorites", + "valueMap": [ + { + "key": "0", + "valueString": "A" + } + ] + }, + { + "key": "favoritesChips", + "valueMap": [] + }, + { + "key": "favoritesFilter", + "valueMap": [] + } + ] + } + ] + } + }, + { + "beginRendering": { + "surfaceId": "demo-text-regex", + "root": "demo-text-regex-root" + } + }, + { + "surfaceUpdate": { + "surfaceId": "demo-text-regex", + "components": [ + { + "id": "demo-text-regex-root", + "component": { + "TextField": { + "label": { + "literalString": "Enter exactly 5 digits" + }, + "text": { + "path": "galleryData/textFieldRegex" + }, + "validationRegexp": "^\\d{5}$" + } + } + } + ] + } + }, + { + "dataModelUpdate": { + "surfaceId": "demo-text-regex", + "contents": [ + { + "key": "galleryData", + "valueMap": [ + { + "key": "textField", + "valueString": "Hello World" + }, + { + "key": "checkbox", + "valueBoolean": false + }, + { + "key": "checkboxChecked", + "valueBoolean": true + }, + { + "key": "slider", + "valueNumber": 30 + }, + { + "key": "date", + "valueString": "2025-10-26" + }, + { + "key": "favorites", + "valueMap": [ + { + "key": "0", + "valueString": "A" + } + ] + }, + { + "key": "favoritesChips", + "valueMap": [] + }, + { + "key": "favoritesFilter", + "valueMap": [] + } + ] + } + ] + } + }, + { + "beginRendering": { + "surfaceId": "demo-checkbox", + "root": "demo-checkbox-root" + } + }, + { + "surfaceUpdate": { + "surfaceId": "demo-checkbox", + "components": [ + { + "id": "demo-checkbox-root", + "component": { + "CheckBox": { + "label": { + "literalString": "Toggle me" + }, + "value": { + "path": "galleryData/checkbox" + } + } + } + } + ] + } + }, + { + "dataModelUpdate": { + "surfaceId": "demo-checkbox", + "contents": [ + { + "key": "galleryData", + "valueMap": [ + { + "key": "textField", + "valueString": "Hello World" + }, + { + "key": "checkbox", + "valueBoolean": false + }, + { + "key": "checkboxChecked", + "valueBoolean": true + }, + { + "key": "slider", + "valueNumber": 30 + }, + { + "key": "date", + "valueString": "2025-10-26" + }, + { + "key": "favorites", + "valueMap": [ + { + "key": "0", + "valueString": "A" + } + ] + }, + { + "key": "favoritesChips", + "valueMap": [] + }, + { + "key": "favoritesFilter", + "valueMap": [] + } + ] + } + ] + } + }, + { + "beginRendering": { + "surfaceId": "demo-slider", + "root": "demo-slider-root" + } + }, + { + "surfaceUpdate": { + "surfaceId": "demo-slider", + "components": [ + { + "id": "demo-slider-root", + "component": { + "Slider": { + "value": { + "path": "galleryData/slider" + }, + "minValue": 0, + "maxValue": 100 + } + } + } + ] + } + }, + { + "dataModelUpdate": { + "surfaceId": "demo-slider", + "contents": [ + { + "key": "galleryData", + "valueMap": [ + { + "key": "textField", + "valueString": "Hello World" + }, + { + "key": "checkbox", + "valueBoolean": false + }, + { + "key": "checkboxChecked", + "valueBoolean": true + }, + { + "key": "slider", + "valueNumber": 30 + }, + { + "key": "date", + "valueString": "2025-10-26" + }, + { + "key": "favorites", + "valueMap": [ + { + "key": "0", + "valueString": "A" + } + ] + }, + { + "key": "favoritesChips", + "valueMap": [] + }, + { + "key": "favoritesFilter", + "valueMap": [] + } + ] + } + ] + } + }, + { + "beginRendering": { + "surfaceId": "demo-date", + "root": "demo-date-root" + } + }, + { + "surfaceUpdate": { + "surfaceId": "demo-date", + "components": [ + { + "id": "demo-date-root", + "component": { + "DateTimeInput": { + "value": { + "path": "galleryData/date" + }, + "enableDate": true + } + } + } + ] + } + }, + { + "dataModelUpdate": { + "surfaceId": "demo-date", + "contents": [ + { + "key": "galleryData", + "valueMap": [ + { + "key": "textField", + "valueString": "Hello World" + }, + { + "key": "checkbox", + "valueBoolean": false + }, + { + "key": "checkboxChecked", + "valueBoolean": true + }, + { + "key": "slider", + "valueNumber": 30 + }, + { + "key": "date", + "valueString": "2025-10-26" + }, + { + "key": "favorites", + "valueMap": [ + { + "key": "0", + "valueString": "A" + } + ] + }, + { + "key": "favoritesChips", + "valueMap": [] + }, + { + "key": "favoritesFilter", + "valueMap": [] + } + ] + } + ] + } + }, + { + "beginRendering": { + "surfaceId": "demo-multichoice", + "root": "demo-multichoice-root" + } + }, + { + "surfaceUpdate": { + "surfaceId": "demo-multichoice", + "components": [ + { + "id": "demo-multichoice-root", + "component": { + "MultipleChoice": { + "selections": { + "path": "galleryData/favorites" + }, + "options": [ + { + "label": { + "literalString": "Apple" + }, + "value": "A" + }, + { + "label": { + "literalString": "Banana" + }, + "value": "B" + }, + { + "label": { + "literalString": "Cherry" + }, + "value": "C" + } + ] + } + } + } + ] + } + }, + { + "dataModelUpdate": { + "surfaceId": "demo-multichoice", + "contents": [ + { + "key": "galleryData", + "valueMap": [ + { + "key": "textField", + "valueString": "Hello World" + }, + { + "key": "checkbox", + "valueBoolean": false + }, + { + "key": "checkboxChecked", + "valueBoolean": true + }, + { + "key": "slider", + "valueNumber": 30 + }, + { + "key": "date", + "valueString": "2025-10-26" + }, + { + "key": "favorites", + "valueMap": [ + { + "key": "0", + "valueString": "A" + } + ] + }, + { + "key": "favoritesChips", + "valueMap": [] + }, + { + "key": "favoritesFilter", + "valueMap": [] + } + ] + } + ] + } + }, + { + "beginRendering": { + "surfaceId": "demo-multichoice-chips", + "root": "demo-multichoice-chips-root" + } + }, + { + "surfaceUpdate": { + "surfaceId": "demo-multichoice-chips", + "components": [ + { + "id": "demo-multichoice-chips-root", + "component": { + "MultipleChoice": { + "selections": { + "path": "galleryData/favoritesChips" + }, + "description": "Select tags (Chips)", + "variant": "chips", + "options": [ + { + "label": { + "literalString": "Work" + }, + "value": "work" + }, + { + "label": { + "literalString": "Home" + }, + "value": "home" + }, + { + "label": { + "literalString": "Urgent" + }, + "value": "urgent" + }, + { + "label": { + "literalString": "Later" + }, + "value": "later" + } + ] + } + } + } + ] + } + }, + { + "dataModelUpdate": { + "surfaceId": "demo-multichoice-chips", + "contents": [ + { + "key": "galleryData", + "valueMap": [ + { + "key": "textField", + "valueString": "Hello World" + }, + { + "key": "checkbox", + "valueBoolean": false + }, + { + "key": "checkboxChecked", + "valueBoolean": true + }, + { + "key": "slider", + "valueNumber": 30 + }, + { + "key": "date", + "valueString": "2025-10-26" + }, + { + "key": "favorites", + "valueMap": [ + { + "key": "0", + "valueString": "A" + } + ] + }, + { + "key": "favoritesChips", + "valueMap": [] + }, + { + "key": "favoritesFilter", + "valueMap": [] + } + ] + } + ] + } + }, + { + "beginRendering": { + "surfaceId": "demo-multichoice-filter", + "root": "demo-multichoice-filter-root" + } + }, + { + "surfaceUpdate": { + "surfaceId": "demo-multichoice-filter", + "components": [ + { + "id": "demo-multichoice-filter-root", + "component": { + "MultipleChoice": { + "selections": { + "path": "galleryData/favoritesFilter" + }, + "description": "Select countries (Filterable)", + "filterable": true, + "options": [ + { + "label": { + "literalString": "United States" + }, + "value": "US" + }, + { + "label": { + "literalString": "Canada" + }, + "value": "CA" + }, + { + "label": { + "literalString": "United Kingdom" + }, + "value": "UK" + }, + { + "label": { + "literalString": "Australia" + }, + "value": "AU" + }, + { + "label": { + "literalString": "Germany" + }, + "value": "DE" + }, + { + "label": { + "literalString": "France" + }, + "value": "FR" + }, + { + "label": { + "literalString": "Japan" + }, + "value": "JP" + } + ] + } + } + } + ] + } + }, + { + "dataModelUpdate": { + "surfaceId": "demo-multichoice-filter", + "contents": [ + { + "key": "galleryData", + "valueMap": [ + { + "key": "textField", + "valueString": "Hello World" + }, + { + "key": "checkbox", + "valueBoolean": false + }, + { + "key": "checkboxChecked", + "valueBoolean": true + }, + { + "key": "slider", + "valueNumber": 30 + }, + { + "key": "date", + "valueString": "2025-10-26" + }, + { + "key": "favorites", + "valueMap": [ + { + "key": "0", + "valueString": "A" + } + ] + }, + { + "key": "favoritesChips", + "valueMap": [] + }, + { + "key": "favoritesFilter", + "valueMap": [] + } + ] + } + ] + } + }, + { + "beginRendering": { + "surfaceId": "demo-image", + "root": "demo-image-root" + } + }, + { + "surfaceUpdate": { + "surfaceId": "demo-image", + "components": [ + { + "id": "demo-image-root", + "component": { + "Image": { + "url": { + "literalString": "http://localhost:10005/assets/a2ui.png" + }, + "usageHint": "mediumFeature" + } + } + } + ] + } + }, + { + "dataModelUpdate": { + "surfaceId": "demo-image", + "contents": [ + { + "key": "galleryData", + "valueMap": [ + { + "key": "textField", + "valueString": "Hello World" + }, + { + "key": "checkbox", + "valueBoolean": false + }, + { + "key": "checkboxChecked", + "valueBoolean": true + }, + { + "key": "slider", + "valueNumber": 30 + }, + { + "key": "date", + "valueString": "2025-10-26" + }, + { + "key": "favorites", + "valueMap": [ + { + "key": "0", + "valueString": "A" + } + ] + }, + { + "key": "favoritesChips", + "valueMap": [] + }, + { + "key": "favoritesFilter", + "valueMap": [] + } + ] + } + ] + } + }, + { + "beginRendering": { + "surfaceId": "demo-button", + "root": "demo-button-root" + } + }, + { + "surfaceUpdate": { + "surfaceId": "demo-button", + "components": [ + { + "id": "demo-button-text", + "component": { + "Text": { + "text": { + "literalString": "Trigger Action" + } + } + } + }, + { + "id": "demo-button-root", + "component": { + "Button": { + "child": "demo-button-text", + "primary": true, + "action": { + "name": "custom_action", + "context": [ + { + "key": "info", + "value": { + "literalString": "Custom Button Clicked" + } + } + ] + } + } + } + } + ] + } + }, + { + "beginRendering": { + "surfaceId": "demo-tabs", + "root": "demo-tabs-root" + } + }, + { + "surfaceUpdate": { + "surfaceId": "demo-tabs", + "components": [ + { + "id": "tab-1-content", + "component": { + "Text": { + "text": { + "literalString": "First Tab Content" + } + } + } + }, + { + "id": "tab-2-content", + "component": { + "Text": { + "text": { + "literalString": "Second Tab Content" + } + } + } + }, + { + "id": "demo-tabs-root", + "component": { + "Tabs": { + "tabItems": [ + { + "title": { + "literalString": "View One" + }, + "child": "tab-1-content" + }, + { + "title": { + "literalString": "View Two" + }, + "child": "tab-2-content" + } + ] + } + } + } + ] + } + }, + { + "beginRendering": { + "surfaceId": "demo-icon", + "root": "icon-root" + } + }, + { + "surfaceUpdate": { + "surfaceId": "demo-icon", + "components": [ + { + "id": "icon-root", + "component": { + "Row": { + "children": { + "explicitList": [ + "icon-1", + "icon-2", + "icon-3" + ] + }, + "distribution": "spaceEvenly", + "alignment": "center" + } + } + }, + { + "id": "icon-1", + "component": { + "Icon": { + "name": { + "literalString": "star" + } + } + } + }, + { + "id": "icon-2", + "component": { + "Icon": { + "name": { + "literalString": "home" + } + } + } + }, + { + "id": "icon-3", + "component": { + "Icon": { + "name": { + "literalString": "settings" + } + } + } + } + ] + } + }, + { + "beginRendering": { + "surfaceId": "demo-divider", + "root": "div-root" + } + }, + { + "surfaceUpdate": { + "surfaceId": "demo-divider", + "components": [ + { + "id": "div-root", + "component": { + "Column": { + "children": { + "explicitList": [ + "div-text-1", + "div-horiz", + "div-text-2" + ] + }, + "distribution": "start", + "alignment": "stretch" + } + } + }, + { + "id": "div-text-1", + "component": { + "Text": { + "text": { + "literalString": "Above Divider" + } + } + } + }, + { + "id": "div-horiz", + "component": { + "Divider": { + "axis": "horizontal" + } + } + }, + { + "id": "div-text-2", + "component": { + "Text": { + "text": { + "literalString": "Below Divider" + } + } + } + } + ] + } + }, + { + "beginRendering": { + "surfaceId": "demo-card", + "root": "card-root" + } + }, + { + "surfaceUpdate": { + "surfaceId": "demo-card", + "components": [ + { + "id": "card-root", + "component": { + "Card": { + "child": "card-text" + } + } + }, + { + "id": "card-text", + "component": { + "Text": { + "text": { + "literalString": "I am inside a Card" + } + } + } + } + ] + } + }, + { + "beginRendering": { + "surfaceId": "demo-video", + "root": "demo-video-root" + } + }, + { + "surfaceUpdate": { + "surfaceId": "demo-video", + "components": [ + { + "id": "demo-video-root", + "component": { + "Video": { + "url": { + "literalString": "https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4" + } + } + } + } + ] + } + }, + { + "dataModelUpdate": { + "surfaceId": "demo-video", + "contents": [ + { + "key": "galleryData", + "valueMap": [ + { + "key": "textField", + "valueString": "Hello World" + }, + { + "key": "checkbox", + "valueBoolean": false + }, + { + "key": "checkboxChecked", + "valueBoolean": true + }, + { + "key": "slider", + "valueNumber": 30 + }, + { + "key": "date", + "valueString": "2025-10-26" + }, + { + "key": "favorites", + "valueMap": [ + { + "key": "0", + "valueString": "A" + } + ] + }, + { + "key": "favoritesChips", + "valueMap": [] + }, + { + "key": "favoritesFilter", + "valueMap": [] + } + ] + } + ] + } + }, + { + "beginRendering": { + "surfaceId": "demo-modal", + "root": "modal-root" + } + }, + { + "surfaceUpdate": { + "surfaceId": "demo-modal", + "components": [ + { + "id": "modal-root", + "component": { + "Modal": { + "entryPointChild": "modal-btn", + "contentChild": "modal-content" + } + } + }, + { + "id": "modal-btn", + "component": { + "Button": { + "child": "modal-btn-text", + "primary": false, + "action": { + "name": "noop" + } + } + } + }, + { + "id": "modal-btn-text", + "component": { + "Text": { + "text": { + "literalString": "Open Modal" + } + } + } + }, + { + "id": "modal-content", + "component": { + "Text": { + "text": { + "literalString": "This is the modal content!" + } + } + } + } + ] + } + }, + { + "beginRendering": { + "surfaceId": "demo-list", + "root": "list-root" + } + }, + { + "surfaceUpdate": { + "surfaceId": "demo-list", + "components": [ + { + "id": "list-root", + "component": { + "List": { + "children": { + "explicitList": [ + "list-item-1", + "list-item-2", + "list-item-3" + ] + }, + "direction": "vertical", + "alignment": "stretch" + } + } + }, + { + "id": "list-item-1", + "component": { + "Text": { + "text": { + "literalString": "Item 1" + } + } + } + }, + { + "id": "list-item-2", + "component": { + "Text": { + "text": { + "literalString": "Item 2" + } + } + } + }, + { + "id": "list-item-3", + "component": { + "Text": { + "text": { + "literalString": "Item 3" + } + } + } + } + ] + } + }, + { + "beginRendering": { + "surfaceId": "demo-audio", + "root": "demo-audio-root" + } + }, + { + "surfaceUpdate": { + "surfaceId": "demo-audio", + "components": [ + { + "id": "demo-audio-root", + "component": { + "AudioPlayer": { + "url": { + "literalString": "http://localhost:10005/assets/audio.mp3" + }, + "description": { + "literalString": "Local Audio Sample" + } + } + } + } + ] + } + }, + { + "dataModelUpdate": { + "surfaceId": "demo-audio", + "contents": [ + { + "key": "galleryData", + "valueMap": [ + { + "key": "textField", + "valueString": "Hello World" + }, + { + "key": "checkbox", + "valueBoolean": false + }, + { + "key": "checkboxChecked", + "valueBoolean": true + }, + { + "key": "slider", + "valueNumber": 30 + }, + { + "key": "date", + "valueString": "2025-10-26" + }, + { + "key": "favorites", + "valueMap": [ + { + "key": "0", + "valueString": "A" + } + ] + }, + { + "key": "favoritesChips", + "valueMap": [] + }, + { + "key": "favoritesFilter", + "valueMap": [] + } + ] + } + ] + } + }, + { + "beginRendering": { + "surfaceId": "response-surface", + "root": "response-text" + } + }, + { + "surfaceUpdate": { + "surfaceId": "response-surface", + "components": [ + { + "id": "response-text", + "component": { + "Text": { + "text": { + "literalString": "Interact with the gallery to see responses. This view is updated by the agent by relaying the raw action commands it received from the client" + } + } + } + } + ] + } + } +] diff --git a/tools/composer/src/data/dojo/restaurant-finder.json b/tools/composer/src/data/dojo/restaurant-finder.json new file mode 100644 index 000000000..f817158f4 --- /dev/null +++ b/tools/composer/src/data/dojo/restaurant-finder.json @@ -0,0 +1,351 @@ +[ + { + "beginRendering": { + "surfaceId": "default", + "root": "root-column", + "styles": { + "primaryColor": "#FF0000", + "font": "Roboto" + } + } + }, + { + "surfaceUpdate": { + "surfaceId": "default", + "components": [ + { + "id": "root-column", + "component": { + "Column": { + "children": { + "explicitList": [ + "title-heading", + "item-list" + ] + } + } + } + }, + { + "id": "title-heading", + "component": { + "Text": { + "usageHint": "h1", + "text": { + "path": "/title" + } + } + } + }, + { + "id": "item-list", + "component": { + "List": { + "direction": "vertical", + "children": { + "template": { + "componentId": "item-card-template", + "dataBinding": "/items" + } + } + } + } + }, + { + "id": "item-card-template", + "component": { + "Card": { + "child": "card-layout" + } + } + }, + { + "id": "card-layout", + "component": { + "Row": { + "children": { + "explicitList": [ + "template-image", + "card-details" + ] + } + } + } + }, + { + "id": "template-image", + "weight": 1, + "component": { + "Image": { + "url": { + "path": "/imageUrl" + } + } + } + }, + { + "id": "card-details", + "weight": 2, + "component": { + "Column": { + "children": { + "explicitList": [ + "template-name", + "template-rating", + "template-detail", + "template-link", + "template-book-button" + ] + } + } + } + }, + { + "id": "template-name", + "component": { + "Text": { + "usageHint": "h3", + "text": { + "path": "/name" + } + } + } + }, + { + "id": "template-rating", + "component": { + "Text": { + "text": { + "path": "/rating" + } + } + } + }, + { + "id": "template-detail", + "component": { + "Text": { + "text": { + "path": "/detail" + } + } + } + }, + { + "id": "template-link", + "component": { + "Text": { + "text": { + "path": "/infoLink" + } + } + } + }, + { + "id": "template-book-button", + "component": { + "Button": { + "child": "book-now-text", + "primary": true, + "action": { + "name": "book_restaurant", + "context": [ + { + "key": "restaurantName", + "value": { + "path": "/name" + } + }, + { + "key": "imageUrl", + "value": { + "path": "/imageUrl" + } + }, + { + "key": "address", + "value": { + "path": "/address" + } + } + ] + } + } + } + }, + { + "id": "book-now-text", + "component": { + "Text": { + "text": { + "literalString": "Book Now" + } + } + } + } + ] + } + }, + { + "dataModelUpdate": { + "surfaceId": "default", + "path": "/", + "contents": [ + { + "key": "title", + "valueString": "Szechuan Restaurants in New York" + }, + { + "key": "items", + "valueMap": [ + { + "key": "item1", + "valueMap": [ + { + "key": "name", + "valueString": "Xi'an Famous Foods" + }, + { + "key": "rating", + "valueString": "\u2605\u2605\u2605\u2605\u2606" + }, + { + "key": "detail", + "valueString": "Spicy and savory hand-pulled noodles." + }, + { + "key": "infoLink", + "valueString": "[More Info](https://www.xianfoods.com/)" + }, + { + "key": "imageUrl", + "valueString": "http://localhost:10007/static/shrimpchowmein.jpeg" + }, + { + "key": "address", + "valueString": "81 St Marks Pl, New York, NY 10003" + } + ] + }, + { + "key": "item2", + "valueMap": [ + { + "key": "name", + "valueString": "Han Dynasty" + }, + { + "key": "rating", + "valueString": "\u2605\u2605\u2605\u2605\u2606" + }, + { + "key": "detail", + "valueString": "Authentic Szechuan cuisine." + }, + { + "key": "infoLink", + "valueString": "[More Info](https://www.handynasty.net/)" + }, + { + "key": "imageUrl", + "valueString": "http://localhost:10007/static/mapotofu.jpeg" + }, + { + "key": "address", + "valueString": "90 3rd Ave, New York, NY 10003" + } + ] + }, + { + "key": "item3", + "valueMap": [ + { + "key": "name", + "valueString": "RedFarm" + }, + { + "key": "rating", + "valueString": "\u2605\u2605\u2605\u2605\u2606" + }, + { + "key": "detail", + "valueString": "Modern Chinese with a farm-to-table approach." + }, + { + "key": "infoLink", + "valueString": "[More Info](https://www.redfarmnyc.com/)" + }, + { + "key": "imageUrl", + "valueString": "http://localhost:10007/static/beefbroccoli.jpeg" + }, + { + "key": "address", + "valueString": "529 Hudson St, New York, NY 10014" + } + ] + }, + { + "key": "item4", + "valueMap": [ + { + "key": "name", + "valueString": "Mott 32" + }, + { + "key": "rating", + "valueString": "\u2605\u2605\u2605\u2605\u2605" + }, + { + "key": "detail", + "valueString": "Upscale Cantonese dining." + }, + { + "key": "infoLink", + "valueString": "[More Info](https://mott32.com/newyork/)" + }, + { + "key": "imageUrl", + "valueString": "http://localhost:10007/static/springrolls.jpeg" + }, + { + "key": "address", + "valueString": "111 W 57th St, New York, NY 10019" + } + ] + }, + { + "key": "item5", + "valueMap": [ + { + "key": "name", + "valueString": "Hwa Yuan Szechuan" + }, + { + "key": "rating", + "valueString": "\u2605\u2605\u2605\u2605\u2606" + }, + { + "key": "detail", + "valueString": "Famous for its cold noodles with sesame sauce." + }, + { + "key": "infoLink", + "valueString": "[More Info](https://hwayuannyc.com/)" + }, + { + "key": "imageUrl", + "valueString": "http://localhost:10007/static/kungpao.jpeg" + }, + { + "key": "address", + "valueString": "40 E Broadway, New York, NY 10002" + } + ] + } + ] + } + ] + } + } +] \ No newline at end of file From 6182ee7edd3b1832df9ca2e52c1be47867a6cc61 Mon Sep 17 00:00:00 2001 From: alan blount Date: Sat, 14 Mar 2026 18:58:03 +0000 Subject: [PATCH 06/46] feat(dojo): implement comprehensive visual design and layout polish for A2UI Dojo - Redesigned Top Command Center with glassmorphic header and functional timeline scrubber. - Replaced native scenario select with Shadcn DropdownMenu. - Polished Data Stream view with active state highlighting, glow effects, and auto-scrolling. - Replaced native checkboxes with custom Tailwind styled toggles in Config view. - Added dynamic grid layout for the Renderers Panel with sophisticated styling per surface (React Web, Discord dark mode replica, Lit Components). - Applied custom slim scrollbars throughout for a premium feel. --- tools/composer/src/app/dojo/page.tsx | 493 +++++++++++++++++++-------- 1 file changed, 359 insertions(+), 134 deletions(-) diff --git a/tools/composer/src/app/dojo/page.tsx b/tools/composer/src/app/dojo/page.tsx index 0949efefd..f85d8e87d 100644 --- a/tools/composer/src/app/dojo/page.tsx +++ b/tools/composer/src/app/dojo/page.tsx @@ -1,11 +1,21 @@ 'use client'; -import React, { useState } from 'react'; -import { Play, Pause, SkipBack, FastForward, Settings, FileJson, LayoutTemplate } from 'lucide-react'; +import React, { useState, useEffect, useRef } from 'react'; +import { + Play, Pause, SkipBack, Settings, FileJson, + ChevronDown, Activity, Code2, CheckSquare, + MessageSquare, Zap, LayoutTemplate +} from 'lucide-react'; import { useJsonlPlayer } from '@/components/dojo/useJsonlPlayer'; import { Button } from '@/components/ui/button'; import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from '@/components/ui/resizable'; import { Separator } from '@/components/ui/separator'; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; import { scenarios, ScenarioId } from '@/data/dojo'; export default function DojoPage() { @@ -30,91 +40,127 @@ export default function DojoPage() { baseIntervalMs: 1000 }); - const toggleTab = () => setActiveTab((prev) => (prev === 'data' ? 'config' : 'data')); + // Auto-scroll logic for the JSONL pane + const streamEndRef = useRef(null); + useEffect(() => { + if (playbackState === 'playing' && streamEndRef.current) { + streamEndRef.current.scrollIntoView({ behavior: 'smooth', block: 'end' }); + } + }, [progress, playbackState]); return ( -
+
{/* Top Header / Command Center */} -
- {/* Toggle Data / Config */} -
+
+ + {/* Left Section: Tab Toggle */} +
- - - {/* Playback Scrubber & Controls */} -
-
- {playbackState === 'playing' ? ( - ) : ( - )}
{/* Timeline Scrubber */} -
- {progress + 1} - seek(parseInt(e.target.value, 10))} - className="flex-1 h-2 bg-secondary rounded-lg appearance-none cursor-pointer" - /> - {totalMessages} +
+ {progress + 1} +
+ seek(parseInt(e.target.value, 10))} + className="absolute inset-0 w-full opacity-0 cursor-pointer z-10" + /> + {/* Custom Range Track */} +
+
1 ? (progress / (totalMessages - 1)) * 100 : 0}%` }} + /> +
+ {/* Custom Range Thumb */} +
1 ? (progress / (totalMessages - 1)) * 100 : 0}% - 6px)` + }} + /> +
+ {totalMessages}
{/* Speed Toggle */}
- - - {/* Scenario Selection Placeholder */} -
- + {/* Right Section: Scenario Selection */} +
+ + + + + + {Object.keys(scenarios).map(id => ( + { setSelectedScenario(id as ScenarioId); stop(); }} + className={`text-sm py-2 cursor-pointer ${selectedScenario === id ? 'bg-primary/10 text-primary font-medium' : ''}`} + > + {id} + + ))} + +
@@ -122,112 +168,291 @@ export default function DojoPage() { {/* Left Pane: JSONL Source or Configuration */} - -
-

- {activeTab === 'data' ? 'JSONL Stream (SSE)' : 'Renderer Configuration'} -

- - {activeTab === 'data' ? ( -
- {(scenarios[selectedScenario] as any).map((msg: any, index: number) => ( -
-
{JSON.stringify(msg, null, 2)}
-
- ))} + +
+
+ +
+

+ {activeTab === 'data' ? ( + <> Event Stream + ) : ( + <> Workspace Config + )} +

- ) : ( -
- {/* Mock Config Toggles */} -
-

Active Surfaces

- - - + + {activeTab === 'data' ? ( +
+ {(scenarios[selectedScenario] as any)?.map((msg: any, index: number) => { + const isActive = index === progress; + const isPast = index < progress; + + return ( +
+ {isActive && ( +
+ )} +
+                          {JSON.stringify(msg, null, 2)}
+                        
+
+ ); + })} +
+ ) : ( +
+ {/* Surfaces Config */} +
+

+ Active Surfaces +

+ +
+ {[ + { id: 'react', label: 'React Adapter (Web)', icon: Code2 }, + { id: 'discord', label: 'Discord Mock', icon: MessageSquare }, + { id: 'lit', label: 'Lit Components', icon: CheckSquare } + ].map(surface => ( + + ))} +
+
-
-

Transport Configuration

- + {/* Transport Config */} +
+

+ Transport layer +

+
+ + +
+

+ Determines how the mock client receives events from the mock server. Currently simulated in-memory. +

+
-
- )} + )} +
- + {/* Right Pane: Active Renderers */} - -
-
- - {renderers.react && ( -
-
- React Renderer -
-
- {/* Placeholder for real React A2UI Renderer component */} -
-

{''}

-

Currently displaying {activeMessages.length} elements

+ +
+
+
+ + {renderers.react && ( +
+
+
+
+
+
+
+ + React Web + +
{/* Balancer */} +
+
+
+
+ +
+
+

{''}

+

Rendering components via React Adapter

+
+
+ State elements: + {activeMessages.length} +
+ {activeMessages.length > 0 && ( +
+
+
+ )} +
-
- )} + )} + + {renderers.discord && ( +
+
+
+ +
+ + # a2ui-demo + +
{/* Balancer */} +
+
+ + {/* Mock Discord UI - Start of conversation */} +
+
+ U +
+
+
+ User + Today at 4:20 PM +
+

Run the {selectedScenario} scenario.

+
+
+ + - {renderers.discord && ( -
-
- Discord Renderer (Adapter) + {/* Bot Response Mock */} +
+
+ +
+
+
+ Agent Bot + Bot + Today at 4:20 PM +
+ + {/* Discord Embed Mock representing active state */} +
+
+ A2UI Surface Rendered +
+

Simulated embed updating from event stream.

+ +
+
+ Total events received: + {activeMessages.length} +
+
+ Status: + + {playbackState === 'playing' ? 'Receiving...' : 'Idle'} + +
+
+
+ + {/* Mock Discord Buttons */} + {activeMessages.length > 0 && ( +
+ + +
+ )} +
+
+ +
-
- {/* Mock Discord UI */} -
-
-
-
- Agent - Today at 4:20 PM + )} + + {renderers.lit && ( +
+
+
+
+
+ + Web Components + +
+
+
+
+
+
+ +
+
+

{''}

+

Framework-agnostic rendering

-
-

Simulated Discord embed updating via {activeMessages.length} states.

+
+
+ {activeMessages.length} states synced
-
- )} - + )} + +
- + + {/* Global styles for custom scrollbars */} +