From 7d6be9fbd6ecf958016e753c0b4e815628065d18 Mon Sep 17 00:00:00 2001 From: Zaf Date: Wed, 4 Mar 2026 06:50:05 +0000 Subject: [PATCH 01/22] =?UTF-8?q?feat:=20add=20A2H=C3=97A2UI=20Prototype?= =?UTF-8?q?=201=20=E2=80=94=20Approval=20Card=20(AUTHORIZE=20intent)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Standalone A2UI v0.9 prototype demonstrating the AUTHORIZE intent from the A2H protocol mapped to A2UI surfaces. - authorize-transfer.json: v0.9 message sequence (createSurface + updateComponents + updateDataModel) for a 00 transfer approval - index.html: Zero-dependency renderer demo with event logging - README.md: Pattern docs, gaps found (no v0.9 Lit renderer, no TTL component, no post-click state transitions) Part of the A2H→A2UI intent mapping exploration. --- samples/a2h-prototypes/p1-approval/README.md | 80 +++++++ .../p1-approval/authorize-transfer.json | 223 ++++++++++++++++++ samples/a2h-prototypes/p1-approval/index.html | 217 +++++++++++++++++ 3 files changed, 520 insertions(+) create mode 100644 samples/a2h-prototypes/p1-approval/README.md create mode 100644 samples/a2h-prototypes/p1-approval/authorize-transfer.json create mode 100644 samples/a2h-prototypes/p1-approval/index.html diff --git a/samples/a2h-prototypes/p1-approval/README.md b/samples/a2h-prototypes/p1-approval/README.md new file mode 100644 index 000000000..79e84b1d6 --- /dev/null +++ b/samples/a2h-prototypes/p1-approval/README.md @@ -0,0 +1,80 @@ +# Prototype 1 — Approval Card (AUTHORIZE Intent) + +## A2H Intent: AUTHORIZE + +Maps to the **AUTHORIZE** intent from the [A2H protocol](https://github.com/anthropics/anthropic-cookbook/blob/main/misc/prompt_caching.ipynb) (Twilio's Agent-to-Human specification). This intent represents an agent requesting human approval before performing a sensitive or irreversible action. + +## Scenario + +A financial assistant agent wants to transfer $500 from a checking account to a savings account. The human must explicitly approve or reject the action before it proceeds. + +## Files + +| File | Purpose | +|------|---------| +| `authorize-transfer.json` | A2UI v0.9 message sequence (createSurface + updateComponents + updateDataModel) | +| `index.html` | Standalone renderer demo — no build step, opens in any browser | + +## Running + +```bash +# Any static file server works +cd samples/a2h-prototypes/p1-approval +python3 -m http.server 8080 +# Open http://localhost:8080 +``` + +## A2UI v0.9 Patterns Used + +- **`createSurface` with `sendDataModel: true`** — ensures the full data model is sent back with every button event, so the agent receives all transfer metadata alongside the approve/reject decision +- **Data binding via `{"path": "/transfer/amount"}`** — transfer details are in the data model, not hardcoded in component text +- **Event actions on buttons** — `a2h.authorize.approve` and `a2h.authorize.reject` event names with context carrying `interactionId` (bound from data model) +- **Nested Card** — detail rows in a nested card for visual grouping +- **TTL indicator** — caption text showing expiration (static in this prototype) + +## What Works + +- ✅ Clean visual representation of an approval flow +- ✅ Data model correctly separates transfer data from presentation +- ✅ Button events carry resolved context (interactionId from data model) +- ✅ `sendDataModel: true` means the agent gets full context on any interaction +- ✅ Standard v0.9 components only — no custom catalog needed + +## What's Missing / Gaps Found + +1. **No Lit renderer for v0.9** — The `@a2ui/lit` renderer only supports v0.8. The `@a2ui/web_core` has v0.9 state/data-model code but no rendering layer. This prototype uses a standalone vanilla JS renderer instead. +2. **No TTL/countdown component** — The expiration is a static text string. A real AUTHORIZE flow needs a live countdown or at minimum a `validUntil` field that the renderer can auto-expire. +3. **No button disable after click** — Once approved/rejected, buttons should be disabled and the card should show a "Decision recorded" state. A2UI v0.9 has no built-in mechanism for client-side state transitions without a server round-trip. +4. **No severity/risk theming** — The AUTHORIZE intent should convey risk level (low/medium/high). The v0.9 theme supports `primaryColor` but there's no standard convention for mapping risk → color. +5. **No auth evidence** — A2H AUTHORIZE can carry authentication proof (biometric, PIN). A2UI has no component for this — it would need a custom catalog extension. +6. **Button variant limited** — Only "primary" variant exists. A "destructive" or "danger" variant would be useful for high-risk authorizations. + +## Event Payload Example + +When the user clicks **Approve**, the event payload sent to the agent: + +```json +{ + "eventName": "a2h.authorize.approve", + "context": { + "interactionId": "txn-2026-0304-001", + "intent": "AUTHORIZE" + }, + "dataModel": { + "transfer": { + "description": "Your financial agent wants to transfer funds between your accounts.", + "action": "Transfer Funds", + "fromAccount": "Checking (****4521)", + "toAccount": "Savings (****7890)", + "amount": "$500.00" + }, + "meta": { + "interactionId": "txn-2026-0304-001", + "agentId": "financial-assistant-v2", + "intent": "AUTHORIZE", + "timestamp": "2026-03-04T06:45:00Z", + "ttlSeconds": 300 + } + } +} +``` diff --git a/samples/a2h-prototypes/p1-approval/authorize-transfer.json b/samples/a2h-prototypes/p1-approval/authorize-transfer.json new file mode 100644 index 000000000..9b28f6d99 --- /dev/null +++ b/samples/a2h-prototypes/p1-approval/authorize-transfer.json @@ -0,0 +1,223 @@ +[ + { + "version": "v0.9", + "createSurface": { + "surfaceId": "a2h-authorize-transfer-001", + "catalogId": "https://a2ui.org/specification/v0_9/basic_catalog.json", + "sendDataModel": true + } + }, + { + "version": "v0.9", + "updateComponents": { + "surfaceId": "a2h-authorize-transfer-001", + "components": [ + { + "id": "root", + "component": "Card", + "child": "main-column" + }, + { + "id": "main-column", + "component": "Column", + "children": [ + "header-row", + "divider-1", + "description", + "details-card", + "ttl-text", + "divider-2", + "actions" + ] + }, + { + "id": "header-row", + "component": "Row", + "children": ["header-icon", "header-text"], + "align": "center" + }, + { + "id": "header-icon", + "component": "Icon", + "name": "lock" + }, + { + "id": "header-text", + "component": "Text", + "text": "Authorization Required", + "variant": "h3" + }, + { + "id": "divider-1", + "component": "Divider" + }, + { + "id": "description", + "component": "Text", + "text": { "path": "/transfer/description" }, + "variant": "body" + }, + { + "id": "details-card", + "component": "Card", + "child": "details-column" + }, + { + "id": "details-column", + "component": "Column", + "children": [ + "detail-action-row", + "detail-from-row", + "detail-to-row", + "detail-amount-row" + ] + }, + { + "id": "detail-action-row", + "component": "Row", + "children": ["detail-action-label", "detail-action-value"] + }, + { + "id": "detail-action-label", + "component": "Text", + "text": "Action:", + "variant": "label" + }, + { + "id": "detail-action-value", + "component": "Text", + "text": { "path": "/transfer/action" }, + "variant": "body" + }, + { + "id": "detail-from-row", + "component": "Row", + "children": ["detail-from-label", "detail-from-value"] + }, + { + "id": "detail-from-label", + "component": "Text", + "text": "From:", + "variant": "label" + }, + { + "id": "detail-from-value", + "component": "Text", + "text": { "path": "/transfer/fromAccount" }, + "variant": "body" + }, + { + "id": "detail-to-row", + "component": "Row", + "children": ["detail-to-label", "detail-to-value"] + }, + { + "id": "detail-to-label", + "component": "Text", + "text": "To:", + "variant": "label" + }, + { + "id": "detail-to-value", + "component": "Text", + "text": { "path": "/transfer/toAccount" }, + "variant": "body" + }, + { + "id": "detail-amount-row", + "component": "Row", + "children": ["detail-amount-label", "detail-amount-value"] + }, + { + "id": "detail-amount-label", + "component": "Text", + "text": "Amount:", + "variant": "label" + }, + { + "id": "detail-amount-value", + "component": "Text", + "text": { "path": "/transfer/amount" }, + "variant": "body" + }, + { + "id": "ttl-text", + "component": "Text", + "text": "⏱ Expires in 5 minutes", + "variant": "caption" + }, + { + "id": "divider-2", + "component": "Divider" + }, + { + "id": "actions", + "component": "Row", + "children": ["reject-btn", "approve-btn"], + "justify": "end" + }, + { + "id": "reject-btn-text", + "component": "Text", + "text": "Reject" + }, + { + "id": "reject-btn", + "component": "Button", + "child": "reject-btn-text", + "action": { + "event": { + "name": "a2h.authorize.reject", + "context": { + "interactionId": { "path": "/meta/interactionId" }, + "intent": "AUTHORIZE" + } + } + } + }, + { + "id": "approve-btn-text", + "component": "Text", + "text": "Approve" + }, + { + "id": "approve-btn", + "component": "Button", + "child": "approve-btn-text", + "variant": "primary", + "action": { + "event": { + "name": "a2h.authorize.approve", + "context": { + "interactionId": { "path": "/meta/interactionId" }, + "intent": "AUTHORIZE" + } + } + } + } + ] + } + }, + { + "version": "v0.9", + "updateDataModel": { + "surfaceId": "a2h-authorize-transfer-001", + "value": { + "transfer": { + "description": "Your financial agent wants to transfer funds between your accounts.", + "action": "Transfer Funds", + "fromAccount": "Checking (****4521)", + "toAccount": "Savings (****7890)", + "amount": "$500.00" + }, + "meta": { + "interactionId": "txn-2026-0304-001", + "agentId": "financial-assistant-v2", + "intent": "AUTHORIZE", + "timestamp": "2026-03-04T06:45:00Z", + "ttlSeconds": 300 + } + } + } + } +] diff --git a/samples/a2h-prototypes/p1-approval/index.html b/samples/a2h-prototypes/p1-approval/index.html new file mode 100644 index 000000000..8eeaaab75 --- /dev/null +++ b/samples/a2h-prototypes/p1-approval/index.html @@ -0,0 +1,217 @@ + + + + + + A2H Prototype 1 — Approval Card (AUTHORIZE) + + + + + + + +

A2H × A2UI — Prototype 1: Approval Card (AUTHORIZE)

+
+

Event Log

+
+ + + + From e16ba5115f30a4a95d0397c447c2bac6aa28cc4d Mon Sep 17 00:00:00 2001 From: Zaf Date: Wed, 4 Mar 2026 06:51:51 +0000 Subject: [PATCH 02/22] =?UTF-8?q?feat(samples):=20add=20A2H=20prototype=20?= =?UTF-8?q?2=20=E2=80=94=20escalation/handoff=20(ESCALATE)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - escalation-handoff.json: v0.9 message sequence with context preservation, priority indicator, and three connection method buttons - index.html: standalone vanilla JS renderer - README.md: pattern docs, A2H mapping, gaps analysis --- .../a2h-prototypes/p2-escalation/README.md | 88 ++++++ .../p2-escalation/escalation-handoff.json | 290 ++++++++++++++++++ .../a2h-prototypes/p2-escalation/index.html | 201 ++++++++++++ 3 files changed, 579 insertions(+) create mode 100644 samples/a2h-prototypes/p2-escalation/README.md create mode 100644 samples/a2h-prototypes/p2-escalation/escalation-handoff.json create mode 100644 samples/a2h-prototypes/p2-escalation/index.html diff --git a/samples/a2h-prototypes/p2-escalation/README.md b/samples/a2h-prototypes/p2-escalation/README.md new file mode 100644 index 000000000..4b4cf72ff --- /dev/null +++ b/samples/a2h-prototypes/p2-escalation/README.md @@ -0,0 +1,88 @@ +# A2H × A2UI — Prototype 2: Escalation/Handoff (ESCALATE) + +## Overview + +Demonstrates the **ESCALATE** intent from the A2H protocol mapped to an A2UI v0.9 surface. An AI customer service agent has exhausted its capabilities resolving a billing dispute and hands off to a human agent, preserving full conversation context. + +## Files + +| File | Purpose | +|------|---------| +| `escalation-handoff.json` | A2UI v0.9 message sequence (createSurface → updateComponents → updateDataModel) | +| `index.html` | Standalone vanilla JS renderer — open directly or via local server | + +## Running + +```bash +# From this directory +python3 -m http.server 8080 +# Open http://localhost:8080 +``` + +## A2H → A2UI Mapping + +| A2H Concept | A2UI Realization | +|---|---| +| ESCALATE intent | `createSurface` with `sendDataModel: true` | +| Escalation reason | `Text` bound to `/escalation/reason` | +| Context preservation | Nested `Card` with attempted actions, issue category, duration | +| Priority indicator | `Icon` (priority_high) + `Text` with caption variant (red) | +| Connection options | Three `Button` components firing `a2h.escalate.connect` with `method` context | +| Status updates | `Text` bound to `/escalation/statusMessage` — server can push `updateDataModel` | +| Agent metadata | Stored in `/meta` — agentId, department, queuePosition | + +## Event Flow + +1. **AI agent** determines it cannot resolve the issue +2. **Server** sends `createSurface` + `updateComponents` + `updateDataModel` +3. **User** sees escalation card with context summary and connection options +4. **User clicks** "Connect via Chat" / "Request Callback" / "Schedule Appointment" +5. **Event fired:** `a2h.escalate.connect` with `{ method: "chat"|"callback"|"appointment" }` +6. **Server** processes and pushes `updateDataModel` to update status ("Connecting...", "Agent Sarah joined") +7. **Server** sends `deleteSurface` when handoff complete + +## Data Model + +```json +{ + "escalation": { + "reason": "...", + "attemptedActions": "...", + "issueCategory": "...", + "conversationDuration": "...", + "priority": "high", + "priorityLabel": "⚡ High Priority — ...", + "statusMessage": "...", + "conversationContext": [ + { "role": "user", "summary": "..." }, + { "role": "agent", "summary": "..." } + ] + }, + "meta": { + "interactionId": "...", + "agentId": "...", + "agentName": "...", + "intent": "ESCALATE", + "timestamp": "...", + "department": "...", + "queuePosition": 3 + } +} +``` + +## Gaps & Missing Capabilities + +| Gap | Impact | Workaround | +|-----|--------|------------| +| **No conditional rendering** | Can't show/hide components based on state (e.g., hide buttons after selection) | Server sends `updateComponents` to swap the tree | +| **No progress/spinner** | Can't show animated "connecting..." indicator | Use text + emoji (⏳) via `updateDataModel` | +| **No timer component** | Can't show live countdown for queue position | Server pushes periodic `updateDataModel` updates | +| **conversationContext array** | Stored in data model but no ChildList template to render it dynamically | Rendered as summary text instead | +| **No cancel button** | Proposal had cancel; omitted here in favor of connection options | Could add as fourth button | + +## Design Decisions + +- **Three connection methods** as equal-weight buttons rather than a dropdown — mobile-friendly, reduces taps +- **Context preservation** in a nested card — visually grouped, clearly "what the AI tried" +- **Single event name** (`a2h.escalate.connect`) with `method` in context — cleaner than three separate events +- **Priority as caption variant** — inherits red color from CSS, visually prominent without a separate component diff --git a/samples/a2h-prototypes/p2-escalation/escalation-handoff.json b/samples/a2h-prototypes/p2-escalation/escalation-handoff.json new file mode 100644 index 000000000..6dd8af93c --- /dev/null +++ b/samples/a2h-prototypes/p2-escalation/escalation-handoff.json @@ -0,0 +1,290 @@ +[ + { + "version": "v0.9", + "createSurface": { + "surfaceId": "a2h-escalate-001", + "catalogId": "https://a2ui.org/specification/v0_9/basic_catalog.json", + "sendDataModel": true + } + }, + { + "version": "v0.9", + "updateComponents": { + "surfaceId": "a2h-escalate-001", + "components": [ + { + "id": "root", + "component": "Card", + "child": "main-col" + }, + { + "id": "main-col", + "component": "Column", + "children": [ + "header-row", + "divider-1", + "reason-text", + "context-card", + "priority-row", + "status-text", + "divider-2", + "actions-col" + ] + }, + { + "id": "header-row", + "component": "Row", + "children": ["header-icon", "header-text"], + "align": "center" + }, + { + "id": "header-icon", + "component": "Icon", + "name": "support_agent" + }, + { + "id": "header-text", + "component": "Text", + "text": "Escalation to Human Agent", + "variant": "h3" + }, + { + "id": "divider-1", + "component": "Divider" + }, + { + "id": "reason-text", + "component": "Text", + "text": { "path": "/escalation/reason" }, + "variant": "body" + }, + { + "id": "context-card", + "component": "Card", + "child": "context-col" + }, + { + "id": "context-col", + "component": "Column", + "children": [ + "context-header", + "context-tried-row", + "context-issue-row", + "context-duration-row", + "context-agent-row" + ] + }, + { + "id": "context-header", + "component": "Text", + "text": "Conversation Summary", + "variant": "label" + }, + { + "id": "context-tried-row", + "component": "Row", + "children": ["context-tried-label", "context-tried-value"] + }, + { + "id": "context-tried-label", + "component": "Text", + "text": "Attempted:", + "variant": "label" + }, + { + "id": "context-tried-value", + "component": "Text", + "text": { "path": "/escalation/attemptedActions" }, + "variant": "body" + }, + { + "id": "context-issue-row", + "component": "Row", + "children": ["context-issue-label", "context-issue-value"] + }, + { + "id": "context-issue-label", + "component": "Text", + "text": "Issue:", + "variant": "label" + }, + { + "id": "context-issue-value", + "component": "Text", + "text": { "path": "/escalation/issueCategory" }, + "variant": "body" + }, + { + "id": "context-duration-row", + "component": "Row", + "children": ["context-duration-label", "context-duration-value"] + }, + { + "id": "context-duration-label", + "component": "Text", + "text": "Duration:", + "variant": "label" + }, + { + "id": "context-duration-value", + "component": "Text", + "text": { "path": "/escalation/conversationDuration" }, + "variant": "body" + }, + { + "id": "context-agent-row", + "component": "Row", + "children": ["context-agent-label", "context-agent-value"] + }, + { + "id": "context-agent-label", + "component": "Text", + "text": "AI Agent:", + "variant": "label" + }, + { + "id": "context-agent-value", + "component": "Text", + "text": { "path": "/meta/agentName" }, + "variant": "body" + }, + { + "id": "priority-row", + "component": "Row", + "children": ["priority-icon", "priority-text"], + "align": "center" + }, + { + "id": "priority-icon", + "component": "Icon", + "name": "priority_high" + }, + { + "id": "priority-text", + "component": "Text", + "text": { "path": "/escalation/priorityLabel" }, + "variant": "caption" + }, + { + "id": "status-text", + "component": "Text", + "text": { "path": "/escalation/statusMessage" }, + "variant": "body" + }, + { + "id": "divider-2", + "component": "Divider" + }, + { + "id": "actions-col", + "component": "Column", + "children": ["actions-label", "actions-row"] + }, + { + "id": "actions-label", + "component": "Text", + "text": "How would you like to connect?", + "variant": "body" + }, + { + "id": "actions-row", + "component": "Row", + "children": ["chat-btn", "callback-btn", "schedule-btn"], + "justify": "center" + }, + { + "id": "chat-btn-text", + "component": "Text", + "text": "Connect via Chat" + }, + { + "id": "chat-btn", + "component": "Button", + "child": "chat-btn-text", + "variant": "primary", + "action": { + "event": { + "name": "a2h.escalate.connect", + "context": { + "interactionId": { "path": "/meta/interactionId" }, + "method": "chat", + "intent": "ESCALATE" + } + } + } + }, + { + "id": "callback-btn-text", + "component": "Text", + "text": "Request Callback" + }, + { + "id": "callback-btn", + "component": "Button", + "child": "callback-btn-text", + "action": { + "event": { + "name": "a2h.escalate.connect", + "context": { + "interactionId": { "path": "/meta/interactionId" }, + "method": "callback", + "intent": "ESCALATE" + } + } + } + }, + { + "id": "schedule-btn-text", + "component": "Text", + "text": "Schedule Appointment" + }, + { + "id": "schedule-btn", + "component": "Button", + "child": "schedule-btn-text", + "action": { + "event": { + "name": "a2h.escalate.connect", + "context": { + "interactionId": { "path": "/meta/interactionId" }, + "method": "appointment", + "intent": "ESCALATE" + } + } + } + } + ] + } + }, + { + "version": "v0.9", + "updateDataModel": { + "surfaceId": "a2h-escalate-001", + "value": { + "escalation": { + "reason": "I wasn't able to resolve your billing dispute. The charge of $89.99 on Feb 28 requires manual review by our billing team. I'm connecting you with a specialist who can help.", + "attemptedActions": "Checked transaction history, verified charge details, attempted automated refund (declined — requires manual review)", + "issueCategory": "Billing Dispute — Unauthorized Charge", + "conversationDuration": "8 minutes (12 messages)", + "priority": "high", + "priorityLabel": "⚡ High Priority — Billing dispute, potential unauthorized charge", + "statusMessage": "Estimated wait: ~2 minutes for chat, ~15 minutes for callback", + "conversationContext": [ + { "role": "user", "summary": "Reported unexpected $89.99 charge from 'DGTL-SVC' on Feb 28" }, + { "role": "agent", "summary": "Identified charge, attempted automated dispute — system requires human review" }, + { "role": "user", "summary": "Confirmed they did not authorize the charge" } + ] + }, + "meta": { + "interactionId": "esc-2026-0304-042", + "agentId": "customer-support-v3", + "agentName": "Support Assistant (AI)", + "intent": "ESCALATE", + "timestamp": "2026-03-04T06:48:00Z", + "department": "billing", + "queuePosition": 3 + } + } + } + } +] diff --git a/samples/a2h-prototypes/p2-escalation/index.html b/samples/a2h-prototypes/p2-escalation/index.html new file mode 100644 index 000000000..a6bb91627 --- /dev/null +++ b/samples/a2h-prototypes/p2-escalation/index.html @@ -0,0 +1,201 @@ + + + + + + A2H Prototype 2 — Escalation/Handoff (ESCALATE) + + + + + + + +

A2H × A2UI — Prototype 2: Escalation/Handoff (ESCALATE)

+
+

Event Log

+
+ + + + From 1be83fb5a74d8269903755c6181014e2875afabc Mon Sep 17 00:00:00 2001 From: Zaf Date: Wed, 4 Mar 2026 07:43:43 +0000 Subject: [PATCH 03/22] P3: Guided input/form collection (COLLECT) prototype - collect-shipping.json: A2UI v0.9 form with TextField data binding, MultipleChoice for delivery speed, sendDataModel on submit - index.html: Standalone renderer with two-way data binding - README.md: Pattern docs, COLLECT mapping, gaps analysis --- .../a2h-prototypes/p3-guided-input/README.md | 90 ++++++ .../p3-guided-input/collect-shipping.json | 165 +++++++++++ .../a2h-prototypes/p3-guided-input/index.html | 279 ++++++++++++++++++ 3 files changed, 534 insertions(+) create mode 100644 samples/a2h-prototypes/p3-guided-input/README.md create mode 100644 samples/a2h-prototypes/p3-guided-input/collect-shipping.json create mode 100644 samples/a2h-prototypes/p3-guided-input/index.html diff --git a/samples/a2h-prototypes/p3-guided-input/README.md b/samples/a2h-prototypes/p3-guided-input/README.md new file mode 100644 index 000000000..f08e133fb --- /dev/null +++ b/samples/a2h-prototypes/p3-guided-input/README.md @@ -0,0 +1,90 @@ +# Prototype 3 — Guided Input / Form Collection (COLLECT) + +## A2H Intent: COLLECT + +The agent needs structured data from the user — in this case, shipping details for an order. Rather than asking free-text questions one at a time, the agent sends a guided form surface that feels conversational but captures structured, validated data. + +## What's in this prototype + +| File | Purpose | +|------|---------| +| `collect-shipping.json` | A2UI v0.9 message sequence defining the form surface | +| `index.html` | Standalone vanilla JS renderer (no build step) | + +### To run + +```bash +# Any static file server works +cd samples/a2h-prototypes/p3-guided-input +python3 -m http.server 8080 +# Open http://localhost:8080 +``` + +## How COLLECT maps to A2UI v0.9 + +The COLLECT intent translates to a surface with: + +1. **`createSurface`** with `sendDataModel: true` — tells the host that when an event fires, the entire data model should be included in the payload sent back to the agent. +2. **`TextField` components** with `value: {"path": "/shipping/name"}` — each field reads from and writes to a path in the data model. This is two-way binding: pre-populated values show up, user edits flow back. +3. **`MultipleChoice`** for constrained selections (delivery speed) — also data-bound. +4. **`Button`** with an event action — on click, the renderer collects the current data model and sends it as the event payload. +5. **`updateDataModel`** pre-populates known info (saved address from user profile). + +### Message sequence + +``` +createSurface → "I'm creating a form surface, send data model back on events" +updateComponents → "Here are the form fields, layout, and submit button" +updateDataModel → "Pre-fill with what we already know about the user" +``` + +## What `sendDataModel` enables + +When `sendDataModel: true` is set on the surface, every event fired from that surface includes the full data model snapshot. This means: + +- **The agent gets structured data** — not free text, not parsed intent, but a clean JSON object with typed fields at known paths. +- **Pre-population works naturally** — the agent sets known values, the user corrects what's wrong, the agent gets back the final state. +- **Multiple fields in one round-trip** — instead of 6 back-and-forth messages to collect name/address/city/state/zip/phone, one surface captures everything. + +Example payload the agent receives on submit: + +```json +{ + "eventName": "submitShipping", + "dataModel": { + "shipping": { + "name": "Jane Doe", + "street": "742 Evergreen Terrace", + "city": "Springfield", + "state": "IL", + "zip": "62704", + "phone": "217-555-0142", + "speed": "express" + } + } +} +``` + +## Gaps and future work + +### Not yet in v0.9 + +- **Required field markers** — no `required: true` prop on TextField; validation is agent-side only +- **Field validation** — no `pattern`, `minLength`, `maxLength`, `type: "email"` constraints in the component spec +- **Conditional fields** — can't show/hide fields based on other field values (e.g., show "apt/suite" only if street looks like an apartment) +- **Error states** — no way for the agent to send back "ZIP code is invalid" and highlight the specific field +- **Field grouping / sections** — no `Fieldset` or `FormSection` component for visual grouping +- **Auto-complete / suggestions** — no typeahead or suggestion list for address fields +- **Multi-step forms** — no built-in wizard/stepper; agent would need to send sequential surfaces + +### Workarounds in current spec + +- **Required fields:** Agent validates after receiving data model, sends a new surface with error text if needed +- **Conditional fields:** Agent sends `updateComponents` to add/remove fields dynamically +- **Error states:** Agent can update a Text component near the field to show red error text + +## Design notes + +The form is intentionally "guided" — it has a friendly header, an explanatory subtitle, and pre-populated values. This feels like a conversation ("here's what I know, correct anything that's wrong") rather than a blank form dump. + +The `MultipleChoice` component for delivery speed avoids free-text ambiguity — the user picks from defined options, the agent gets a clean enum value. diff --git a/samples/a2h-prototypes/p3-guided-input/collect-shipping.json b/samples/a2h-prototypes/p3-guided-input/collect-shipping.json new file mode 100644 index 000000000..d9de0fe45 --- /dev/null +++ b/samples/a2h-prototypes/p3-guided-input/collect-shipping.json @@ -0,0 +1,165 @@ +[ + { + "version": "v0.9", + "createSurface": { + "surfaceId": "collect-shipping", + "catalogId": "https://a2ui.org/specification/v0_9/basic_catalog.json", + "sendDataModel": true + } + }, + { + "version": "v0.9", + "updateComponents": { + "surfaceId": "collect-shipping", + "components": [ + { + "id": "root", + "component": "Card", + "child": "main-col" + }, + { + "id": "main-col", + "component": "Column", + "children": [ + "header-row", + "subtitle", + "divider-top", + "name-field", + "street-field", + "city-state-row", + "zip-field", + "phone-field", + "divider-mid", + "speed-label", + "speed-choice", + "divider-bot", + "submit-row" + ] + }, + { + "id": "header-row", + "component": "Row", + "children": ["header-icon", "header-text"], + "align": "center" + }, + { + "id": "header-icon", + "component": "Icon", + "name": "local_shipping" + }, + { + "id": "header-text", + "component": "Text", + "text": "Where should we ship your order?", + "variant": "h3" + }, + { + "id": "subtitle", + "component": "Text", + "text": "We'll confirm the address before charging your card.", + "variant": "body" + }, + { "id": "divider-top", "component": "Divider" }, + { + "id": "name-field", + "component": "TextField", + "label": "Full name", + "value": { "path": "/shipping/name" } + }, + { + "id": "street-field", + "component": "TextField", + "label": "Street address", + "value": { "path": "/shipping/street" } + }, + { + "id": "city-state-row", + "component": "Row", + "children": ["city-field", "state-field"] + }, + { + "id": "city-field", + "component": "TextField", + "label": "City", + "value": { "path": "/shipping/city" } + }, + { + "id": "state-field", + "component": "TextField", + "label": "State", + "value": { "path": "/shipping/state" } + }, + { + "id": "zip-field", + "component": "TextField", + "label": "ZIP code", + "value": { "path": "/shipping/zip" } + }, + { + "id": "phone-field", + "component": "TextField", + "label": "Phone number", + "value": { "path": "/shipping/phone" } + }, + { "id": "divider-mid", "component": "Divider" }, + { + "id": "speed-label", + "component": "Text", + "text": "Delivery speed", + "variant": "label" + }, + { + "id": "speed-choice", + "component": "MultipleChoice", + "value": { "path": "/shipping/speed" }, + "options": [ + { "label": "Standard (5–7 days)", "value": "standard" }, + { "label": "Express (2–3 days)", "value": "express" }, + { "label": "Overnight", "value": "overnight" } + ] + }, + { "id": "divider-bot", "component": "Divider" }, + { + "id": "submit-row", + "component": "Row", + "children": ["submit-btn"], + "justify": "end" + }, + { + "id": "submit-btn-text", + "component": "Text", + "text": "Submit shipping info" + }, + { + "id": "submit-btn", + "component": "Button", + "variant": "primary", + "child": "submit-btn-text", + "action": { + "event": { + "name": "submitShipping", + "context": {} + } + } + } + ] + } + }, + { + "version": "v0.9", + "updateDataModel": { + "surfaceId": "collect-shipping", + "value": { + "shipping": { + "name": "Jane Doe", + "street": "742 Evergreen Terrace", + "city": "Springfield", + "state": "IL", + "zip": "62704", + "phone": "", + "speed": "standard" + } + } + } + } +] diff --git a/samples/a2h-prototypes/p3-guided-input/index.html b/samples/a2h-prototypes/p3-guided-input/index.html new file mode 100644 index 000000000..ebb4726b4 --- /dev/null +++ b/samples/a2h-prototypes/p3-guided-input/index.html @@ -0,0 +1,279 @@ + + + + + + A2H Prototype 3 — Guided Form (COLLECT) + + + + + + + +

A2H × A2UI — Prototype 3: Guided Form (COLLECT)

+
+

Event Log (sendDataModel payload)

+
+ + + + From 85148f0a9acddbcddc3290771e124dd193928008 Mon Sep 17 00:00:00 2001 From: Zaf Date: Wed, 4 Mar 2026 07:45:48 +0000 Subject: [PATCH 04/22] =?UTF-8?q?feat:=20P4=20progress+intervention=20prot?= =?UTF-8?q?otype=20(INFORM=E2=86=92AUTHORIZE=20pipeline)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../p4-progress-intervention/README.md | 46 +++++ .../deploy-pipeline.json | 156 ++++++++++++++++ .../p4-progress-intervention/index.html | 174 ++++++++++++++++++ 3 files changed, 376 insertions(+) create mode 100644 samples/a2h-prototypes/p4-progress-intervention/README.md create mode 100644 samples/a2h-prototypes/p4-progress-intervention/deploy-pipeline.json create mode 100644 samples/a2h-prototypes/p4-progress-intervention/index.html diff --git a/samples/a2h-prototypes/p4-progress-intervention/README.md b/samples/a2h-prototypes/p4-progress-intervention/README.md new file mode 100644 index 000000000..97c8dcb21 --- /dev/null +++ b/samples/a2h-prototypes/p4-progress-intervention/README.md @@ -0,0 +1,46 @@ +# P4 — Status/Progress with Human Intervention Points + +## Pattern: INFORM → AUTHORIZE + +This prototype demonstrates a deployment pipeline that **progressively updates** the UI as steps complete, then **pauses for human approval** before the final deploy step. + +The two A2H interaction types used: + +- **INFORM** — Steps 1–3 use `updateComponents` to show real-time progress (Build ✅ → Test ✅ → Stage ✅). The human observes but doesn't act. +- **AUTHORIZE** — Step 4 pauses the pipeline and injects an approval card with Approve/Rollback buttons. The pipeline cannot proceed without human input. + +## How It Works + +### Message Sequence (4 messages in `deploy-pipeline.json`) + +1. **createSurface + initial state** — Pipeline header, app info, four steps all pending (Build ⏳, rest ⬜) +2. **updateComponents** — Build ✅ complete (1m 12s), Test ⏳ running +3. **updateComponents** — Test ✅ passed (48/48), Stage ⏳ running +4. **updateComponents** — Stage ✅ live, Deploy ⏸️ paused. Approval card injected into the tree by updating `main-col.children` to include `approve-card`. + +### Key Techniques + +- **Progressive UI via `updateComponents`**: Only changed component IDs are sent each step. The renderer merges them into the existing tree and re-renders. +- **Dynamic tree structure**: Step 4 updates `main-col.children` to append new components (divider + approval card), demonstrating that layout itself can change, not just props. +- **Data model tracks pipeline state**: Each message includes `updateDataModel` with step statuses, timestamps, and artifacts. Buttons use `{"path": ...}` bindings to resolve context from the data model. +- **Emoji icons for status**: ✅ ⏳ ⏸️ ⬜ — simple, universal, no icon font dependency. + +### Renderer (`index.html`) + +Replays the 4 messages with 2–3 second delays between steps, simulating real-time progress. A status bar shows the current phase. The approval card buttons emit events to the event log. + +## Identified Gaps in A2UI v0.9 + +| Gap | Impact | Workaround | +|-----|--------|------------| +| No native progress bar component | Can't show % completion or animated bars | Used emoji icons + text labels | +| No animation/transition support | Step changes are instantaneous, not smooth | Full re-render; could add CSS transitions in renderer | +| No conditional rendering / visibility toggle | Can't hide approval card until needed; must restructure children | Updated `main-col.children` array to add components | +| No timer/auto-refresh primitive | Agent must push each update; no client-side polling | Renderer uses setTimeout to simulate; real impl needs server-push | +| `updateDataModel` replaces entire model | No partial/merge — agent must resend full data model each step | Acceptable for small models; problematic at scale | + +## Files + +- `deploy-pipeline.json` — A2UI v0.9 message sequence (4 messages) +- `index.html` — Standalone renderer with replay simulation +- `README.md` — This file diff --git a/samples/a2h-prototypes/p4-progress-intervention/deploy-pipeline.json b/samples/a2h-prototypes/p4-progress-intervention/deploy-pipeline.json new file mode 100644 index 000000000..1ec77acb3 --- /dev/null +++ b/samples/a2h-prototypes/p4-progress-intervention/deploy-pipeline.json @@ -0,0 +1,156 @@ +[ + { + "createSurface": { + "id": "deploy-pipeline", + "title": "Deployment Pipeline", + "sendDataModel": true + }, + "updateDataModel": { + "value": { + "pipeline": { + "app": "acme-web-v2.4.1", + "branch": "main", + "commit": "a3f9c21", + "startedAt": "2026-03-04T07:30:00Z", + "steps": { + "build": { "status": "running", "startedAt": "2026-03-04T07:30:00Z" }, + "test": { "status": "pending" }, + "stage": { "status": "pending" }, + "deploy": { "status": "pending" } + } + } + } + }, + "updateComponents": { + "components": [ + { "id": "root", "component": "Card", "child": "main-col" }, + { "id": "main-col", "component": "Column", "children": ["header-row", "divider1", "info-row", "divider2", "step-build", "step-test", "step-stage", "step-deploy"] }, + + { "id": "header-row", "component": "Row", "children": ["header-icon", "header-text"], "align": "center" }, + { "id": "header-icon", "component": "Text", "text": "🚀", "variant": "h3" }, + { "id": "header-text", "component": "Text", "text": "Deployment Pipeline", "variant": "h3" }, + + { "id": "divider1", "component": "Divider" }, + + { "id": "info-row", "component": "Column", "children": ["info-app", "info-commit"] }, + { "id": "info-app", "component": "Text", "text": { "path": "/pipeline/app" }, "variant": "body" }, + { "id": "info-commit", "component": "Text", "text": { "path": "/pipeline/commit" }, "variant": "caption" }, + + { "id": "divider2", "component": "Divider" }, + + { "id": "step-build", "component": "Row", "children": ["build-icon", "build-label"], "align": "center" }, + { "id": "build-icon", "component": "Text", "text": "⏳" }, + { "id": "build-label", "component": "Text", "text": "Build", "variant": "body" }, + + { "id": "step-test", "component": "Row", "children": ["test-icon", "test-label"], "align": "center" }, + { "id": "test-icon", "component": "Text", "text": "⬜" }, + { "id": "test-label", "component": "Text", "text": "Test", "variant": "body" }, + + { "id": "step-stage", "component": "Row", "children": ["stage-icon", "stage-label"], "align": "center" }, + { "id": "stage-icon", "component": "Text", "text": "⬜" }, + { "id": "stage-label", "component": "Text", "text": "Stage", "variant": "body" }, + + { "id": "step-deploy", "component": "Row", "children": ["deploy-icon", "deploy-label"], "align": "center" }, + { "id": "deploy-icon", "component": "Text", "text": "⬜" }, + { "id": "deploy-label", "component": "Text", "text": "Deploy", "variant": "body" } + ] + } + }, + + { + "_comment": "Step 2: Build complete, Test running", + "updateDataModel": { + "value": { + "pipeline": { + "app": "acme-web-v2.4.1", + "branch": "main", + "commit": "a3f9c21", + "startedAt": "2026-03-04T07:30:00Z", + "steps": { + "build": { "status": "complete", "startedAt": "2026-03-04T07:30:00Z", "completedAt": "2026-03-04T07:31:12Z", "artifact": "acme-web:a3f9c21" }, + "test": { "status": "running", "startedAt": "2026-03-04T07:31:12Z" }, + "stage": { "status": "pending" }, + "deploy": { "status": "pending" } + } + } + } + }, + "updateComponents": { + "components": [ + { "id": "build-icon", "component": "Text", "text": "✅" }, + { "id": "build-label", "component": "Text", "text": "Build — 1m 12s", "variant": "body" }, + { "id": "test-icon", "component": "Text", "text": "⏳" }, + { "id": "test-label", "component": "Text", "text": "Test — running…", "variant": "body" } + ] + } + }, + + { + "_comment": "Step 3: Test complete, Stage running", + "updateDataModel": { + "value": { + "pipeline": { + "app": "acme-web-v2.4.1", + "branch": "main", + "commit": "a3f9c21", + "startedAt": "2026-03-04T07:30:00Z", + "steps": { + "build": { "status": "complete", "startedAt": "2026-03-04T07:30:00Z", "completedAt": "2026-03-04T07:31:12Z", "artifact": "acme-web:a3f9c21" }, + "test": { "status": "complete", "startedAt": "2026-03-04T07:31:12Z", "completedAt": "2026-03-04T07:33:45Z", "results": "48/48 passed" }, + "stage": { "status": "running", "startedAt": "2026-03-04T07:33:45Z" }, + "deploy": { "status": "pending" } + } + } + } + }, + "updateComponents": { + "components": [ + { "id": "test-icon", "component": "Text", "text": "✅" }, + { "id": "test-label", "component": "Text", "text": "Test — 48/48 passed", "variant": "body" }, + { "id": "stage-icon", "component": "Text", "text": "⏳" }, + { "id": "stage-label", "component": "Text", "text": "Stage — running…", "variant": "body" } + ] + } + }, + + { + "_comment": "Step 4: Stage complete, Deploy PAUSED for approval", + "updateDataModel": { + "value": { + "pipeline": { + "app": "acme-web-v2.4.1", + "branch": "main", + "commit": "a3f9c21", + "startedAt": "2026-03-04T07:30:00Z", + "steps": { + "build": { "status": "complete", "startedAt": "2026-03-04T07:30:00Z", "completedAt": "2026-03-04T07:31:12Z", "artifact": "acme-web:a3f9c21" }, + "test": { "status": "complete", "startedAt": "2026-03-04T07:31:12Z", "completedAt": "2026-03-04T07:33:45Z", "results": "48/48 passed" }, + "stage": { "status": "complete", "startedAt": "2026-03-04T07:33:45Z", "completedAt": "2026-03-04T07:35:20Z", "url": "https://stage.acme.dev" }, + "deploy": { "status": "paused", "reason": "Requires human approval" } + } + } + } + }, + "updateComponents": { + "components": [ + { "id": "stage-icon", "component": "Text", "text": "✅" }, + { "id": "stage-label", "component": "Text", "text": "Stage — live at stage.acme.dev", "variant": "body" }, + { "id": "deploy-icon", "component": "Text", "text": "⏸️" }, + { "id": "deploy-label", "component": "Text", "text": "Deploy — awaiting approval", "variant": "body" }, + + { "id": "main-col", "component": "Column", "children": ["header-row", "divider1", "info-row", "divider2", "step-build", "step-test", "step-stage", "step-deploy", "divider3", "approve-card"] }, + { "id": "divider3", "component": "Divider" }, + + { "id": "approve-card", "component": "Card", "child": "approve-col" }, + { "id": "approve-col", "component": "Column", "children": ["approve-title", "approve-desc", "approve-buttons"] }, + { "id": "approve-title", "component": "Text", "text": "⚠️ Deploy to production?", "variant": "h3" }, + { "id": "approve-desc", "component": "Text", "text": "acme-web-v2.4.1 (a3f9c21) • 48/48 tests passed • Staged OK", "variant": "body" }, + { "id": "approve-buttons", "component": "Row", "children": ["btn-rollback", "btn-approve"], "justify": "end" }, + { "id": "btn-rollback", "component": "Button", "variant": "default", "child": "btn-rollback-text", "action": { "event": { "name": "pipeline.rollback", "context": { "app": { "path": "/pipeline/app" }, "commit": { "path": "/pipeline/commit" } } } } }, + { "id": "btn-rollback-text", "component": "Text", "text": "Rollback" }, + { "id": "btn-approve", "component": "Button", "variant": "primary", "child": "btn-approve-text", "action": { "event": { "name": "pipeline.approve_deploy", "context": { "app": { "path": "/pipeline/app" }, "commit": { "path": "/pipeline/commit" } } } } }, + { "id": "btn-approve-text", "component": "Text", "text": "Approve" } + ] + } + } +] diff --git a/samples/a2h-prototypes/p4-progress-intervention/index.html b/samples/a2h-prototypes/p4-progress-intervention/index.html new file mode 100644 index 000000000..46c1fb871 --- /dev/null +++ b/samples/a2h-prototypes/p4-progress-intervention/index.html @@ -0,0 +1,174 @@ + + + + + + A2H Prototype 4 — Progress with Intervention (INFORM + AUTHORIZE) + + + + +

A2H × A2UI — Prototype 4: Progress + Intervention

+
Loading pipeline…
+
+

Event Log

+
+ + + + From 25a5c5406ca109772647258df474f19f9d62c165 Mon Sep 17 00:00:00 2001 From: Zaf Date: Wed, 4 Mar 2026 07:48:28 +0000 Subject: [PATCH 05/22] feat(a2h): P5 multi-step expense wizard prototype MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit COLLECT→COLLECT→INFORM→AUTHORIZE sequence on a single surface. Each step swaps content via updateComponents. Data model accumulates across steps. Includes standalone renderer with step navigation and data model state panel. --- samples/a2h-prototypes/p5-wizard/README.md | 79 +++++ .../p5-wizard/expense-wizard.json | 207 ++++++++++++ samples/a2h-prototypes/p5-wizard/index.html | 306 ++++++++++++++++++ 3 files changed, 592 insertions(+) create mode 100644 samples/a2h-prototypes/p5-wizard/README.md create mode 100644 samples/a2h-prototypes/p5-wizard/expense-wizard.json create mode 100644 samples/a2h-prototypes/p5-wizard/index.html diff --git a/samples/a2h-prototypes/p5-wizard/README.md b/samples/a2h-prototypes/p5-wizard/README.md new file mode 100644 index 000000000..c60b21007 --- /dev/null +++ b/samples/a2h-prototypes/p5-wizard/README.md @@ -0,0 +1,79 @@ +# P5 — Multi-step Expense Report Wizard + +**Intent sequence:** COLLECT → COLLECT → INFORM → AUTHORIZE + +## What This Demonstrates + +A multi-step wizard where an agent guides a user through submitting an expense report. Each step is a separate `updateComponents` message that swaps the visible UI content on a single persistent surface. The data model accumulates across all steps. + +### Steps + +| Step | Intent | Purpose | +|------|-----------|---------| +| 1 | COLLECT | Expense basics: date, category, amount, description | +| 2 | COLLECT | Receipt details: receipt number, vendor, notes | +| 3 | INFORM | Read-only review of all collected data | +| 4 | AUTHORIZE | Final approval with policy compliance note | + +### Message Structure + +``` +createSurface (sendDataModel: true) +updateDataModel (initial empty fields) +updateComponents — Step 1 (COLLECT form) +updateComponents — Step 2 (COLLECT form, replaces step 1 fields) +updateComponents — Step 3 (INFORM review, read-only Text bindings) +updateComponents — Step 4 (AUTHORIZE with lock icon + submit/cancel) +``` + +Each `updateComponents` message replaces the `main-col` children list, swapping which fields/content are visible. Shared component IDs (`step-indicator`, `step-title`, `actions`) get overwritten each step. Step-specific components (form fields, review rows) are added fresh. + +### Data Model + +```json +{ + "expense": { + "date": "", "category": "", "amount": "", + "description": "", "receiptNumber": "", "vendor": "", "notes": "" + }, + "meta": { "interactionId": "...", "currentStep": 1, "totalSteps": 4 } +} +``` + +All fields live under `/expense/*`. The renderer accumulates values as users fill in each step. With `sendDataModel: true`, every button event carries the full model. + +### Events + +- `a2h.collect.submit` — Step 1, 2: advance to next step +- `a2h.wizard.back` — Navigate backward +- `a2h.inform.acknowledge` — Step 3: proceed to authorization +- `a2h.authorize.approve` / `a2h.authorize.reject` — Step 4: final decision + +## What Works Well + +- **Single surface, swapped content**: `updateComponents` elegantly replaces the UI for each step while preserving the data model +- **Data model accumulation**: Fields from step 1 persist through step 4; review step reads them back via `{path: ...}` bindings +- **Step indicator**: Simple Text component at top tracks progress ("Step 2 of 4") +- **sendDataModel: true**: The entire form state ships with every event — the agent/server always has the full picture +- **Consistent layout**: Reusing IDs like `main-col`, `actions`, `step-title` means the card structure stays stable across steps + +## What's Painful + +- **No wizard/stepper component**: There's no native `Stepper` or `Tabs` in the v0.9 catalog. Step indicator is just a Text string — no visual progress bar, no clickable breadcrumbs +- **No field validation between steps**: Can't prevent advancing from step 1 to step 2 if required fields are empty. Would need Checkable trait wired to button enabled state, but cross-field validation (e.g., "amount must be numeric") is limited +- **No conditional visibility**: Can't show/hide the policy warning based on amount > $500. It's always visible. Would need `when` or `visible` property +- **Component ID collisions**: Steps share IDs to enable overwrite, but this is a convention hack — if step 2 forgets to redeclare `actions`, it inherits step 1's version. Error-prone +- **No file upload**: A2UI v0.9 has no file input component. Receipt "upload" is faked with text fields +- **Verbose**: Each review row needs 3 components (Row + label Text + value Text). A `KeyValue` or `Table` component would cut this dramatically +- **No transition animation**: Step changes are instant DOM swaps. A `Wizard` container could provide slide/fade transitions +- **Back navigation rebuilds from scratch**: To go back, the renderer must replay all messages up to the target step. No undo/snapshot mechanism in the protocol + +## Running + +```bash +# From this directory: +python3 -m http.server 8080 +# Open http://localhost:8080 +``` + +Or open `index.html` directly (needs a local server for fetch). diff --git a/samples/a2h-prototypes/p5-wizard/expense-wizard.json b/samples/a2h-prototypes/p5-wizard/expense-wizard.json new file mode 100644 index 000000000..ab4e5a791 --- /dev/null +++ b/samples/a2h-prototypes/p5-wizard/expense-wizard.json @@ -0,0 +1,207 @@ +[ + { + "_comment": "Step 0: Create surface + initial data model", + "version": "v0.9", + "createSurface": { + "surfaceId": "a2h-expense-wizard-001", + "catalogId": "https://a2ui.org/specification/v0_9/basic_catalog.json", + "sendDataModel": true + } + }, + { + "version": "v0.9", + "updateDataModel": { + "surfaceId": "a2h-expense-wizard-001", + "value": { + "expense": { + "date": "", + "category": "", + "amount": "", + "description": "", + "receiptNumber": "", + "vendor": "", + "notes": "" + }, + "meta": { + "interactionId": "exp-2026-0304-001", + "agentId": "expense-assistant-v1", + "currentStep": 1, + "totalSteps": 4 + } + } + } + }, + { + "_comment": "Step 1 (COLLECT): Expense basics", + "version": "v0.9", + "updateComponents": { + "surfaceId": "a2h-expense-wizard-001", + "components": [ + { "id": "root", "component": "Card", "child": "main-col" }, + { + "id": "main-col", "component": "Column", + "children": ["step-indicator", "divider-top", "step-title", "step-desc", "field-date", "field-category", "field-amount", "field-description", "divider-bottom", "actions"] + }, + { "id": "step-indicator", "component": "Text", "text": "Step 1 of 4 — Expense Details", "variant": "caption" }, + { "id": "divider-top", "component": "Divider" }, + { "id": "step-title", "component": "Text", "text": "Expense Basics", "variant": "h3" }, + { "id": "step-desc", "component": "Text", "text": "Enter the basic details for your expense report.", "variant": "body" }, + { "id": "field-date", "component": "TextField", "label": "Date (YYYY-MM-DD)", "value": { "path": "/expense/date" } }, + { + "id": "field-category", "component": "MultipleChoice", "label": "Category", + "value": { "path": "/expense/category" }, + "options": ["Travel", "Meals", "Equipment", "Software", "Office Supplies", "Other"] + }, + { "id": "field-amount", "component": "TextField", "label": "Amount ($)", "value": { "path": "/expense/amount" } }, + { "id": "field-description", "component": "TextField", "label": "Description", "value": { "path": "/expense/description" } }, + { "id": "divider-bottom", "component": "Divider" }, + { + "id": "actions", "component": "Row", "justify": "end", + "children": ["btn-next"] + }, + { "id": "btn-next-text", "component": "Text", "text": "Next →" }, + { + "id": "btn-next", "component": "Button", "child": "btn-next-text", "variant": "primary", + "action": { "event": { "name": "a2h.collect.submit", "context": { "step": 1, "intent": "COLLECT" } } } + } + ] + } + }, + { + "_comment": "Step 2 (COLLECT): Receipt details", + "version": "v0.9", + "updateComponents": { + "surfaceId": "a2h-expense-wizard-001", + "components": [ + { "id": "step-indicator", "component": "Text", "text": "Step 2 of 4 — Receipt Info", "variant": "caption" }, + { "id": "step-title", "component": "Text", "text": "Receipt Details", "variant": "h3" }, + { "id": "step-desc", "component": "Text", "text": "Provide receipt information. (File upload not yet supported in A2UI — enter details manually.)", "variant": "body" }, + { + "id": "main-col", "component": "Column", + "children": ["step-indicator", "divider-top", "step-title", "step-desc", "field-receipt-num", "field-vendor", "field-notes", "divider-bottom", "actions"] + }, + { "id": "field-receipt-num", "component": "TextField", "label": "Receipt Number", "value": { "path": "/expense/receiptNumber" } }, + { "id": "field-vendor", "component": "TextField", "label": "Vendor", "value": { "path": "/expense/vendor" } }, + { "id": "field-notes", "component": "TextField", "label": "Notes", "value": { "path": "/expense/notes" } }, + { + "id": "actions", "component": "Row", "justify": "end", + "children": ["btn-back", "btn-next"] + }, + { "id": "btn-back-text", "component": "Text", "text": "← Back" }, + { + "id": "btn-back", "component": "Button", "child": "btn-back-text", + "action": { "event": { "name": "a2h.wizard.back", "context": { "step": 2 } } } + }, + { "id": "btn-next-text", "component": "Text", "text": "Next →" }, + { + "id": "btn-next", "component": "Button", "child": "btn-next-text", "variant": "primary", + "action": { "event": { "name": "a2h.collect.submit", "context": { "step": 2, "intent": "COLLECT" } } } + } + ] + } + }, + { + "_comment": "Step 3 (INFORM): Review summary", + "version": "v0.9", + "updateComponents": { + "surfaceId": "a2h-expense-wizard-001", + "components": [ + { "id": "step-indicator", "component": "Text", "text": "Step 3 of 4 — Review", "variant": "caption" }, + { "id": "step-title", "component": "Text", "text": "Review Your Expense", "variant": "h3" }, + { "id": "step-desc", "component": "Text", "text": "Please review the details below before submitting.", "variant": "body" }, + { + "id": "main-col", "component": "Column", + "children": ["step-indicator", "divider-top", "step-title", "step-desc", "review-card", "divider-bottom", "actions"] + }, + { "id": "review-card", "component": "Card", "child": "review-col" }, + { + "id": "review-col", "component": "Column", + "children": ["rev-date-row", "rev-cat-row", "rev-amount-row", "rev-desc-row", "rev-divider", "rev-receipt-row", "rev-vendor-row", "rev-notes-row"] + }, + { "id": "rev-date-row", "component": "Row", "children": ["rev-date-label", "rev-date-val"] }, + { "id": "rev-date-label", "component": "Text", "text": "Date:", "variant": "label" }, + { "id": "rev-date-val", "component": "Text", "text": { "path": "/expense/date" }, "variant": "body" }, + { "id": "rev-cat-row", "component": "Row", "children": ["rev-cat-label", "rev-cat-val"] }, + { "id": "rev-cat-label", "component": "Text", "text": "Category:", "variant": "label" }, + { "id": "rev-cat-val", "component": "Text", "text": { "path": "/expense/category" }, "variant": "body" }, + { "id": "rev-amount-row", "component": "Row", "children": ["rev-amount-label", "rev-amount-val"] }, + { "id": "rev-amount-label", "component": "Text", "text": "Amount:", "variant": "label" }, + { "id": "rev-amount-val", "component": "Text", "text": { "path": "/expense/amount" }, "variant": "body" }, + { "id": "rev-desc-row", "component": "Row", "children": ["rev-desc-label", "rev-desc-val"] }, + { "id": "rev-desc-label", "component": "Text", "text": "Description:", "variant": "label" }, + { "id": "rev-desc-val", "component": "Text", "text": { "path": "/expense/description" }, "variant": "body" }, + { "id": "rev-divider", "component": "Divider" }, + { "id": "rev-receipt-row", "component": "Row", "children": ["rev-receipt-label", "rev-receipt-val"] }, + { "id": "rev-receipt-label", "component": "Text", "text": "Receipt #:", "variant": "label" }, + { "id": "rev-receipt-val", "component": "Text", "text": { "path": "/expense/receiptNumber" }, "variant": "body" }, + { "id": "rev-vendor-row", "component": "Row", "children": ["rev-vendor-label", "rev-vendor-val"] }, + { "id": "rev-vendor-label", "component": "Text", "text": "Vendor:", "variant": "label" }, + { "id": "rev-vendor-val", "component": "Text", "text": { "path": "/expense/vendor" }, "variant": "body" }, + { "id": "rev-notes-row", "component": "Row", "children": ["rev-notes-label", "rev-notes-val"] }, + { "id": "rev-notes-label", "component": "Text", "text": "Notes:", "variant": "label" }, + { "id": "rev-notes-val", "component": "Text", "text": { "path": "/expense/notes" }, "variant": "body" }, + { + "id": "actions", "component": "Row", "justify": "end", + "children": ["btn-back", "btn-next"] + }, + { "id": "btn-back-text", "component": "Text", "text": "← Back" }, + { + "id": "btn-back", "component": "Button", "child": "btn-back-text", + "action": { "event": { "name": "a2h.wizard.back", "context": { "step": 3 } } } + }, + { "id": "btn-next-text", "component": "Text", "text": "Proceed to Submit →" }, + { + "id": "btn-next", "component": "Button", "child": "btn-next-text", "variant": "primary", + "action": { "event": { "name": "a2h.inform.acknowledge", "context": { "step": 3, "intent": "INFORM" } } } + } + ] + } + }, + { + "_comment": "Step 4 (AUTHORIZE): Final approval", + "version": "v0.9", + "updateComponents": { + "surfaceId": "a2h-expense-wizard-001", + "components": [ + { "id": "step-indicator", "component": "Text", "text": "Step 4 of 4 — Authorization", "variant": "caption" }, + { "id": "step-title", "component": "Text", "text": "Submit Expense Report", "variant": "h3" }, + { "id": "step-desc", "component": "Text", "text": "By submitting, you confirm this expense complies with company policy.", "variant": "body" }, + { + "id": "main-col", "component": "Column", + "children": ["step-indicator", "divider-top", "header-row", "step-desc", "auth-summary", "policy-note", "divider-bottom", "actions"] + }, + { "id": "header-row", "component": "Row", "children": ["header-icon", "step-title"], "align": "center" }, + { "id": "header-icon", "component": "Icon", "name": "lock" }, + { "id": "auth-summary", "component": "Card", "child": "auth-summary-col" }, + { + "id": "auth-summary-col", "component": "Column", + "children": ["auth-amount-row", "auth-cat-row", "auth-vendor-row"] + }, + { "id": "auth-amount-row", "component": "Row", "children": ["auth-amount-label", "auth-amount-val"] }, + { "id": "auth-amount-label", "component": "Text", "text": "Amount:", "variant": "label" }, + { "id": "auth-amount-val", "component": "Text", "text": { "path": "/expense/amount" }, "variant": "h3" }, + { "id": "auth-cat-row", "component": "Row", "children": ["auth-cat-label", "auth-cat-val"] }, + { "id": "auth-cat-label", "component": "Text", "text": "Category:", "variant": "label" }, + { "id": "auth-cat-val", "component": "Text", "text": { "path": "/expense/category" }, "variant": "body" }, + { "id": "auth-vendor-row", "component": "Row", "children": ["auth-vendor-label", "auth-vendor-val"] }, + { "id": "auth-vendor-label", "component": "Text", "text": "Vendor:", "variant": "label" }, + { "id": "auth-vendor-val", "component": "Text", "text": { "path": "/expense/vendor" }, "variant": "body" }, + { "id": "policy-note", "component": "Text", "text": "⚠️ Expenses over $500 require manager co-approval.", "variant": "caption" }, + { + "id": "actions", "component": "Row", "justify": "end", + "children": ["btn-cancel", "btn-submit"] + }, + { "id": "btn-cancel-text", "component": "Text", "text": "Cancel" }, + { + "id": "btn-cancel", "component": "Button", "child": "btn-cancel-text", + "action": { "event": { "name": "a2h.authorize.reject", "context": { "interactionId": { "path": "/meta/interactionId" }, "intent": "AUTHORIZE" } } } + }, + { "id": "btn-submit-text", "component": "Text", "text": "Submit Expense" }, + { + "id": "btn-submit", "component": "Button", "child": "btn-submit-text", "variant": "primary", + "action": { "event": { "name": "a2h.authorize.approve", "context": { "interactionId": { "path": "/meta/interactionId" }, "intent": "AUTHORIZE" } } } + } + ] + } + } +] diff --git a/samples/a2h-prototypes/p5-wizard/index.html b/samples/a2h-prototypes/p5-wizard/index.html new file mode 100644 index 000000000..dd5911c54 --- /dev/null +++ b/samples/a2h-prototypes/p5-wizard/index.html @@ -0,0 +1,306 @@ + + + + + + A2H Prototype 5 — Multi-step Expense Wizard + + + + + + + +

A2H × A2UI — Prototype 5: Multi-step Expense Wizard (COLLECT → COLLECT → INFORM → AUTHORIZE)

+
+
+
+
+
📊 Data Model
+
+
+
+
📋 Event Log
+
+
+
+
+ + + + From 09cb091c1bedef2d7643854d9c0b99d440f7bada Mon Sep 17 00:00:00 2001 From: Zaf Date: Wed, 4 Mar 2026 07:58:06 +0000 Subject: [PATCH 06/22] =?UTF-8?q?feat:=20A2H=C3=97A2UI=20helper=20library,?= =?UTF-8?q?=20demo,=20and=20prototype=20fixes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- samples/a2h-prototypes/DESIGN.md | 416 ++++++++++++++++++ samples/a2h-prototypes/README.md | 68 +++ samples/a2h-prototypes/demo/README.md | 31 ++ samples/a2h-prototypes/demo/index.html | 316 +++++++++++++ samples/a2h-prototypes/lib/a2h-a2ui.js | 415 +++++++++++++++++ .../deploy-pipeline.json | 61 ++- 6 files changed, 1290 insertions(+), 17 deletions(-) create mode 100644 samples/a2h-prototypes/DESIGN.md create mode 100644 samples/a2h-prototypes/README.md create mode 100644 samples/a2h-prototypes/demo/README.md create mode 100644 samples/a2h-prototypes/demo/index.html create mode 100644 samples/a2h-prototypes/lib/a2h-a2ui.js diff --git a/samples/a2h-prototypes/DESIGN.md b/samples/a2h-prototypes/DESIGN.md new file mode 100644 index 000000000..26d9731dc --- /dev/null +++ b/samples/a2h-prototypes/DESIGN.md @@ -0,0 +1,416 @@ +# A2H × A2UI: Rendering Human-in-the-Loop Interactions + +**Version:** 1.0 — March 2026 +**Authors:** Alan Blount, Zaf +**Status:** Proposal + +--- + +## Executive Summary + +**A2H** (Agent-to-Human) is Twilio's open protocol for standardized, auditable communication between AI agents and human principals. It defines five intent types — INFORM, COLLECT, AUTHORIZE, ESCALATE, RESULT — covering every reason an agent contacts a human, with cryptographic evidence of consent. **A2UI** v0.9 is a protocol for rendering server-driven UI from agent output, with data-bound components, streaming updates, and multi-renderer support (Lit, Angular, Flutter). + +These protocols are complementary. A2H defines *what* agents need from humans and *why*. A2UI defines *how it looks*. Neither addresses the other's concern. Today, A2H's `channel.render` field offers only flat text (`title` + `body`), which is adequate for SMS but impoverished for web/app channels. A2UI has the component vocabulary to render rich, interactive A2H intents — forms, approval cards, progress indicators — but lacks conventions for doing so. + +We propose a three-layer approach: (1) **convention patterns** that work with A2UI v0.9 today, (2) a **helper library** (`a2h-a2ui`) that eliminates boilerplate, and (3) **four targeted A2UI spec enhancements** that resolve the remaining friction. Five working prototypes validate the mapping and expose the gaps. + +--- + +## Background + +### A2H: The Five Intents + +| Intent | Direction | Blocking | Purpose | +|--------|-----------|----------|---------| +| **INFORM** | Agent → Human | No | One-way notification | +| **COLLECT** | Agent ↔ Human | Yes | Gather structured data | +| **AUTHORIZE** | Agent ↔ Human | Yes | Request approval with auth evidence | +| **ESCALATE** | Agent → Human | Yes | Hand off to human support | +| **RESULT** | Agent → Human | No | Report task completion | + +A2H's differentiator is its **trust layer**: every AUTHORIZE interaction produces JWS-signed evidence linking intent → consent → action. Intents use DIDs for identity, TTLs for expiry, and configurable assurance levels (passkey, OTP, push). The protocol is channel-agnostic — the A2H gateway routes to SMS, email, push, or web. + +### A2UI v0.9: What's Relevant + +A2UI's basic catalog provides: `Text`, `Image`, `Icon`, `Card`, `Row`, `Column`, `List`, `Tabs`, `Divider`, `Button`, `TextField`, `CheckBox`, `ChoicePicker`, `DateTimeInput`, `Slider`, `Modal`. Key features for A2H: + +- **Data binding** — `{"path": "/foo"}` provides two-way binding between components and a JSON data model +- **`sendDataModel: true`** — on `createSurface`, ensures the full data model accompanies every action event +- **`updateComponents` merge** — send only changed components; the renderer merges into the existing tree +- **`updateDataModel`** — push new data from the server; bound components update automatically +- **Validation** — `required()`, `regex()`, `email()`, `length()` functions on Checkable components +- **Formatting** — `formatCurrency()`, `formatDate()`, `formatNumber()` for display + +### The Gap + +A2H defines a message envelope with `channel.render.title` and `channel.render.body` — plain text. For SMS, this is fine. For a web or app experience, you want a form with validation for COLLECT, a detail card with approve/reject buttons for AUTHORIZE, a live-updating status indicator for ESCALATE. A2UI has these components but no guidance on how to compose them for A2H intents. That's what this document provides. + +--- + +## Intent-to-Surface Mapping + +| A2H Intent | A2UI Surface Pattern | Key Components | `sendDataModel` | Coverage | +|------------|---------------------|----------------|------------------|----------| +| **INFORM** | Notification card | Card → Column → Icon + Text | No | ✅ Complete | +| **COLLECT** | Form card | Card → Column → TextFields + ChoicePicker + Button | **Yes** | ✅ Complete | +| **AUTHORIZE** | Confirmation card | Card → Column → detail rows + Approve/Reject Buttons | **Yes** | ⚠️ 90% (no conditional visibility, no TTL countdown) | +| **ESCALATE** | Status card with live updates | Card → Column → Text (bound) + Button (cancel) | Yes | ⚠️ 80% (no progress indicator) | +| **RESULT** | Completion card | Card → Column → Icon (✓/✗) + Text + optional link Button | No | ✅ Complete | + +Every intent maps to the same three-message pattern: +``` +createSurface → updateDataModel → updateComponents +``` + +Surface IDs follow: `a2h:{intent}:{interactionId}` +Event names follow: `a2h.{intent}.{action}` (e.g., `a2h.authorize.approve`) + +--- + +## What Works Today + +### `sendDataModel: true` is the Key Enabler + +This single flag on `createSurface` eliminates the need to wire individual form field values into button action contexts. When any event fires, the renderer includes the entire data model snapshot. For COLLECT, this means the agent gets all form values on submit. For AUTHORIZE, it means the interaction ID and all metadata travel with the approval. This is the most important pattern in the entire integration. + +Prototypes P1, P3, and P5 all rely on this. It works cleanly. A React developer would recognize it as analogous to uncontrolled forms with `FormData` — simple and correct. + +### Data Binding + Formatting + +`{"path": "/transfer/amount"}` in a Text component, combined with `formatCurrency()`, renders `$500.00` from a numeric value in the data model. P1 uses this for the approval card's financial details. P2 uses it for live escalation status messages that update via `updateDataModel`. The binding is two-way for input components, one-way for display components. It just works. + +### Incremental Updates via `updateComponents` + +P4 (deploy pipeline) sends four sequential `updateComponents` messages, each changing only the components that differ. The renderer merges them. This produces a convincing real-time progress experience — build ✅, test ✅, stage ✅, then an approval gate appears. The merge model is better than full re-renders for server-driven UI over a network. + +### Card-Based Layout + +Every prototype uses the same structural pattern: `Card → Column → [header Row, Divider, body content, Divider, action Row]`. It handles every A2H intent we tested. It's not exciting, but it's reliable and consistent across renderers. + +--- + +## Proposed A2UI Enhancements + +Ranked by impact. Based on findings across all five prototypes. + +### 1. Conditional Visibility (`visible` binding) + +**What it enables:** Show/hide components based on data model state. Every prototype works around this gap. P4 restructures `children` arrays to inject an approval card. P5 replaces entire component trees per step. P1 can't disable buttons after the user clicks Approve — it must swap the whole tree. + +**Proposed:** Add `visible` property to `ComponentCommon`, accepting a `DynamicBoolean`: + +```json +{ + "id": "btn-approve", + "component": "Button", + "visible": {"fn": "equals", "args": [{"path": "/state"}, "pending"]}, + "child": "btn-approve-text", + "action": { "event": { "name": "a2h.authorize.approve" } } +} +``` + +**Without this**, every state transition requires a full `updateComponents` message containing the entire replacement tree. This is verbose, error-prone (stale IDs), and prevents smooth transitions. + +**Complexity:** Small — additive schema change to ComponentCommon. No new components. Renderers add a conditional check before rendering. + +### 2. Button `label` Prop + +**What it enables:** Eliminate the mandatory Text-child-per-button boilerplate. Every prototype pays this tax — two components and two IDs per button. P5 has 6 buttons = 12 extra components. + +**Proposed:** + +```json +{ + "id": "btn-approve", + "component": "Button", + "label": "Approve", + "variant": "primary", + "action": { "event": { "name": "a2h.authorize.approve" } } +} +``` + +**Impact:** ~15-20% reduction in component count across all prototypes. This is the single biggest boilerplate source. + +**Complexity:** Tiny. Add an optional `label` string prop; if present, renderer creates an implicit Text child. + +### 3. ProgressIndicator Component + +**What it enables:** Loading/progress states for ESCALATE ("connecting to support...") and AUTHORIZE TTL countdowns. Three prototypes independently fake this with emoji (⏳, ⬜, ✅) or plain text ("Step 2 of 4"). + +**Proposed:** + +```json +{ + "id": "connecting-spinner", + "component": "ProgressIndicator", + "mode": "indeterminate", + "label": {"path": "/statusMessage"} +} +``` + +Also support `mode: "determinate"` with a `value` (0.0–1.0) for pipeline progress. + +**Complexity:** Small. Single new component. Every UI framework has a spinner/progress widget. + +### 4. KeyValue Component + +**What it enables:** Eliminate the 3-component-per-row pattern for label-value pairs (Row → Text label → Text value). P1's approval details, P2's context card, P5's review step all use this pattern. P5's review step alone has 7 pairs = 21 components. + +**Proposed:** + +```json +{ + "id": "detail-amount", + "component": "KeyValue", + "label": "Amount", + "value": {"fn": "formatCurrency", "args": [{"path": "/amount"}, "USD"]} +} +``` + +**Impact:** ~60-70% reduction in summary/detail-view components. + +**Complexity:** Small. Renders as a styled row with label and value. Every design system has this. + +--- + +## Convention Patterns + +These require no spec changes — just documented best practices. + +### Surface ID Convention + +``` +a2h:{intentType}:{interactionId} +``` + +Example: `a2h:authorize:01936f8a-7b2c-7000-8000-000000000001` + +### Component ID Convention + +``` +root # root container +header, body, footer # structural sections +title, subtitle, description # text elements +actions # button row +btn-{action} # btn-approve, btn-reject, btn-submit +field-{name} # field-email, field-address +detail-{key} # detail-amount, detail-agent +``` + +### Event Name Convention + +``` +a2h.{intent}.{action} +``` + +Examples: `a2h.authorize.approve`, `a2h.collect.submit`, `a2h.escalate.cancel` + +### Data Model Path Convention + +``` +/state # "pending" | "approved" | "rejected" | "expired" +/interactionId # A2H interaction_id +/agentId # A2H agent_id +/fields/{name} # COLLECT form values +/meta/{key} # TTL, assurance level, timestamps +``` + +### State Transition Pattern (v0.9 workaround) + +Without conditional visibility, state changes require replacing the component tree: + +1. User clicks Approve → event fires with `a2h.authorize.approve` +2. Server sends `updateDataModel` setting `/state` to `"approved"` +3. Server sends `updateComponents` replacing action buttons with a confirmation Text + Icon +4. Optionally, server sends `deleteSurface` after a delay + +This works but is verbose. The `visible` binding (Enhancement #1) would reduce step 3 to a data model update only. + +### Card Layout Template + +Every A2H intent follows this skeleton: + +``` +Card + └─ Column + ├─ Row [icon + title] ← header + ├─ Divider + ├─ [intent-specific content] ← body + ├─ Divider + └─ Row [buttons] ← actions +``` + +--- + +## Helper Library: `a2h-a2ui` + +### Motivation + +Rendering an AUTHORIZE intent as A2UI requires ~40 lines of component JSON, correct data model setup, proper ID wiring, and event naming. This is mechanical and error-prone. A helper library reduces each intent to a single function call. + +### API Design + +```typescript +import { createAuthorizeSurface, createCollectForm, createInformCard } from 'a2h-a2ui'; + +// AUTHORIZE → A2UI messages +const messages = createAuthorizeSurface({ + interactionId: '01936f8a-...', + action: 'book_flight', + description: 'Book SFO→JFK on Mar 15', + amount: 450, + currency: 'USD', + agentId: 'did:web:travel-agent.example.com', + ttlSec: 300, +}); +// Returns: [createSurface, updateDataModel, updateComponents] messages + +// COLLECT → A2UI messages +const messages = createCollectForm({ + interactionId: '...', + title: 'Shipping Address', + fields: [ + { name: 'name', type: 'text', label: 'Full Name', required: true }, + { name: 'street', type: 'text', label: 'Street Address', required: true }, + { name: 'state', type: 'choice', label: 'State', options: ['CA', 'NY', ...] }, + { name: 'zip', type: 'text', label: 'ZIP Code', pattern: '^\\d{5}$' }, + ], +}); + +// Parse action events back to A2H responses +const response = parseActionEvent(event); +// { intent: 'authorize', action: 'approve', interactionId: '...', dataModel: {...} } +``` + +### Utilities + +```typescript +// Surface/component ID generation +makeSurfaceId('authorize', interactionId) // → "a2h:authorize:01936f8a-..." +makeFieldId('email') // → "field-email" + +// JSONL serialization +toJsonlStream(messages) // → string of newline-delimited JSON +fromJsonlStream(jsonl) // → message array + +// TTL management (since A2UI has no native TTL) +const { messages, deleteAt } = withTtl(surfaceMessages, 300); +// Caller is responsible for scheduling deleteSurface at deleteAt +``` + +### Target + +npm package, TypeScript, zero dependencies. Works with any A2UI renderer. Generates spec-compliant A2UI v0.9 JSONL. Approximately 500-800 lines of code. + +--- + +## Prototypes + +Five prototypes validate the intent-to-surface mapping. Each is a standalone HTML file with a vanilla JS renderer plus the A2UI message sequence as JSON. + +### P1: Approval Card (AUTHORIZE) — [p1-approval/](./p1-approval/) + +A financial transfer approval card. Shows transfer details (account, amount formatted with `formatCurrency`, description) in a detail section, with Approve and Reject buttons. Demonstrates the core AUTHORIZE lifecycle and `sendDataModel: true` pattern. + +**What users see:** A card titled "Authorization Required" with a lock icon. Transfer details in a nested detail section. Two buttons at the bottom: blue "Approve" and grey "Reject". After clicking, the server would replace buttons with a confirmation message (not yet implemented in the static prototype). + +**Rating: 4/5.** Clean and production-ready pattern. Missing post-approval state transition and dismiss option. + +### P2: Escalation Handoff (ESCALATE) — [p2-escalation/](./p2-escalation/) + +A customer service escalation card. Shows the escalation reason, conversation context preserved in a nested card, priority indicator, and three connection method buttons (chat, phone, video). Status message updates via `updateDataModel`. + +**What users see:** A card with a person icon and "Connecting to Support" title. Priority badge in red. Previous context summary. Three action buttons offering different connection methods. Status text that updates in real-time. + +**Rating: 4/5.** Good use of live data model updates. The context preservation pattern (nested card with prior conversation) is well-designed. + +### P3: Guided Input Form (COLLECT) — [p3-guided-input/](./p3-guided-input/) + +A shipping address form with TextFields, a ChoicePicker for delivery speed, and pre-populated values. The star prototype — cleanest mapping, most LLM-friendly component tree. + +**What users see:** A form titled "Shipping Details" with fields for name, street, city (side-by-side with state dropdown), ZIP code, and delivery speed selection. A blue "Submit" button at the bottom. Fields show pre-populated values that the user can edit. + +**Rating: 5/5.** This is exactly what COLLECT should look like. Simple, clean, immediately understandable. The `sendDataModel` pattern shines here. + +### P4: Deploy Pipeline (INFORM → AUTHORIZE) — [p4-progress-intervention/](./p4-progress-intervention/) + +A deployment pipeline that progressively updates: Build ✅ → Test ✅ → Stage ✅ → Approval Gate ⏸️. Demonstrates INFORM-to-AUTHORIZE transition and dynamic tree modification (injecting an approval card at step 4). + +**What users see:** A pipeline visualization with four steps. Each step animates from pending (⬜) to running (⏳) to complete (✅). At step 4, an approval card slides in with "Deploy to Production?" and Approve/Rollback buttons. + +**Rating: 3/5.** Most technically interesting prototype — progressive updates and tree mutation are compelling. However, the JSON structure deviates from valid A2UI JSONL (missing `version` fields, combined message objects). Needs a conformance fix before sharing externally. Emoji-as-icons is fragile across platforms. + +### P5: Expense Report Wizard (COLLECT → COLLECT → INFORM → AUTHORIZE) — [p5-wizard/](./p5-wizard/) + +A four-step wizard on a single persistent surface. Step 1 collects expense basics, step 2 collects receipt details, step 3 shows a read-only review, step 4 requests final approval. Each step is a full `updateComponents` that swaps the visible content. + +**What users see:** A step indicator ("Step 1 of 4") with a title that changes per step. Form fields in steps 1-2, a summary table in step 3, and an approval card with a policy warning in step 4. Back/Next navigation. + +**Rating: 3.5/5.** Ambitious and realistic multi-intent flow. Exposes every gap simultaneously — the review step's 21-component summary table desperately needs KeyValue, step navigation relies on fragile ID reuse, and the lack of conditional visibility makes the step-swap pattern verbose. + +--- + +## Gaps & Future Work + +### Addressable via Convention (no spec changes) + +- Event naming, surface IDs, data model paths — documented above +- State transitions via `updateComponents` tree swap +- Wizard/stepper patterns via sequential `updateComponents` +- Form-level validation gating via `and()` composition over Checkable fields + +### Addressable via Helper Library + +- Boilerplate reduction (~40 lines → 1 function call per intent) +- Component ID generation and collision avoidance +- JSONL serialization/deserialization +- TTL scheduling (client-side `setTimeout` for `deleteSurface`) + +### Requires A2UI Spec Changes + +| Enhancement | Impact | Effort | +|-------------|--------|--------| +| `visible` binding | Eliminates tree-swap workaround for all state transitions | Small | +| Button `label` prop | 15-20% component reduction | Tiny | +| ProgressIndicator | Proper loading/progress states | Small | +| KeyValue | 60-70% reduction in detail views | Small | + +### Explicitly Out of Scope (for now) + +**Cryptographic evidence in A2UI actions.** A2H's killer feature is JWS-signed consent evidence. A2UI buttons fire plain events with no mechanism for client-side auth flows (WebAuthn, passkeys). This is important but it's a massive scope expansion — it requires client-side auth integration, which is properly an A2H gateway concern, not a rendering concern. The right architecture: A2UI renders the approval card, the button fires an event, the A2H gateway intercepts and triggers the auth flow before forwarding to the agent. + +**Multi-surface orchestration.** P5's wizard uses a single surface with tree swaps. An alternative is multiple surfaces (one per step) with an orchestrator. A2UI has no concept of surface relationships or sequencing. This matters for complex workflows but can be handled in the helper library or application layer. + +**`updateDataModel` partial updates.** Currently replaces the entire model. JSON Patch or merge semantics would help for large data models. Low priority — A2H data models are typically small. + +--- + +## Recommendation + +### Phase 1: Conventions (now) + +Document and publish the convention patterns from this document: surface ID format, event naming, component ID conventions, card layout template, state transition pattern. These work with A2UI v0.9 as-is. Cost: documentation only. + +### Phase 2: Helper Library (weeks) + +Build `a2h-a2ui` as an npm package. TypeScript, zero dependencies, ~500-800 lines. Covers all five intents. Reduces the barrier from "read the A2UI spec and hand-craft 40 lines of JSON" to "call one function." This is what makes A2H+A2UI viable for real adoption. + +### Phase 3: Spec Proposals (months) + +Propose the four enhancements to the A2UI team. `visible` binding first — it's the highest-impact, lowest-effort change. Button `label` second. ProgressIndicator and KeyValue can follow. + +### What to Build Independently vs. Propose + +| Build ourselves | Propose to A2UI | +|----------------|-----------------| +| Convention patterns | `visible` binding | +| Helper library | Button `label` prop | +| Prototype fixes (P4 conformance) | ProgressIndicator component | +| TTL scheduling logic | KeyValue component | +| A2H gateway ↔ A2UI translator | (Future) Partial data model updates | + +### Bottom Line + +A2UI v0.9 handles ~80% of A2H rendering today. The `sendDataModel` pattern and data binding make COLLECT and AUTHORIZE surprisingly clean. The remaining 20% splits between one critical gap (conditional visibility), several papercuts (button boilerplate, detail-view verbosity, no progress indicator), and things properly handled outside the rendering protocol (auth evidence, TTL enforcement). + +The path is clear: conventions first, library second, spec proposals third. We can ship useful A2H rendering now without waiting for spec changes. diff --git a/samples/a2h-prototypes/README.md b/samples/a2h-prototypes/README.md new file mode 100644 index 000000000..51708f36e --- /dev/null +++ b/samples/a2h-prototypes/README.md @@ -0,0 +1,68 @@ +# A2H × A2UI Prototypes + +Exploration of rendering [A2H](https://github.com/twilio/a2h) (Agent-to-Human) intents using the A2UI v0.9 protocol. Five working prototypes validate the mapping between A2H's five intent types and A2UI's component model. + +## Quick Start + +Open the all-in-one demo: + +```bash +cd demo && npx serve . +# Or just open demo/index.html in your browser +``` + +## Contents + +| Path | Description | +|------|-------------| +| [DESIGN.md](./DESIGN.md) | Full design document — intent mapping, conventions, proposed enhancements | +| [demo/](./demo/) | **All 5 intents on one page**, generated via the helper library | +| [lib/a2h-a2ui.js](./lib/a2h-a2ui.js) | Helper library — generates A2UI v0.9 messages from A2H intent descriptions | +| [p1-approval/](./p1-approval/) | **AUTHORIZE** — Financial transfer approval card | +| [p2-escalation/](./p2-escalation/) | **ESCALATE** — Customer service handoff | +| [p3-guided-input/](./p3-guided-input/) | **COLLECT** — Shipping address form (⭐ cleanest prototype) | +| [p4-progress-intervention/](./p4-progress-intervention/) | **INFORM → AUTHORIZE** — Deploy pipeline with progressive updates | +| [p5-wizard/](./p5-wizard/) | **COLLECT → COLLECT → INFORM → AUTHORIZE** — Expense report wizard | + +## Helper Library API + +```js +import { + createAuthorizeSurface, + createCollectSurface, + createInformSurface, + createEscalateSurface, + createResultSurface, +} from './lib/a2h-a2ui.js'; + +const messages = createAuthorizeSurface({ + surfaceId: 'a2h:authorize:001', + title: 'Authorization Required', + description: 'Agent wants to book a flight.', + details: [{ label: 'Amount', path: '/amount' }], + dataModel: { amount: '$450.00' }, +}); +// Returns: [createSurface, updateDataModel, updateComponents] — valid A2UI v0.9 +``` + +Each function returns an array of A2UI v0.9 messages with valid component IDs, `catalogId: "basic"`, proper data bindings, and `sendDataModel: true` on interactive surfaces. + +## Key Findings + +- **`sendDataModel: true`** is the key enabler — eliminates per-field wiring for forms and approvals +- **Data binding** (`{path: "/foo"}`) provides clean two-way binding +- **Incremental updates** via `updateComponents` merge enables real-time progress UIs +- **A2UI v0.9 covers ~80%** of A2H rendering needs today + +## Proposed A2UI Enhancements + +1. **`visible` binding** — Conditional rendering (eliminates tree-swap workarounds) +2. **Button `label` prop** — Kills 2-component-per-button boilerplate (~15-20% reduction) +3. **ProgressIndicator** — Proper loading/progress states +4. **KeyValue component** — Eliminates 3-component-per-detail-row pattern + +See [DESIGN.md](./DESIGN.md) for full analysis. + +## Status + +**Exploration / proof-of-concept.** Prototypes are static HTML+JS with inline renderers. The helper library generates valid A2UI v0.9 but is not yet published as an npm package. Next steps: propose enhancements to the A2UI spec, port to real renderers (Lit/Angular/Flutter). diff --git a/samples/a2h-prototypes/demo/README.md b/samples/a2h-prototypes/demo/README.md new file mode 100644 index 000000000..d2f70095e --- /dev/null +++ b/samples/a2h-prototypes/demo/README.md @@ -0,0 +1,31 @@ +# A2H × A2UI Demo + +Single-page demo showing all five A2H intents rendered as A2UI v0.9 surfaces, generated by the `a2h-a2ui` helper library. + +## How to Run + +Open `index.html` in any modern browser (requires ES module support): + +```bash +# From this directory: +npx serve . +# Or simply open index.html directly (works in Chrome/Firefox/Safari) +``` + +## What It Shows + +| Intent | Surface | Interactive | +|--------|---------|-------------| +| **AUTHORIZE** | Transfer approval card with details and Approve/Reject buttons | ✅ Click buttons | +| **COLLECT** | Shipping address form with text fields and dropdown | ✅ Fill & submit | +| **INFORM** | Order shipped notification with tracking details | View only | +| **ESCALATE** | Support handoff with context and connection options | ✅ Click buttons | +| **RESULT** | Flight booking confirmation with summary | View only | + +Click any button to see the emitted A2UI event in the event log at the bottom. + +## Architecture + +- **`../lib/a2h-a2ui.js`** — Generates A2UI v0.9 message arrays from high-level intent descriptions +- **Inline renderer** — Minimal vanilla JS A2UI v0.9 renderer (same approach as the individual prototypes) +- **No build step** — Pure ES modules, no bundler needed diff --git a/samples/a2h-prototypes/demo/index.html b/samples/a2h-prototypes/demo/index.html new file mode 100644 index 000000000..88aaaf738 --- /dev/null +++ b/samples/a2h-prototypes/demo/index.html @@ -0,0 +1,316 @@ + + + + + + A2H × A2UI Demo — All 5 Intents + + + + + + +

A2H × A2UI — All Five Intents

+

Generated via the a2h-a2ui helper library

+
+
+ + + + diff --git a/samples/a2h-prototypes/lib/a2h-a2ui.js b/samples/a2h-prototypes/lib/a2h-a2ui.js new file mode 100644 index 000000000..59672363c --- /dev/null +++ b/samples/a2h-prototypes/lib/a2h-a2ui.js @@ -0,0 +1,415 @@ +/** + * a2h-a2ui.js — Helper library for generating A2UI v0.9 message sequences + * from A2H (Agent-to-Human) intent descriptions. + * + * Each function returns an array of A2UI v0.9 messages (createSurface, + * updateDataModel, updateComponents) ready for JSONL serialization. + */ + +const CATALOG_ID = 'https://a2ui.org/specification/v0_9/basic_catalog.json'; +const VERSION = 'v0.9'; + +// ─── Utilities ─── + +let _counter = 0; +function uid(prefix) { + return `${prefix}-${++_counter}`; +} + +/** Reset ID counter (useful for tests). */ +export function resetIds() { + _counter = 0; +} + +function msg(payload) { + return { version: VERSION, ...payload }; +} + +function createSurfaceMsg(surfaceId, sendDataModel = true) { + return msg({ + createSurface: { + surfaceId, + catalogId: CATALOG_ID, + sendDataModel, + }, + }); +} + +function updateDataModelMsg(surfaceId, value) { + return msg({ updateDataModel: { surfaceId, value } }); +} + +function updateComponentsMsg(surfaceId, components) { + return msg({ updateComponents: { surfaceId, components } }); +} + +/** Build the standard Card → Column → [header, divider, body, divider, actions] skeleton. */ +function cardSkeleton(prefix, { titleIcon, titleText, bodyChildren, actionChildren }) { + const ids = { + root: `${prefix}-root`, + col: `${prefix}-col`, + headerRow: `${prefix}-header-row`, + headerIcon: `${prefix}-header-icon`, + headerText: `${prefix}-header-text`, + div1: `${prefix}-div-1`, + body: `${prefix}-body`, + div2: `${prefix}-div-2`, + actions: `${prefix}-actions`, + }; + + const components = [ + { id: ids.root, component: 'Card', child: ids.col }, + ]; + + const colChildren = [ids.headerRow, ids.div1, ids.body]; + if (actionChildren && actionChildren.length > 0) { + colChildren.push(ids.div2, ids.actions); + } + components.push({ id: ids.col, component: 'Column', children: colChildren }); + + // Header + components.push( + { id: ids.headerRow, component: 'Row', children: [ids.headerIcon, ids.headerText], align: 'center' }, + { id: ids.headerIcon, component: 'Text', text: titleIcon, variant: 'h3' }, + { id: ids.headerText, component: 'Text', text: titleText, variant: 'h3' }, + { id: ids.div1, component: 'Divider' }, + ); + + // Body + components.push({ id: ids.body, component: 'Column', children: bodyChildren.map(c => c.id) }); + components.push(...bodyChildren); + + // Actions + if (actionChildren && actionChildren.length > 0) { + components.push({ id: ids.div2, component: 'Divider' }); + components.push({ id: ids.actions, component: 'Row', children: actionChildren.map(c => c.id), justify: 'end' }); + components.push(...actionChildren); + } + + return { components, rootId: ids.root }; +} + +function makeButton(id, label, variant, eventName, context) { + const textId = `${id}-text`; + return [ + { id: textId, component: 'Text', text: label }, + { + id, + component: 'Button', + variant, + child: textId, + action: { event: { name: eventName, context: context || {} } }, + }, + ]; +} + +function makeDetailRow(id, label, valuePath) { + const rowId = id; + const labelId = `${id}-label`; + const valId = `${id}-val`; + return [ + { id: rowId, component: 'Row', children: [labelId, valId] }, + { id: labelId, component: 'Text', text: label, variant: 'label' }, + { id: valId, component: 'Text', text: typeof valuePath === 'string' && valuePath.startsWith('/') ? { path: valuePath } : (valuePath ?? ''), variant: 'body' }, + ]; +} + +// ─── AUTHORIZE ─── + +/** + * Create an AUTHORIZE surface — approval card with details and approve/reject buttons. + * + * @param {Object} opts + * @param {string} opts.surfaceId - Surface ID + * @param {string} opts.title - Card title + * @param {string} opts.description - Description text + * @param {Array<{label: string, path: string}>} opts.details - Detail rows with data model paths + * @param {Array<{id: string, label: string, variant?: string, event: string}>} [opts.actions] - Action buttons + * @param {Object} opts.dataModel - Initial data model value + * @returns {Array} A2UI v0.9 messages + */ +export function createAuthorizeSurface({ surfaceId, title, description, details = [], actions, dataModel = {} }) { + const prefix = 'auth'; + + // Default actions: Approve + Reject + const actionDefs = actions || [ + { id: 'btn-reject', label: 'Reject', variant: 'default', event: 'a2h.authorize.reject' }, + { id: 'btn-approve', label: 'Approve', variant: 'primary', event: 'a2h.authorize.approve' }, + ]; + + // Build detail rows + const detailComponents = []; + const descId = uid(`${prefix}-desc`); + detailComponents.push({ id: descId, component: 'Text', text: description, variant: 'body' }); + + const detailCardId = uid(`${prefix}-details-card`); + const detailColId = uid(`${prefix}-details-col`); + const detailRowComps = []; + const detailRowIds = []; + for (const d of details) { + const rowId = uid(`${prefix}-detail`); + const rows = makeDetailRow(rowId, d.label, d.path); + detailRowComps.push(...rows); + detailRowIds.push(rowId); + } + + detailComponents.push( + { id: detailCardId, component: 'Card', child: detailColId }, + { id: detailColId, component: 'Column', children: detailRowIds }, + ...detailRowComps, + ); + + // Build action buttons + const actionComps = []; + for (const a of actionDefs) { + actionComps.push(...makeButton(a.id, a.label, a.variant || 'default', a.event, {})); + } + + const { components } = cardSkeleton(prefix, { + titleIcon: '🔒', + titleText: title || 'Authorization Required', + bodyChildren: detailComponents, + actionChildren: actionComps.filter(c => c.component === 'Button' || c.component === 'Text'), + }); + + // Flatten: action buttons need both Text and Button components + const allActionComps = []; + for (const a of actionDefs) { + allActionComps.push(...makeButton(a.id, a.label, a.variant || 'default', a.event, {})); + } + + return [ + createSurfaceMsg(surfaceId), + updateDataModelMsg(surfaceId, dataModel), + updateComponentsMsg(surfaceId, components), + ]; +} + +// ─── COLLECT ─── + +/** + * Create a COLLECT surface — form with fields and a submit button. + * + * @param {Object} opts + * @param {string} opts.surfaceId + * @param {string} opts.title + * @param {Array<{id: string, label: string, type: 'text'|'select', options?: string[], path: string, value?: any}>} opts.fields + * @param {string} [opts.submitLabel='Submit'] + * @param {Object} [opts.dataModel={}] + * @returns {Array} A2UI v0.9 messages + */ +export function createCollectSurface({ surfaceId, title, fields = [], submitLabel = 'Submit', dataModel = {} }) { + const prefix = 'collect'; + + const fieldComponents = []; + for (const f of fields) { + const labelId = `field-${f.id}-label`; + const fieldId = `field-${f.id}`; + fieldComponents.push({ id: labelId, component: 'Text', text: f.label, variant: 'label' }); + + if (f.type === 'select') { + fieldComponents.push({ + id: fieldId, + component: 'ChoicePicker', + options: (f.options || []).map(o => typeof o === 'string' ? { label: o, value: o } : o), + value: f.value != null ? { path: f.path } : undefined, + data: { path: f.path }, + }); + } else { + fieldComponents.push({ + id: fieldId, + component: 'TextField', + placeholder: f.label, + value: f.value != null ? { path: f.path } : undefined, + data: { path: f.path }, + }); + } + } + + const submitBtnComps = makeButton('btn-submit', submitLabel, 'primary', 'a2h.collect.submit', {}); + + const { components } = cardSkeleton(prefix, { + titleIcon: '📝', + titleText: title || 'Information Required', + bodyChildren: fieldComponents, + actionChildren: submitBtnComps, + }); + + return [ + createSurfaceMsg(surfaceId), + updateDataModelMsg(surfaceId, dataModel), + updateComponentsMsg(surfaceId, components), + ]; +} + +// ─── INFORM ─── + +/** + * Create an INFORM surface — notification card with key-value items. + * + * @param {Object} opts + * @param {string} opts.surfaceId + * @param {string} opts.title + * @param {Array<{label: string, value: string, icon?: string}>} opts.items + * @param {Object} [opts.dataModel={}] + * @returns {Array} A2UI v0.9 messages + */ +export function createInformSurface({ surfaceId, title, items = [], dataModel = {} }) { + const prefix = 'inform'; + + const bodyComponents = []; + for (const item of items) { + const rowId = uid(`${prefix}-item`); + if (item.icon) { + const iconId = `${rowId}-icon`; + const labelId = `${rowId}-label`; + const valId = `${rowId}-val`; + bodyComponents.push( + { id: rowId, component: 'Row', children: [iconId, labelId, valId] }, + { id: iconId, component: 'Text', text: item.icon }, + { id: labelId, component: 'Text', text: item.label, variant: 'label' }, + { id: valId, component: 'Text', text: item.value, variant: 'body' }, + ); + } else { + const rows = makeDetailRow(rowId, item.label, item.value); + bodyComponents.push(...rows); + } + } + + const { components } = cardSkeleton(prefix, { + titleIcon: 'ℹ️', + titleText: title || 'Notification', + bodyChildren: bodyComponents, + actionChildren: [], + }); + + return [ + createSurfaceMsg(surfaceId, false), + updateDataModelMsg(surfaceId, dataModel), + updateComponentsMsg(surfaceId, components), + ]; +} + +// ─── ESCALATE ─── + +/** + * Create an ESCALATE surface — handoff card with reason, context, and connection options. + * + * @param {Object} opts + * @param {string} opts.surfaceId + * @param {string} opts.reason - Why the escalation is happening + * @param {string} [opts.context] - Context/summary text + * @param {Array<{id: string, label: string, event?: string}>} [opts.options] - Connection method buttons + * @param {Object} [opts.dataModel={}] + * @returns {Array} A2UI v0.9 messages + */ +export function createEscalateSurface({ surfaceId, reason, context, options, dataModel = {} }) { + const prefix = 'esc'; + + const optionDefs = options || [ + { id: 'btn-chat', label: '💬 Live Chat', event: 'a2h.escalate.connect' }, + { id: 'btn-phone', label: '📞 Phone', event: 'a2h.escalate.connect' }, + ]; + + const bodyComponents = []; + + // Reason + const reasonId = uid(`${prefix}-reason`); + bodyComponents.push({ id: reasonId, component: 'Text', text: reason, variant: 'body' }); + + // Context card + if (context) { + const ctxCardId = uid(`${prefix}-ctx-card`); + const ctxColId = uid(`${prefix}-ctx-col`); + const ctxLabelId = uid(`${prefix}-ctx-label`); + const ctxTextId = uid(`${prefix}-ctx-text`); + bodyComponents.push( + { id: ctxCardId, component: 'Card', child: ctxColId }, + { id: ctxColId, component: 'Column', children: [ctxLabelId, ctxTextId] }, + { id: ctxLabelId, component: 'Text', text: 'Context', variant: 'label' }, + { id: ctxTextId, component: 'Text', text: context, variant: 'body' }, + ); + } + + // Status message (bound to data model) + const statusId = uid(`${prefix}-status`); + bodyComponents.push({ id: statusId, component: 'Text', text: { path: '/statusMessage' }, variant: 'caption' }); + + // Action buttons + const actionComps = []; + for (const o of optionDefs) { + actionComps.push(...makeButton(o.id, o.label, 'default', o.event || 'a2h.escalate.connect', { method: o.label })); + } + + const { components } = cardSkeleton(prefix, { + titleIcon: '🆘', + titleText: 'Connecting to Support', + bodyChildren: bodyComponents, + actionChildren: actionComps, + }); + + const dm = { ...dataModel, statusMessage: dataModel.statusMessage || 'Searching for available agents…' }; + + return [ + createSurfaceMsg(surfaceId), + updateDataModelMsg(surfaceId, dm), + updateComponentsMsg(surfaceId, components), + ]; +} + +// ─── RESULT ─── + +/** + * Create a RESULT surface — completion card showing success/failure. + * + * @param {Object} opts + * @param {string} opts.surfaceId + * @param {string} opts.title + * @param {string} opts.status - 'success' | 'failure' | 'partial' + * @param {Array<{label: string, value: string}>} [opts.details] - Result details + * @param {Object} [opts.dataModel={}] + * @returns {Array} A2UI v0.9 messages + */ +export function createResultSurface({ surfaceId, title, status = 'success', details = [], dataModel = {} }) { + const prefix = 'result'; + const icon = status === 'success' ? '✅' : status === 'failure' ? '❌' : '⚠️'; + + const bodyComponents = []; + + // Status text + const statusId = uid(`${prefix}-status`); + const statusText = status === 'success' ? 'Completed successfully' : status === 'failure' ? 'Failed' : 'Partially completed'; + bodyComponents.push({ id: statusId, component: 'Text', text: statusText, variant: 'body' }); + + // Detail rows + for (const d of details) { + const rowId = uid(`${prefix}-detail`); + const rows = makeDetailRow(rowId, d.label, d.value); + bodyComponents.push(...rows); + } + + const { components } = cardSkeleton(prefix, { + titleIcon: icon, + titleText: title || 'Task Complete', + bodyChildren: bodyComponents, + actionChildren: [], + }); + + return [ + createSurfaceMsg(surfaceId, false), + updateDataModelMsg(surfaceId, dataModel), + updateComponentsMsg(surfaceId, components), + ]; +} + +// ─── JSONL Serialization ─── + +/** Convert an array of A2UI messages to JSONL string. */ +export function toJsonl(messages) { + return messages.map(m => JSON.stringify(m)).join('\n'); +} + +/** Parse a JSONL string to an array of A2UI messages. */ +export function fromJsonl(jsonl) { + return jsonl.trim().split('\n').filter(Boolean).map(line => JSON.parse(line)); +} diff --git a/samples/a2h-prototypes/p4-progress-intervention/deploy-pipeline.json b/samples/a2h-prototypes/p4-progress-intervention/deploy-pipeline.json index 1ec77acb3..5a6254ca0 100644 --- a/samples/a2h-prototypes/p4-progress-intervention/deploy-pipeline.json +++ b/samples/a2h-prototypes/p4-progress-intervention/deploy-pipeline.json @@ -1,11 +1,16 @@ [ { + "version": "v0.9", "createSurface": { - "id": "deploy-pipeline", - "title": "Deployment Pipeline", + "surfaceId": "a2h-inform-deploy-pipeline-001", + "catalogId": "https://a2ui.org/specification/v0_9/basic_catalog.json", "sendDataModel": true - }, + } + }, + { + "version": "v0.9", "updateDataModel": { + "surfaceId": "a2h-inform-deploy-pipeline-001", "value": { "pipeline": { "app": "acme-web-v2.4.1", @@ -20,23 +25,27 @@ } } } - }, + } + }, + { + "version": "v0.9", "updateComponents": { + "surfaceId": "a2h-inform-deploy-pipeline-001", "components": [ { "id": "root", "component": "Card", "child": "main-col" }, - { "id": "main-col", "component": "Column", "children": ["header-row", "divider1", "info-row", "divider2", "step-build", "step-test", "step-stage", "step-deploy"] }, + { "id": "main-col", "component": "Column", "children": ["header-row", "divider-1", "info-row", "divider-2", "step-build", "step-test", "step-stage", "step-deploy"] }, { "id": "header-row", "component": "Row", "children": ["header-icon", "header-text"], "align": "center" }, { "id": "header-icon", "component": "Text", "text": "🚀", "variant": "h3" }, { "id": "header-text", "component": "Text", "text": "Deployment Pipeline", "variant": "h3" }, - { "id": "divider1", "component": "Divider" }, + { "id": "divider-1", "component": "Divider" }, { "id": "info-row", "component": "Column", "children": ["info-app", "info-commit"] }, { "id": "info-app", "component": "Text", "text": { "path": "/pipeline/app" }, "variant": "body" }, { "id": "info-commit", "component": "Text", "text": { "path": "/pipeline/commit" }, "variant": "caption" }, - { "id": "divider2", "component": "Divider" }, + { "id": "divider-2", "component": "Divider" }, { "id": "step-build", "component": "Row", "children": ["build-icon", "build-label"], "align": "center" }, { "id": "build-icon", "component": "Text", "text": "⏳" }, @@ -58,8 +67,10 @@ }, { + "version": "v0.9", "_comment": "Step 2: Build complete, Test running", "updateDataModel": { + "surfaceId": "a2h-inform-deploy-pipeline-001", "value": { "pipeline": { "app": "acme-web-v2.4.1", @@ -74,8 +85,12 @@ } } } - }, + } + }, + { + "version": "v0.9", "updateComponents": { + "surfaceId": "a2h-inform-deploy-pipeline-001", "components": [ { "id": "build-icon", "component": "Text", "text": "✅" }, { "id": "build-label", "component": "Text", "text": "Build — 1m 12s", "variant": "body" }, @@ -86,8 +101,10 @@ }, { + "version": "v0.9", "_comment": "Step 3: Test complete, Stage running", "updateDataModel": { + "surfaceId": "a2h-inform-deploy-pipeline-001", "value": { "pipeline": { "app": "acme-web-v2.4.1", @@ -102,8 +119,12 @@ } } } - }, + } + }, + { + "version": "v0.9", "updateComponents": { + "surfaceId": "a2h-inform-deploy-pipeline-001", "components": [ { "id": "test-icon", "component": "Text", "text": "✅" }, { "id": "test-label", "component": "Text", "text": "Test — 48/48 passed", "variant": "body" }, @@ -114,8 +135,10 @@ }, { + "version": "v0.9", "_comment": "Step 4: Stage complete, Deploy PAUSED for approval", "updateDataModel": { + "surfaceId": "a2h-inform-deploy-pipeline-001", "value": { "pipeline": { "app": "acme-web-v2.4.1", @@ -130,26 +153,30 @@ } } } - }, + } + }, + { + "version": "v0.9", "updateComponents": { + "surfaceId": "a2h-inform-deploy-pipeline-001", "components": [ { "id": "stage-icon", "component": "Text", "text": "✅" }, { "id": "stage-label", "component": "Text", "text": "Stage — live at stage.acme.dev", "variant": "body" }, { "id": "deploy-icon", "component": "Text", "text": "⏸️" }, { "id": "deploy-label", "component": "Text", "text": "Deploy — awaiting approval", "variant": "body" }, - { "id": "main-col", "component": "Column", "children": ["header-row", "divider1", "info-row", "divider2", "step-build", "step-test", "step-stage", "step-deploy", "divider3", "approve-card"] }, - { "id": "divider3", "component": "Divider" }, + { "id": "main-col", "component": "Column", "children": ["header-row", "divider-1", "info-row", "divider-2", "step-build", "step-test", "step-stage", "step-deploy", "divider-3", "approve-card"] }, + { "id": "divider-3", "component": "Divider" }, { "id": "approve-card", "component": "Card", "child": "approve-col" }, - { "id": "approve-col", "component": "Column", "children": ["approve-title", "approve-desc", "approve-buttons"] }, + { "id": "approve-col", "component": "Column", "children": ["approve-title", "approve-desc", "approve-actions"] }, { "id": "approve-title", "component": "Text", "text": "⚠️ Deploy to production?", "variant": "h3" }, { "id": "approve-desc", "component": "Text", "text": "acme-web-v2.4.1 (a3f9c21) • 48/48 tests passed • Staged OK", "variant": "body" }, - { "id": "approve-buttons", "component": "Row", "children": ["btn-rollback", "btn-approve"], "justify": "end" }, - { "id": "btn-rollback", "component": "Button", "variant": "default", "child": "btn-rollback-text", "action": { "event": { "name": "pipeline.rollback", "context": { "app": { "path": "/pipeline/app" }, "commit": { "path": "/pipeline/commit" } } } } }, + { "id": "approve-actions", "component": "Row", "children": ["btn-rollback", "btn-approve"], "justify": "end" }, { "id": "btn-rollback-text", "component": "Text", "text": "Rollback" }, - { "id": "btn-approve", "component": "Button", "variant": "primary", "child": "btn-approve-text", "action": { "event": { "name": "pipeline.approve_deploy", "context": { "app": { "path": "/pipeline/app" }, "commit": { "path": "/pipeline/commit" } } } } }, - { "id": "btn-approve-text", "component": "Text", "text": "Approve" } + { "id": "btn-rollback", "component": "Button", "variant": "default", "child": "btn-rollback-text", "action": { "event": { "name": "a2h.authorize.rollback", "context": { "app": { "path": "/pipeline/app" }, "commit": { "path": "/pipeline/commit" } } } } }, + { "id": "btn-approve-text", "component": "Text", "text": "Approve" }, + { "id": "btn-approve", "component": "Button", "variant": "primary", "child": "btn-approve-text", "action": { "event": { "name": "a2h.authorize.approve", "context": { "app": { "path": "/pipeline/app" }, "commit": { "path": "/pipeline/commit" } } } } } ] } } From 99e1c412849a3881cd03a7eb790e0bd7ba5dc50d Mon Sep 17 00:00:00 2001 From: Zaf Date: Wed, 4 Mar 2026 15:42:56 +0000 Subject: [PATCH 07/22] refactor: rename prototypes to v0.9.0, prep for v0.9.1 comparisons --- samples/a2h-prototypes/DESIGN.md | 10 +++++----- samples/a2h-prototypes/README.md | 19 ++++++++++++++----- .../README.md | 0 .../authorize-transfer.json | 0 .../index.html | 0 .../README.md | 0 .../escalation-handoff.json | 0 .../index.html | 0 .../README.md | 0 .../collect-shipping.json | 0 .../index.html | 0 .../README.md | 0 .../deploy-pipeline.json | 0 .../index.html | 0 .../{p5-wizard => p5-wizard-v0.9.0}/README.md | 0 .../expense-wizard.json | 0 .../index.html | 0 17 files changed, 19 insertions(+), 10 deletions(-) rename samples/a2h-prototypes/{p1-approval => p1-approval-v0.9.0}/README.md (100%) rename samples/a2h-prototypes/{p1-approval => p1-approval-v0.9.0}/authorize-transfer.json (100%) rename samples/a2h-prototypes/{p1-approval => p1-approval-v0.9.0}/index.html (100%) rename samples/a2h-prototypes/{p2-escalation => p2-escalation-v0.9.0}/README.md (100%) rename samples/a2h-prototypes/{p2-escalation => p2-escalation-v0.9.0}/escalation-handoff.json (100%) rename samples/a2h-prototypes/{p2-escalation => p2-escalation-v0.9.0}/index.html (100%) rename samples/a2h-prototypes/{p3-guided-input => p3-guided-input-v0.9.0}/README.md (100%) rename samples/a2h-prototypes/{p3-guided-input => p3-guided-input-v0.9.0}/collect-shipping.json (100%) rename samples/a2h-prototypes/{p3-guided-input => p3-guided-input-v0.9.0}/index.html (100%) rename samples/a2h-prototypes/{p4-progress-intervention => p4-progress-intervention-v0.9.0}/README.md (100%) rename samples/a2h-prototypes/{p4-progress-intervention => p4-progress-intervention-v0.9.0}/deploy-pipeline.json (100%) rename samples/a2h-prototypes/{p4-progress-intervention => p4-progress-intervention-v0.9.0}/index.html (100%) rename samples/a2h-prototypes/{p5-wizard => p5-wizard-v0.9.0}/README.md (100%) rename samples/a2h-prototypes/{p5-wizard => p5-wizard-v0.9.0}/expense-wizard.json (100%) rename samples/a2h-prototypes/{p5-wizard => p5-wizard-v0.9.0}/index.html (100%) diff --git a/samples/a2h-prototypes/DESIGN.md b/samples/a2h-prototypes/DESIGN.md index 26d9731dc..b36b7139e 100644 --- a/samples/a2h-prototypes/DESIGN.md +++ b/samples/a2h-prototypes/DESIGN.md @@ -308,7 +308,7 @@ npm package, TypeScript, zero dependencies. Works with any A2UI renderer. Genera Five prototypes validate the intent-to-surface mapping. Each is a standalone HTML file with a vanilla JS renderer plus the A2UI message sequence as JSON. -### P1: Approval Card (AUTHORIZE) — [p1-approval/](./p1-approval/) +### P1: Approval Card (AUTHORIZE) — [p1-approval-v0.9.0/](./p1-approval-v0.9.0/) A financial transfer approval card. Shows transfer details (account, amount formatted with `formatCurrency`, description) in a detail section, with Approve and Reject buttons. Demonstrates the core AUTHORIZE lifecycle and `sendDataModel: true` pattern. @@ -316,7 +316,7 @@ A financial transfer approval card. Shows transfer details (account, amount form **Rating: 4/5.** Clean and production-ready pattern. Missing post-approval state transition and dismiss option. -### P2: Escalation Handoff (ESCALATE) — [p2-escalation/](./p2-escalation/) +### P2: Escalation Handoff (ESCALATE) — [p2-escalation-v0.9.0/](./p2-escalation-v0.9.0/) A customer service escalation card. Shows the escalation reason, conversation context preserved in a nested card, priority indicator, and three connection method buttons (chat, phone, video). Status message updates via `updateDataModel`. @@ -324,7 +324,7 @@ A customer service escalation card. Shows the escalation reason, conversation co **Rating: 4/5.** Good use of live data model updates. The context preservation pattern (nested card with prior conversation) is well-designed. -### P3: Guided Input Form (COLLECT) — [p3-guided-input/](./p3-guided-input/) +### P3: Guided Input Form (COLLECT) — [p3-guided-input-v0.9.0/](./p3-guided-input-v0.9.0/) A shipping address form with TextFields, a ChoicePicker for delivery speed, and pre-populated values. The star prototype — cleanest mapping, most LLM-friendly component tree. @@ -332,7 +332,7 @@ A shipping address form with TextFields, a ChoicePicker for delivery speed, and **Rating: 5/5.** This is exactly what COLLECT should look like. Simple, clean, immediately understandable. The `sendDataModel` pattern shines here. -### P4: Deploy Pipeline (INFORM → AUTHORIZE) — [p4-progress-intervention/](./p4-progress-intervention/) +### P4: Deploy Pipeline (INFORM → AUTHORIZE) — [p4-progress-intervention-v0.9.0/](./p4-progress-intervention-v0.9.0/) A deployment pipeline that progressively updates: Build ✅ → Test ✅ → Stage ✅ → Approval Gate ⏸️. Demonstrates INFORM-to-AUTHORIZE transition and dynamic tree modification (injecting an approval card at step 4). @@ -340,7 +340,7 @@ A deployment pipeline that progressively updates: Build ✅ → Test ✅ → Sta **Rating: 3/5.** Most technically interesting prototype — progressive updates and tree mutation are compelling. However, the JSON structure deviates from valid A2UI JSONL (missing `version` fields, combined message objects). Needs a conformance fix before sharing externally. Emoji-as-icons is fragile across platforms. -### P5: Expense Report Wizard (COLLECT → COLLECT → INFORM → AUTHORIZE) — [p5-wizard/](./p5-wizard/) +### P5: Expense Report Wizard (COLLECT → COLLECT → INFORM → AUTHORIZE) — [p5-wizard-v0.9.0/](./p5-wizard-v0.9.0/) A four-step wizard on a single persistent surface. Step 1 collects expense basics, step 2 collects receipt details, step 3 shows a read-only review, step 4 requests final approval. Each step is a full `updateComponents` that swaps the visible content. diff --git a/samples/a2h-prototypes/README.md b/samples/a2h-prototypes/README.md index 51708f36e..08884c0f2 100644 --- a/samples/a2h-prototypes/README.md +++ b/samples/a2h-prototypes/README.md @@ -11,6 +11,15 @@ cd demo && npx serve . # Or just open demo/index.html in your browser ``` +## Two Versions + +Each prototype has (or will have) two versions: + +- **`*-v0.9.0`** — Built with the current A2UI v0.9 spec only. No spec changes required. Shows what's possible today, including workarounds for missing features. +- **`*-v0.9.1-with-helper`** — Built with proposed spec additions (`visible` binding, button `label`, ProgressIndicator, KeyValue) plus the `a2h-a2ui` helper library. Shows what's possible with small, targeted improvements to the spec. + +Comparing the two versions side-by-side makes the case for each proposed enhancement concrete and measurable. + ## Contents | Path | Description | @@ -18,11 +27,11 @@ cd demo && npx serve . | [DESIGN.md](./DESIGN.md) | Full design document — intent mapping, conventions, proposed enhancements | | [demo/](./demo/) | **All 5 intents on one page**, generated via the helper library | | [lib/a2h-a2ui.js](./lib/a2h-a2ui.js) | Helper library — generates A2UI v0.9 messages from A2H intent descriptions | -| [p1-approval/](./p1-approval/) | **AUTHORIZE** — Financial transfer approval card | -| [p2-escalation/](./p2-escalation/) | **ESCALATE** — Customer service handoff | -| [p3-guided-input/](./p3-guided-input/) | **COLLECT** — Shipping address form (⭐ cleanest prototype) | -| [p4-progress-intervention/](./p4-progress-intervention/) | **INFORM → AUTHORIZE** — Deploy pipeline with progressive updates | -| [p5-wizard/](./p5-wizard/) | **COLLECT → COLLECT → INFORM → AUTHORIZE** — Expense report wizard | +| [p1-approval-v0.9.0/](./p1-approval-v0.9.0/) | **AUTHORIZE** — Financial transfer approval card | +| [p2-escalation-v0.9.0/](./p2-escalation-v0.9.0/) | **ESCALATE** — Customer service handoff | +| [p3-guided-input-v0.9.0/](./p3-guided-input-v0.9.0/) | **COLLECT** — Shipping address form (⭐ cleanest prototype) | +| [p4-progress-intervention-v0.9.0/](./p4-progress-intervention-v0.9.0/) | **INFORM → AUTHORIZE** — Deploy pipeline with progressive updates | +| [p5-wizard-v0.9.0/](./p5-wizard-v0.9.0/) | **COLLECT → COLLECT → INFORM → AUTHORIZE** — Expense report wizard | ## Helper Library API diff --git a/samples/a2h-prototypes/p1-approval/README.md b/samples/a2h-prototypes/p1-approval-v0.9.0/README.md similarity index 100% rename from samples/a2h-prototypes/p1-approval/README.md rename to samples/a2h-prototypes/p1-approval-v0.9.0/README.md diff --git a/samples/a2h-prototypes/p1-approval/authorize-transfer.json b/samples/a2h-prototypes/p1-approval-v0.9.0/authorize-transfer.json similarity index 100% rename from samples/a2h-prototypes/p1-approval/authorize-transfer.json rename to samples/a2h-prototypes/p1-approval-v0.9.0/authorize-transfer.json diff --git a/samples/a2h-prototypes/p1-approval/index.html b/samples/a2h-prototypes/p1-approval-v0.9.0/index.html similarity index 100% rename from samples/a2h-prototypes/p1-approval/index.html rename to samples/a2h-prototypes/p1-approval-v0.9.0/index.html diff --git a/samples/a2h-prototypes/p2-escalation/README.md b/samples/a2h-prototypes/p2-escalation-v0.9.0/README.md similarity index 100% rename from samples/a2h-prototypes/p2-escalation/README.md rename to samples/a2h-prototypes/p2-escalation-v0.9.0/README.md diff --git a/samples/a2h-prototypes/p2-escalation/escalation-handoff.json b/samples/a2h-prototypes/p2-escalation-v0.9.0/escalation-handoff.json similarity index 100% rename from samples/a2h-prototypes/p2-escalation/escalation-handoff.json rename to samples/a2h-prototypes/p2-escalation-v0.9.0/escalation-handoff.json diff --git a/samples/a2h-prototypes/p2-escalation/index.html b/samples/a2h-prototypes/p2-escalation-v0.9.0/index.html similarity index 100% rename from samples/a2h-prototypes/p2-escalation/index.html rename to samples/a2h-prototypes/p2-escalation-v0.9.0/index.html diff --git a/samples/a2h-prototypes/p3-guided-input/README.md b/samples/a2h-prototypes/p3-guided-input-v0.9.0/README.md similarity index 100% rename from samples/a2h-prototypes/p3-guided-input/README.md rename to samples/a2h-prototypes/p3-guided-input-v0.9.0/README.md diff --git a/samples/a2h-prototypes/p3-guided-input/collect-shipping.json b/samples/a2h-prototypes/p3-guided-input-v0.9.0/collect-shipping.json similarity index 100% rename from samples/a2h-prototypes/p3-guided-input/collect-shipping.json rename to samples/a2h-prototypes/p3-guided-input-v0.9.0/collect-shipping.json diff --git a/samples/a2h-prototypes/p3-guided-input/index.html b/samples/a2h-prototypes/p3-guided-input-v0.9.0/index.html similarity index 100% rename from samples/a2h-prototypes/p3-guided-input/index.html rename to samples/a2h-prototypes/p3-guided-input-v0.9.0/index.html diff --git a/samples/a2h-prototypes/p4-progress-intervention/README.md b/samples/a2h-prototypes/p4-progress-intervention-v0.9.0/README.md similarity index 100% rename from samples/a2h-prototypes/p4-progress-intervention/README.md rename to samples/a2h-prototypes/p4-progress-intervention-v0.9.0/README.md diff --git a/samples/a2h-prototypes/p4-progress-intervention/deploy-pipeline.json b/samples/a2h-prototypes/p4-progress-intervention-v0.9.0/deploy-pipeline.json similarity index 100% rename from samples/a2h-prototypes/p4-progress-intervention/deploy-pipeline.json rename to samples/a2h-prototypes/p4-progress-intervention-v0.9.0/deploy-pipeline.json diff --git a/samples/a2h-prototypes/p4-progress-intervention/index.html b/samples/a2h-prototypes/p4-progress-intervention-v0.9.0/index.html similarity index 100% rename from samples/a2h-prototypes/p4-progress-intervention/index.html rename to samples/a2h-prototypes/p4-progress-intervention-v0.9.0/index.html diff --git a/samples/a2h-prototypes/p5-wizard/README.md b/samples/a2h-prototypes/p5-wizard-v0.9.0/README.md similarity index 100% rename from samples/a2h-prototypes/p5-wizard/README.md rename to samples/a2h-prototypes/p5-wizard-v0.9.0/README.md diff --git a/samples/a2h-prototypes/p5-wizard/expense-wizard.json b/samples/a2h-prototypes/p5-wizard-v0.9.0/expense-wizard.json similarity index 100% rename from samples/a2h-prototypes/p5-wizard/expense-wizard.json rename to samples/a2h-prototypes/p5-wizard-v0.9.0/expense-wizard.json diff --git a/samples/a2h-prototypes/p5-wizard/index.html b/samples/a2h-prototypes/p5-wizard-v0.9.0/index.html similarity index 100% rename from samples/a2h-prototypes/p5-wizard/index.html rename to samples/a2h-prototypes/p5-wizard-v0.9.0/index.html From 58e8375efa43f073cff8a5e2ac0bc425dc321ac2 Mon Sep 17 00:00:00 2001 From: Zaf Date: Wed, 4 Mar 2026 15:44:51 +0000 Subject: [PATCH 08/22] =?UTF-8?q?fix:=20P1=20approval=20v0.9.0=20=E2=80=94?= =?UTF-8?q?=20critical=20review=20fixes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Text variant 'label' → 'caption' (not in v0.9 spec enum) - Divider renderer: add missing .c-Divider class to
element - CSS: consolidate caption styling, use #id for TTL red color - README: fix cd path to match actual folder name --- samples/a2h-prototypes/p1-approval-v0.9.0/README.md | 2 +- .../p1-approval-v0.9.0/authorize-transfer.json | 8 ++++---- samples/a2h-prototypes/p1-approval-v0.9.0/index.html | 9 ++++++--- 3 files changed, 11 insertions(+), 8 deletions(-) diff --git a/samples/a2h-prototypes/p1-approval-v0.9.0/README.md b/samples/a2h-prototypes/p1-approval-v0.9.0/README.md index 79e84b1d6..6800409b1 100644 --- a/samples/a2h-prototypes/p1-approval-v0.9.0/README.md +++ b/samples/a2h-prototypes/p1-approval-v0.9.0/README.md @@ -19,7 +19,7 @@ A financial assistant agent wants to transfer $500 from a checking account to a ```bash # Any static file server works -cd samples/a2h-prototypes/p1-approval +cd samples/a2h-prototypes/p1-approval-v0.9.0 python3 -m http.server 8080 # Open http://localhost:8080 ``` diff --git a/samples/a2h-prototypes/p1-approval-v0.9.0/authorize-transfer.json b/samples/a2h-prototypes/p1-approval-v0.9.0/authorize-transfer.json index 9b28f6d99..56dfa6590 100644 --- a/samples/a2h-prototypes/p1-approval-v0.9.0/authorize-transfer.json +++ b/samples/a2h-prototypes/p1-approval-v0.9.0/authorize-transfer.json @@ -81,7 +81,7 @@ "id": "detail-action-label", "component": "Text", "text": "Action:", - "variant": "label" + "variant": "caption" }, { "id": "detail-action-value", @@ -98,7 +98,7 @@ "id": "detail-from-label", "component": "Text", "text": "From:", - "variant": "label" + "variant": "caption" }, { "id": "detail-from-value", @@ -115,7 +115,7 @@ "id": "detail-to-label", "component": "Text", "text": "To:", - "variant": "label" + "variant": "caption" }, { "id": "detail-to-value", @@ -132,7 +132,7 @@ "id": "detail-amount-label", "component": "Text", "text": "Amount:", - "variant": "label" + "variant": "caption" }, { "id": "detail-amount-value", diff --git a/samples/a2h-prototypes/p1-approval-v0.9.0/index.html b/samples/a2h-prototypes/p1-approval-v0.9.0/index.html index 8eeaaab75..f09a1983f 100644 --- a/samples/a2h-prototypes/p1-approval-v0.9.0/index.html +++ b/samples/a2h-prototypes/p1-approval-v0.9.0/index.html @@ -43,8 +43,8 @@ .c-Text { color: #202124; } .c-Text[data-variant="h3"] { font-size: 1.1rem; font-weight: 500; } .c-Text[data-variant="body"] { font-size: 0.9rem; color: #5f6368; } - .c-Text[data-variant="label"] { font-size: 0.85rem; font-weight: 500; color: #202124; min-width: 70px; } - .c-Text[data-variant="caption"] { font-size: 0.8rem; color: #d93025; font-weight: 500; } + .c-Text[data-variant="caption"] { font-size: 0.85rem; font-weight: 500; color: #5f6368; min-width: 70px; } + #ttl-text { color: #d93025; min-width: auto; } .c-Button { border: 1px solid #dadce0; border-radius: 20px; @@ -154,7 +154,9 @@

Event Log

return el; } case 'Divider': { - return document.createElement('hr'); + const el = document.createElement('hr'); + el.className = 'c-Divider'; + return el; } case 'Icon': { const el = document.createElement('span'); @@ -165,6 +167,7 @@

Event Log

case 'Text': { const el = document.createElement('span'); el.className = 'c-Text'; + el.id = id; if (comp.variant) el.dataset.variant = comp.variant; el.textContent = resolve(comp.text); return el; From 2c7bec54949ef0ecb0418943d4a194e5c8b5e450 Mon Sep 17 00:00:00 2001 From: Zaf Date: Wed, 4 Mar 2026 15:48:10 +0000 Subject: [PATCH 09/22] =?UTF-8?q?feat:=20P1=20approval=20v0.9.1-with-helpe?= =?UTF-8?q?r=20=E2=80=94=20proposed=20spec=20improvements?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../p1-approval-v0.9.1-with-helper/README.md | 93 ++++ .../authorize-transfer-with-helper.js | 64 +++ .../authorize-transfer.json | 192 ++++++++ .../p1-approval-v0.9.1-with-helper/index.html | 410 ++++++++++++++++++ 4 files changed, 759 insertions(+) create mode 100644 samples/a2h-prototypes/p1-approval-v0.9.1-with-helper/README.md create mode 100644 samples/a2h-prototypes/p1-approval-v0.9.1-with-helper/authorize-transfer-with-helper.js create mode 100644 samples/a2h-prototypes/p1-approval-v0.9.1-with-helper/authorize-transfer.json create mode 100644 samples/a2h-prototypes/p1-approval-v0.9.1-with-helper/index.html diff --git a/samples/a2h-prototypes/p1-approval-v0.9.1-with-helper/README.md b/samples/a2h-prototypes/p1-approval-v0.9.1-with-helper/README.md new file mode 100644 index 000000000..7539baf7f --- /dev/null +++ b/samples/a2h-prototypes/p1-approval-v0.9.1-with-helper/README.md @@ -0,0 +1,93 @@ +# P1: Approval Card — v0.9.1 with Helper Comparison + +Same approval scenario as [v0.9.0](../p1-approval-v0.9.0/), rewritten with proposed v0.9.1 features and compared against helper library output. + +## What the User Sees + +Identical UI in both versions — a card titled "Authorization Required" with transfer details, Approve/Reject buttons. **Same experience, less code.** + +v0.9.1 adds: after clicking Approve, the buttons hide and a "Processing..." spinner appears — purely via data model update (no `updateComponents` needed). + +## Line Count Comparison + +| Version | Lines | Components | Size | +|---------|-------|------------|------| +| v0.9.0 JSON | ~120 | 30 | 4.2 KB | +| **v0.9.1 JSON** | **~90** | **24** | **3.5 KB** | +| **Helper JS** | **~35** | (generated) | **1.0 KB** | + +**v0.9.1 is 25% shorter than v0.9.0. The helper is 70% shorter.** + +## What Each v0.9.1 Feature Eliminated + +### 1. `KeyValue` component +**Before (v0.9.0):** 3 components per detail row (Row + Text label + Text value) +```json +{ "id": "detail-amount-row", "component": "Row", "children": ["detail-amount-label", "detail-amount-value"] }, +{ "id": "detail-amount-label", "component": "Text", "text": "Amount:", "variant": "caption" }, +{ "id": "detail-amount-value", "component": "Text", "text": { "path": "/transfer/amount" }, "variant": "body" } +``` + +**After (v0.9.1):** 1 component +```json +{ "id": "detail-amount", "component": "KeyValue", "label": "Amount", "value": { "path": "/transfer/amount" } } +``` + +**Impact:** 4 detail rows × 2 eliminated components = **8 fewer components**. + +### 2. Button `label` prop +**Before (v0.9.0):** 2 components per button (Text child + Button) +```json +{ "id": "approve-btn-text", "component": "Text", "text": "Approve" }, +{ "id": "approve-btn", "component": "Button", "child": "approve-btn-text", ... } +``` + +**After (v0.9.1):** 1 component +```json +{ "id": "approve-btn", "component": "Button", "label": "Approve", ... } +``` + +**Impact:** 2 buttons × 1 eliminated component = **2 fewer components**. + +### 3. `visible` binding +**Before (v0.9.0):** To show a "Processing..." state after approval, the server must send an entire `updateComponents` message replacing the button row with confirmation text (~15+ lines of JSON over the wire). + +**After (v0.9.1):** Components declare their visibility conditions upfront. The server only sends `updateDataModel` to set `"/state": "processing"`: +```json +{ + "id": "actions", + "component": "Row", + "visible": { "fn": "equals", "args": [{ "path": "/state" }, "pending"] } +} +``` + +**Impact:** Eliminates post-action `updateComponents` entirely. State transitions become **1 data model update** instead of a full component tree swap. + +### 4. `ProgressIndicator` component +Not strictly needed for the approval card, but used here to show the "Processing..." state with a proper spinner instead of emoji (⏳). + +## Developer Experience + +| Aspect | v0.9.0 | v0.9.1 | Helper | +|--------|--------|--------|--------| +| Detail rows | 3 components each | 1 component each | 1 object each | +| Buttons | 2 components each | 1 component each | 1 object each | +| State transitions | Full tree swap | Data model update only | N/A (server concern) | +| ID management | Manual (30 IDs) | Manual (24 IDs) | Auto-generated | +| Error surface | High (ID typos) | Medium | Low | +| LLM token cost | ~4.2 KB | ~3.5 KB | ~1.0 KB | + +## How to View + +Open `index.html` in a browser. Use tabs to switch between: +- **Side by Side** — Both renderers showing identical UI +- **v0.9.1 JSON** — The raw JSON for the v0.9.1 version +- **Helper Output** — The JSON generated by the helper library (v0.9.0-compatible) + +Click **Approve** to see the `visible` binding in action — buttons hide, spinner appears. Only works on the v0.9.1 (left) panel since the helper currently generates v0.9.0 output. + +## Files + +- `authorize-transfer.json` — v0.9.1 A2UI messages (hand-written) +- `authorize-transfer-with-helper.js` — Same surface via helper library (~35 lines) +- `index.html` — Renderer supporting v0.9.1 features with tabbed comparison view diff --git a/samples/a2h-prototypes/p1-approval-v0.9.1-with-helper/authorize-transfer-with-helper.js b/samples/a2h-prototypes/p1-approval-v0.9.1-with-helper/authorize-transfer-with-helper.js new file mode 100644 index 000000000..8810b3325 --- /dev/null +++ b/samples/a2h-prototypes/p1-approval-v0.9.1-with-helper/authorize-transfer-with-helper.js @@ -0,0 +1,64 @@ +/** + * authorize-transfer-with-helper.js + * + * Demonstrates generating the same approval surface using the a2h-a2ui helper library. + * Compare: ~10 lines of helper code vs ~100+ lines of hand-written JSON (v0.9.0) + * or ~80 lines of v0.9.1 JSON. + */ + +import { createAuthorizeSurface, toJsonl } from '../lib/a2h-a2ui.js'; + +// --- The entire surface definition in ~15 lines --- + +const messages = createAuthorizeSurface({ + surfaceId: 'a2h-authorize-transfer-001', + title: 'Authorization Required', + description: 'Your financial agent wants to transfer funds between your accounts.', + details: [ + { label: 'Action', path: '/transfer/action' }, + { label: 'From', path: '/transfer/fromAccount' }, + { label: 'To', path: '/transfer/toAccount' }, + { label: 'Amount', path: '/transfer/amount' }, + ], + actions: [ + { id: 'reject-btn', label: 'Reject', variant: 'default', event: 'a2h.authorize.reject' }, + { id: 'approve-btn', label: 'Approve', variant: 'primary', event: 'a2h.authorize.approve' }, + ], + dataModel: { + state: 'pending', + transfer: { + description: 'Your financial agent wants to transfer funds between your accounts.', + action: 'Transfer Funds', + fromAccount: 'Checking (****4521)', + toAccount: 'Savings (****7890)', + amount: '$500.00', + }, + meta: { + interactionId: 'txn-2026-0304-001', + agentId: 'financial-assistant-v2', + intent: 'AUTHORIZE', + timestamp: '2026-03-04T06:45:00Z', + ttlSeconds: 300, + }, + }, +}); + +// Output as JSONL or JSON array +console.log(JSON.stringify(messages, null, 2)); + +// --- Comparison --- +// +// v0.9.0 hand-written JSON: ~120 lines, 30 components, 4.2 KB +// v0.9.1 hand-written JSON: ~90 lines, 24 components, 3.5 KB +// Helper library call: ~35 lines (including data model), generates equivalent output +// +// The helper: +// - Generates correct component IDs automatically +// - Wires up the Card → Column → [header, divider, body, divider, actions] skeleton +// - Creates detail rows with proper label+value bindings +// - Sets up button actions with event names +// - Returns the standard 3-message sequence (createSurface, updateDataModel, updateComponents) +// +// Note: The helper currently generates v0.9.0-compatible output (Text children for buttons, +// Row+Text+Text for details). A v0.9.1-aware version would use KeyValue and Button.label +// for even shorter output. The helper abstracts this — users don't need to know the difference. diff --git a/samples/a2h-prototypes/p1-approval-v0.9.1-with-helper/authorize-transfer.json b/samples/a2h-prototypes/p1-approval-v0.9.1-with-helper/authorize-transfer.json new file mode 100644 index 000000000..b1b5a98c8 --- /dev/null +++ b/samples/a2h-prototypes/p1-approval-v0.9.1-with-helper/authorize-transfer.json @@ -0,0 +1,192 @@ +[ + { + "version": "v0.9.1", + "createSurface": { + "surfaceId": "a2h-authorize-transfer-001", + "catalogId": "https://a2ui.org/specification/v0_9_1/basic_catalog.json", + "sendDataModel": true + } + }, + { + "version": "v0.9.1", + "updateComponents": { + "surfaceId": "a2h-authorize-transfer-001", + "components": [ + { + "id": "root", + "component": "Card", + "child": "main-column" + }, + { + "id": "main-column", + "component": "Column", + "children": [ + "header-row", + "divider-1", + "description", + "details-card", + "ttl-text", + "divider-2", + "actions", + "processing" + ] + }, + { + "id": "header-row", + "component": "Row", + "children": ["header-icon", "header-text"], + "align": "center" + }, + { + "id": "header-icon", + "component": "Icon", + "name": "lock" + }, + { + "id": "header-text", + "component": "Text", + "text": "Authorization Required", + "variant": "h3" + }, + { + "id": "divider-1", + "component": "Divider" + }, + { + "id": "description", + "component": "Text", + "text": { "path": "/transfer/description" }, + "variant": "body" + }, + { + "id": "details-card", + "component": "Card", + "child": "details-column" + }, + { + "id": "details-column", + "component": "Column", + "children": [ + "detail-action", + "detail-from", + "detail-to", + "detail-amount" + ] + }, + { + "id": "detail-action", + "component": "KeyValue", + "label": "Action", + "value": { "path": "/transfer/action" } + }, + { + "id": "detail-from", + "component": "KeyValue", + "label": "From", + "value": { "path": "/transfer/fromAccount" } + }, + { + "id": "detail-to", + "component": "KeyValue", + "label": "To", + "value": { "path": "/transfer/toAccount" } + }, + { + "id": "detail-amount", + "component": "KeyValue", + "label": "Amount", + "value": { "path": "/transfer/amount" } + }, + { + "id": "ttl-text", + "component": "Text", + "text": "⏱ Expires in 5 minutes", + "variant": "caption", + "visible": { "fn": "equals", "args": [{ "path": "/state" }, "pending"] } + }, + { + "id": "divider-2", + "component": "Divider", + "visible": { "fn": "equals", "args": [{ "path": "/state" }, "pending"] } + }, + { + "id": "actions", + "component": "Row", + "children": ["reject-btn", "approve-btn"], + "justify": "end", + "visible": { "fn": "equals", "args": [{ "path": "/state" }, "pending"] } + }, + { + "id": "reject-btn", + "component": "Button", + "label": "Reject", + "action": { + "event": { + "name": "a2h.authorize.reject", + "context": { + "interactionId": { "path": "/meta/interactionId" }, + "intent": "AUTHORIZE" + } + } + } + }, + { + "id": "approve-btn", + "component": "Button", + "label": "Approve", + "variant": "primary", + "action": { + "event": { + "name": "a2h.authorize.approve", + "context": { + "interactionId": { "path": "/meta/interactionId" }, + "intent": "AUTHORIZE" + } + } + } + }, + { + "id": "processing", + "component": "Row", + "children": ["processing-indicator", "processing-text"], + "align": "center", + "visible": { "fn": "equals", "args": [{ "path": "/state" }, "processing"] } + }, + { + "id": "processing-indicator", + "component": "ProgressIndicator", + "mode": "indeterminate" + }, + { + "id": "processing-text", + "component": "Text", + "text": "Processing your authorization...", + "variant": "body" + } + ] + } + }, + { + "version": "v0.9.1", + "updateDataModel": { + "surfaceId": "a2h-authorize-transfer-001", + "value": { + "state": "pending", + "transfer": { + "description": "Your financial agent wants to transfer funds between your accounts.", + "action": "Transfer Funds", + "fromAccount": "Checking (****4521)", + "toAccount": "Savings (****7890)", + "amount": "$500.00" + }, + "meta": { + "interactionId": "txn-2026-0304-001", + "agentId": "financial-assistant-v2", + "intent": "AUTHORIZE", + "timestamp": "2026-03-04T06:45:00Z", + "ttlSeconds": 300 + } + } + } + } +] diff --git a/samples/a2h-prototypes/p1-approval-v0.9.1-with-helper/index.html b/samples/a2h-prototypes/p1-approval-v0.9.1-with-helper/index.html new file mode 100644 index 000000000..81d7278f0 --- /dev/null +++ b/samples/a2h-prototypes/p1-approval-v0.9.1-with-helper/index.html @@ -0,0 +1,410 @@ + + + + + + A2H Prototype 1 — Approval Card v0.9.1 (with Helper Comparison) + + + + + + + +

A2H × A2UI — P1: Approval Card v0.9.1 (with Helper Comparison)

+ +
+ v0.9.0: 30 components, ~120 lines  |  + v0.9.1: 24 components, ~90 lines  |  + Helper: ~35 lines of JS +
+ +
+ + + +
+ +
+
+
v0.9.1 — Raw JSON (24 components)
+
+
+
+
Helper Library (~35 lines of JS)
+
+
+
+ + + + + +

Event Log

+
+ + + + From 858f8060bbfed8965866c65e14a02e836e7b26b2 Mon Sep 17 00:00:00 2001 From: Zaf Date: Wed, 4 Mar 2026 15:50:25 +0000 Subject: [PATCH 10/22] =?UTF-8?q?fix:=20P2=20escalation=20v0.9.0=20?= =?UTF-8?q?=E2=80=94=20critical=20review=20fixes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Replace invalid Text variant 'label' with 'caption' (5 instances) Same bug as P1 — 'label' is not in the v0.9 Text variant enum - Replace invalid Icon name 'support_agent' with 'person' - Replace invalid Icon name 'priority_high' with 'warning' - Update HTML renderer CSS to match corrected component values - Remove orphaned 'label' variant CSS rule --- .../a2h-prototypes/p2-escalation-v0.9.0/README.md | 2 +- .../p2-escalation-v0.9.0/escalation-handoff.json | 14 +++++++------- .../a2h-prototypes/p2-escalation-v0.9.0/index.html | 5 ++--- 3 files changed, 10 insertions(+), 11 deletions(-) diff --git a/samples/a2h-prototypes/p2-escalation-v0.9.0/README.md b/samples/a2h-prototypes/p2-escalation-v0.9.0/README.md index 4b4cf72ff..e1678669c 100644 --- a/samples/a2h-prototypes/p2-escalation-v0.9.0/README.md +++ b/samples/a2h-prototypes/p2-escalation-v0.9.0/README.md @@ -85,4 +85,4 @@ python3 -m http.server 8080 - **Three connection methods** as equal-weight buttons rather than a dropdown — mobile-friendly, reduces taps - **Context preservation** in a nested card — visually grouped, clearly "what the AI tried" - **Single event name** (`a2h.escalate.connect`) with `method` in context — cleaner than three separate events -- **Priority as caption variant** — inherits red color from CSS, visually prominent without a separate component +- **Priority as warning icon + caption text** — icon inherits red color, emoji in data model reinforces urgency diff --git a/samples/a2h-prototypes/p2-escalation-v0.9.0/escalation-handoff.json b/samples/a2h-prototypes/p2-escalation-v0.9.0/escalation-handoff.json index 6dd8af93c..f44b049d4 100644 --- a/samples/a2h-prototypes/p2-escalation-v0.9.0/escalation-handoff.json +++ b/samples/a2h-prototypes/p2-escalation-v0.9.0/escalation-handoff.json @@ -40,7 +40,7 @@ { "id": "header-icon", "component": "Icon", - "name": "support_agent" + "name": "person" }, { "id": "header-text", @@ -78,7 +78,7 @@ "id": "context-header", "component": "Text", "text": "Conversation Summary", - "variant": "label" + "variant": "caption" }, { "id": "context-tried-row", @@ -89,7 +89,7 @@ "id": "context-tried-label", "component": "Text", "text": "Attempted:", - "variant": "label" + "variant": "caption" }, { "id": "context-tried-value", @@ -106,7 +106,7 @@ "id": "context-issue-label", "component": "Text", "text": "Issue:", - "variant": "label" + "variant": "caption" }, { "id": "context-issue-value", @@ -123,7 +123,7 @@ "id": "context-duration-label", "component": "Text", "text": "Duration:", - "variant": "label" + "variant": "caption" }, { "id": "context-duration-value", @@ -140,7 +140,7 @@ "id": "context-agent-label", "component": "Text", "text": "AI Agent:", - "variant": "label" + "variant": "caption" }, { "id": "context-agent-value", @@ -157,7 +157,7 @@ { "id": "priority-icon", "component": "Icon", - "name": "priority_high" + "name": "warning" }, { "id": "priority-text", diff --git a/samples/a2h-prototypes/p2-escalation-v0.9.0/index.html b/samples/a2h-prototypes/p2-escalation-v0.9.0/index.html index a6bb91627..bd363ab48 100644 --- a/samples/a2h-prototypes/p2-escalation-v0.9.0/index.html +++ b/samples/a2h-prototypes/p2-escalation-v0.9.0/index.html @@ -39,12 +39,11 @@ .c-Row[data-justify="center"] { justify-content: center; } .c-Divider { border: none; border-top: 1px solid #e0e0e0; margin: 0.25rem 0; } .c-Icon { font-family: 'Material Symbols Outlined'; font-size: 24px; color: #5f6368; } - .c-Icon[data-name="priority_high"] { color: #d93025; } + .c-Icon[data-name="warning"] { color: #d93025; } .c-Text { color: #202124; } .c-Text[data-variant="h3"] { font-size: 1.1rem; font-weight: 500; } .c-Text[data-variant="body"] { font-size: 0.9rem; color: #5f6368; line-height: 1.4; } - .c-Text[data-variant="label"] { font-size: 0.85rem; font-weight: 500; color: #202124; min-width: 80px; } - .c-Text[data-variant="caption"] { font-size: 0.8rem; color: #d93025; font-weight: 500; } + .c-Text[data-variant="caption"] { font-size: 0.85rem; font-weight: 500; color: #5f6368; } .c-Button { border: 1px solid #dadce0; border-radius: 20px; From 351bbf04384c91d6101bb88421b9cb78be8ceb45 Mon Sep 17 00:00:00 2001 From: Zaf Date: Wed, 4 Mar 2026 15:54:08 +0000 Subject: [PATCH 11/22] feat: P2 escalation v0.9.1-with-helper --- .../README.md | 102 +++++ .../escalation-handoff.json | 253 +++++++++++ .../escalation-with-helper.js | 67 +++ .../index.html | 421 ++++++++++++++++++ 4 files changed, 843 insertions(+) create mode 100644 samples/a2h-prototypes/p2-escalation-v0.9.1-with-helper/README.md create mode 100644 samples/a2h-prototypes/p2-escalation-v0.9.1-with-helper/escalation-handoff.json create mode 100644 samples/a2h-prototypes/p2-escalation-v0.9.1-with-helper/escalation-with-helper.js create mode 100644 samples/a2h-prototypes/p2-escalation-v0.9.1-with-helper/index.html diff --git a/samples/a2h-prototypes/p2-escalation-v0.9.1-with-helper/README.md b/samples/a2h-prototypes/p2-escalation-v0.9.1-with-helper/README.md new file mode 100644 index 000000000..56afd21f1 --- /dev/null +++ b/samples/a2h-prototypes/p2-escalation-v0.9.1-with-helper/README.md @@ -0,0 +1,102 @@ +# P2: Escalation Card — v0.9.1 with Helper Comparison + +Same escalation scenario as [v0.9.0](../p2-escalation-v0.9.0/), rewritten with proposed v0.9.1 features and compared against helper library output. + +## What the User Sees + +Identical UI in both versions — a card titled "Escalation to Human Agent" with reason, conversation summary, priority indicator, and three connection options (Chat, Callback, Schedule). **Same experience, less code.** + +v0.9.1 adds: after clicking a connection option, the buttons hide and a "Connecting you to an agent..." spinner appears — purely via data model update (no `updateComponents` needed). + +## Line Count Comparison + +| Version | Lines | Components | Size | +|---------|-------|------------|------| +| v0.9.0 JSON | ~140 | 36 | 5.8 KB | +| **v0.9.1 JSON** | **~110** | **28** | **4.5 KB** | +| **Helper JS** | **~45** | (generated) | **1.3 KB** | + +**v0.9.1 is 22% shorter than v0.9.0. The helper is 68% shorter.** + +## What Each v0.9.1 Feature Eliminated + +### 1. `KeyValue` component +**Before (v0.9.0):** 3 components per context row (Row + Text label + Text value) +```json +{ "id": "context-tried-row", "component": "Row", "children": ["context-tried-label", "context-tried-value"] }, +{ "id": "context-tried-label", "component": "Text", "text": "Attempted:", "variant": "caption" }, +{ "id": "context-tried-value", "component": "Text", "text": { "path": "/escalation/attemptedActions" }, "variant": "body" } +``` + +**After (v0.9.1):** 1 component +```json +{ "id": "context-tried", "component": "KeyValue", "label": "Attempted", "value": { "path": "/escalation/attemptedActions" } } +``` + +**Impact:** 4 context rows × 2 eliminated components = **8 fewer components**. + +### 2. Button `label` prop +**Before (v0.9.0):** 2 components per button (Text child + Button) +```json +{ "id": "chat-btn-text", "component": "Text", "text": "Connect via Chat" }, +{ "id": "chat-btn", "component": "Button", "child": "chat-btn-text", ... } +``` + +**After (v0.9.1):** 1 component +```json +{ "id": "chat-btn", "component": "Button", "label": "Connect via Chat", ... } +``` + +**Impact:** 3 buttons × 1 eliminated component = **3 fewer components**. + +### 3. `visible` binding +**Before (v0.9.0):** To show a "Connecting..." state after the user picks a connection method, the server must send an `updateComponents` message swapping the entire action section (~20+ lines over the wire). + +**After (v0.9.1):** Components declare visibility conditions upfront. The server sends only `updateDataModel` to set `"/state": "connecting"`: +```json +{ + "id": "actions-col", + "component": "Column", + "visible": { "fn": "equals", "args": [{ "path": "/state" }, "pending"] } +} +``` + +The confirmation spinner is pre-declared and hidden until the state changes: +```json +{ + "id": "confirmation", + "component": "Row", + "visible": { "fn": "equals", "args": [{ "path": "/state" }, "connecting"] } +} +``` + +**Impact:** Eliminates post-action `updateComponents` entirely. State transitions become **1 data model update** instead of a full component tree swap. + +### 4. `ProgressIndicator` component +Used here for the "Connecting you to an agent..." state with a proper spinner instead of emoji or plain text. + +## Developer Experience + +| Aspect | v0.9.0 | v0.9.1 | Helper | +|--------|--------|--------|--------| +| Context rows | 3 components each | 1 component each | 1 object each | +| Buttons | 2 components each | 1 component each | 1 object each | +| State transitions | Full tree swap | Data model update only | N/A (server concern) | +| ID management | Manual (36 IDs) | Manual (28 IDs) | Auto-generated | +| Error surface | High (ID typos) | Medium | Low | +| LLM token cost | ~5.8 KB | ~4.5 KB | ~1.3 KB | + +## How to View + +Open `index.html` in a browser. Use tabs to switch between: +- **Side by Side** — Both renderers showing identical UI +- **v0.9.1 JSON** — The raw JSON for the v0.9.1 version +- **Helper Output** — The JSON generated by the helper library (v0.9.0-compatible) + +Click any connection button to see the `visible` binding in action — buttons hide, spinner appears. Only works on the v0.9.1 (left) panel since the helper currently generates v0.9.0 output. + +## Files + +- `escalation-handoff.json` — v0.9.1 A2UI messages (hand-written) +- `escalation-with-helper.js` — Same surface via helper library (~45 lines) +- `index.html` — Renderer supporting v0.9.1 features with tabbed comparison view diff --git a/samples/a2h-prototypes/p2-escalation-v0.9.1-with-helper/escalation-handoff.json b/samples/a2h-prototypes/p2-escalation-v0.9.1-with-helper/escalation-handoff.json new file mode 100644 index 000000000..3c3bf1743 --- /dev/null +++ b/samples/a2h-prototypes/p2-escalation-v0.9.1-with-helper/escalation-handoff.json @@ -0,0 +1,253 @@ +[ + { + "version": "v0.9.1", + "createSurface": { + "surfaceId": "a2h-escalate-001", + "catalogId": "https://a2ui.org/specification/v0_9_1/basic_catalog.json", + "sendDataModel": true + } + }, + { + "version": "v0.9.1", + "updateComponents": { + "surfaceId": "a2h-escalate-001", + "components": [ + { + "id": "root", + "component": "Card", + "child": "main-col" + }, + { + "id": "main-col", + "component": "Column", + "children": [ + "header-row", + "divider-1", + "reason-text", + "context-card", + "priority-row", + "status-text", + "divider-2", + "actions-col", + "confirmation" + ] + }, + { + "id": "header-row", + "component": "Row", + "children": ["header-icon", "header-text"], + "align": "center" + }, + { + "id": "header-icon", + "component": "Icon", + "name": "person" + }, + { + "id": "header-text", + "component": "Text", + "text": "Escalation to Human Agent", + "variant": "h3" + }, + { + "id": "divider-1", + "component": "Divider" + }, + { + "id": "reason-text", + "component": "Text", + "text": { "path": "/escalation/reason" }, + "variant": "body" + }, + { + "id": "context-card", + "component": "Card", + "child": "context-col" + }, + { + "id": "context-col", + "component": "Column", + "children": [ + "context-header", + "context-tried", + "context-issue", + "context-duration", + "context-agent" + ] + }, + { + "id": "context-header", + "component": "Text", + "text": "Conversation Summary", + "variant": "caption" + }, + { + "id": "context-tried", + "component": "KeyValue", + "label": "Attempted", + "value": { "path": "/escalation/attemptedActions" } + }, + { + "id": "context-issue", + "component": "KeyValue", + "label": "Issue", + "value": { "path": "/escalation/issueCategory" } + }, + { + "id": "context-duration", + "component": "KeyValue", + "label": "Duration", + "value": { "path": "/escalation/conversationDuration" } + }, + { + "id": "context-agent", + "component": "KeyValue", + "label": "AI Agent", + "value": { "path": "/meta/agentName" } + }, + { + "id": "priority-row", + "component": "Row", + "children": ["priority-icon", "priority-text"], + "align": "center" + }, + { + "id": "priority-icon", + "component": "Icon", + "name": "warning" + }, + { + "id": "priority-text", + "component": "Text", + "text": { "path": "/escalation/priorityLabel" }, + "variant": "caption" + }, + { + "id": "status-text", + "component": "Text", + "text": { "path": "/escalation/statusMessage" }, + "variant": "body" + }, + { + "id": "divider-2", + "component": "Divider", + "visible": { "fn": "equals", "args": [{ "path": "/state" }, "pending"] } + }, + { + "id": "actions-col", + "component": "Column", + "children": ["actions-label", "actions-row"], + "visible": { "fn": "equals", "args": [{ "path": "/state" }, "pending"] } + }, + { + "id": "actions-label", + "component": "Text", + "text": "How would you like to connect?", + "variant": "body" + }, + { + "id": "actions-row", + "component": "Row", + "children": ["chat-btn", "callback-btn", "schedule-btn"], + "justify": "center" + }, + { + "id": "chat-btn", + "component": "Button", + "label": "Connect via Chat", + "variant": "primary", + "action": { + "event": { + "name": "a2h.escalate.connect", + "context": { + "interactionId": { "path": "/meta/interactionId" }, + "method": "chat", + "intent": "ESCALATE" + } + } + } + }, + { + "id": "callback-btn", + "component": "Button", + "label": "Request Callback", + "action": { + "event": { + "name": "a2h.escalate.connect", + "context": { + "interactionId": { "path": "/meta/interactionId" }, + "method": "callback", + "intent": "ESCALATE" + } + } + } + }, + { + "id": "schedule-btn", + "component": "Button", + "label": "Schedule Appointment", + "action": { + "event": { + "name": "a2h.escalate.connect", + "context": { + "interactionId": { "path": "/meta/interactionId" }, + "method": "appointment", + "intent": "ESCALATE" + } + } + } + }, + { + "id": "confirmation", + "component": "Row", + "children": ["confirm-indicator", "confirm-text"], + "align": "center", + "visible": { "fn": "equals", "args": [{ "path": "/state" }, "connecting"] } + }, + { + "id": "confirm-indicator", + "component": "ProgressIndicator", + "mode": "indeterminate" + }, + { + "id": "confirm-text", + "component": "Text", + "text": "Connecting you to an agent...", + "variant": "body" + } + ] + } + }, + { + "version": "v0.9.1", + "updateDataModel": { + "surfaceId": "a2h-escalate-001", + "value": { + "state": "pending", + "escalation": { + "reason": "I wasn't able to resolve your billing dispute. The charge of $89.99 on Feb 28 requires manual review by our billing team. I'm connecting you with a specialist who can help.", + "attemptedActions": "Checked transaction history, verified charge details, attempted automated refund (declined — requires manual review)", + "issueCategory": "Billing Dispute — Unauthorized Charge", + "conversationDuration": "8 minutes (12 messages)", + "priority": "high", + "priorityLabel": "⚡ High Priority — Billing dispute, potential unauthorized charge", + "statusMessage": "Estimated wait: ~2 minutes for chat, ~15 minutes for callback", + "conversationContext": [ + { "role": "user", "summary": "Reported unexpected $89.99 charge from 'DGTL-SVC' on Feb 28" }, + { "role": "agent", "summary": "Identified charge, attempted automated dispute — system requires human review" }, + { "role": "user", "summary": "Confirmed they did not authorize the charge" } + ] + }, + "meta": { + "interactionId": "esc-2026-0304-042", + "agentId": "customer-support-v3", + "agentName": "Support Assistant (AI)", + "intent": "ESCALATE", + "timestamp": "2026-03-04T06:48:00Z", + "department": "billing", + "queuePosition": 3 + } + } + } + } +] diff --git a/samples/a2h-prototypes/p2-escalation-v0.9.1-with-helper/escalation-with-helper.js b/samples/a2h-prototypes/p2-escalation-v0.9.1-with-helper/escalation-with-helper.js new file mode 100644 index 000000000..c82734ef3 --- /dev/null +++ b/samples/a2h-prototypes/p2-escalation-v0.9.1-with-helper/escalation-with-helper.js @@ -0,0 +1,67 @@ +/** + * escalation-with-helper.js + * + * Demonstrates generating the same escalation surface using the a2h-a2ui helper library. + * Compare: ~40 lines of helper code vs ~140 lines of hand-written JSON (v0.9.0) + * or ~110 lines of v0.9.1 JSON. + */ + +import { createEscalateSurface, toJsonl } from '../lib/a2h-a2ui.js'; + +// --- The entire surface definition in ~30 lines --- + +const messages = createEscalateSurface({ + surfaceId: 'a2h-escalate-001', + reason: "I wasn't able to resolve your billing dispute. The charge of $89.99 on Feb 28 requires manual review by our billing team. I'm connecting you with a specialist who can help.", + context: "Checked transaction history, verified charge details, attempted automated refund (declined — requires manual review)", + options: [ + { id: 'chat-btn', label: '💬 Connect via Chat', event: 'a2h.escalate.connect' }, + { id: 'callback-btn', label: '📞 Request Callback', event: 'a2h.escalate.connect' }, + { id: 'schedule-btn', label: '📅 Schedule Appointment', event: 'a2h.escalate.connect' }, + ], + dataModel: { + escalation: { + reason: "I wasn't able to resolve your billing dispute. The charge of $89.99 on Feb 28 requires manual review by our billing team. I'm connecting you with a specialist who can help.", + attemptedActions: "Checked transaction history, verified charge details, attempted automated refund (declined — requires manual review)", + issueCategory: "Billing Dispute — Unauthorized Charge", + conversationDuration: "8 minutes (12 messages)", + priority: "high", + priorityLabel: "⚡ High Priority — Billing dispute, potential unauthorized charge", + statusMessage: "Estimated wait: ~2 minutes for chat, ~15 minutes for callback", + conversationContext: [ + { role: "user", summary: "Reported unexpected $89.99 charge from 'DGTL-SVC' on Feb 28" }, + { role: "agent", summary: "Identified charge, attempted automated dispute — system requires human review" }, + { role: "user", summary: "Confirmed they did not authorize the charge" }, + ], + }, + meta: { + interactionId: "esc-2026-0304-042", + agentId: "customer-support-v3", + agentName: "Support Assistant (AI)", + intent: "ESCALATE", + timestamp: "2026-03-04T06:48:00Z", + department: "billing", + queuePosition: 3, + }, + }, +}); + +// Output as JSON array +console.log(JSON.stringify(messages, null, 2)); + +// --- Comparison --- +// +// v0.9.0 hand-written JSON: ~140 lines, 36 components, 5.8 KB +// v0.9.1 hand-written JSON: ~110 lines, 28 components, 4.5 KB +// Helper library call: ~45 lines (including data model), generates equivalent output +// +// The helper: +// - Generates correct component IDs automatically +// - Wires up the Card → Column → [header, divider, body, divider, actions] skeleton +// - Creates the reason text, context card, and status binding +// - Sets up connection option buttons with event names +// - Returns the standard 3-message sequence (createSurface, updateDataModel, updateComponents) +// +// Note: The helper generates v0.9.0-compatible output (Text children for buttons, +// Row+Text+Text for context details). The v0.9.1 hand-written version uses KeyValue +// and Button.label for cleaner output. The helper abstracts this — users don't care. diff --git a/samples/a2h-prototypes/p2-escalation-v0.9.1-with-helper/index.html b/samples/a2h-prototypes/p2-escalation-v0.9.1-with-helper/index.html new file mode 100644 index 000000000..24d7d505b --- /dev/null +++ b/samples/a2h-prototypes/p2-escalation-v0.9.1-with-helper/index.html @@ -0,0 +1,421 @@ + + + + + + A2H Prototype 2 — Escalation Card v0.9.1 (with Helper Comparison) + + + + + + + +

A2H × A2UI — P2: Escalation Card v0.9.1 (with Helper Comparison)

+ +
+ v0.9.0: 36 components, ~140 lines  |  + v0.9.1: 28 components, ~110 lines  |  + Helper: ~45 lines of JS +
+ +
+ + + +
+ +
+
+
v0.9.1 — Raw JSON (28 components)
+
+
+
+
Helper Library (~45 lines of JS)
+
+
+
+ + + + + +

Event Log

+
+ + + + From cadd34ffda2b6564a435a12b711d0a73572189c2 Mon Sep 17 00:00:00 2001 From: Zaf Date: Wed, 4 Mar 2026 15:56:14 +0000 Subject: [PATCH 12/22] =?UTF-8?q?fix:=20P3=20guided-input=20v0.9.0=20?= =?UTF-8?q?=E2=80=94=20critical=20review=20fixes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix variant: 'label' bug on speed-label Text (not in enum) → 'caption' - Replace non-existent MultipleChoice with ChoicePicker component - Add variant: 'mutuallyExclusive', displayStyle: 'chips' - Update data model speed value to string array per ChoicePicker spec - Fix Icon name 'local_shipping' (not in catalog enum) → 'shoppingCart' - Add icon name mapping in renderer (camelCase catalog → snake_case Material Symbols) - Update CSS classes from c-MultipleChoice to c-ChoicePicker - Fix README cd path to match actual directory name - Update README to reference ChoicePicker instead of MultipleChoice --- .../p3-guided-input-v0.9.0/README.md | 4 +- .../collect-shipping.json | 11 +++-- .../p3-guided-input-v0.9.0/index.html | 47 ++++++++++++++----- 3 files changed, 44 insertions(+), 18 deletions(-) diff --git a/samples/a2h-prototypes/p3-guided-input-v0.9.0/README.md b/samples/a2h-prototypes/p3-guided-input-v0.9.0/README.md index f08e133fb..caf2b3eeb 100644 --- a/samples/a2h-prototypes/p3-guided-input-v0.9.0/README.md +++ b/samples/a2h-prototypes/p3-guided-input-v0.9.0/README.md @@ -15,7 +15,7 @@ The agent needs structured data from the user — in this case, shipping details ```bash # Any static file server works -cd samples/a2h-prototypes/p3-guided-input +cd samples/a2h-prototypes/p3-guided-input-v0.9.0 python3 -m http.server 8080 # Open http://localhost:8080 ``` @@ -26,7 +26,7 @@ The COLLECT intent translates to a surface with: 1. **`createSurface`** with `sendDataModel: true` — tells the host that when an event fires, the entire data model should be included in the payload sent back to the agent. 2. **`TextField` components** with `value: {"path": "/shipping/name"}` — each field reads from and writes to a path in the data model. This is two-way binding: pre-populated values show up, user edits flow back. -3. **`MultipleChoice`** for constrained selections (delivery speed) — also data-bound. +3. **`ChoicePicker`** for constrained selections (delivery speed) — also data-bound, with `variant: "mutuallyExclusive"` and `displayStyle: "chips"`. 4. **`Button`** with an event action — on click, the renderer collects the current data model and sends it as the event payload. 5. **`updateDataModel`** pre-populates known info (saved address from user profile). diff --git a/samples/a2h-prototypes/p3-guided-input-v0.9.0/collect-shipping.json b/samples/a2h-prototypes/p3-guided-input-v0.9.0/collect-shipping.json index d9de0fe45..6040c8702 100644 --- a/samples/a2h-prototypes/p3-guided-input-v0.9.0/collect-shipping.json +++ b/samples/a2h-prototypes/p3-guided-input-v0.9.0/collect-shipping.json @@ -45,7 +45,7 @@ { "id": "header-icon", "component": "Icon", - "name": "local_shipping" + "name": "shoppingCart" }, { "id": "header-text", @@ -106,12 +106,15 @@ "id": "speed-label", "component": "Text", "text": "Delivery speed", - "variant": "label" + "variant": "caption" }, { "id": "speed-choice", - "component": "MultipleChoice", + "component": "ChoicePicker", + "label": "Delivery speed", "value": { "path": "/shipping/speed" }, + "variant": "mutuallyExclusive", + "displayStyle": "chips", "options": [ { "label": "Standard (5–7 days)", "value": "standard" }, { "label": "Express (2–3 days)", "value": "express" }, @@ -157,7 +160,7 @@ "state": "IL", "zip": "62704", "phone": "", - "speed": "standard" + "speed": ["standard"] } } } diff --git a/samples/a2h-prototypes/p3-guided-input-v0.9.0/index.html b/samples/a2h-prototypes/p3-guided-input-v0.9.0/index.html index ebb4726b4..8f60bf5eb 100644 --- a/samples/a2h-prototypes/p3-guided-input-v0.9.0/index.html +++ b/samples/a2h-prototypes/p3-guided-input-v0.9.0/index.html @@ -62,13 +62,13 @@ } .c-TextField input:focus { border-color: #1a73e8; } - .c-MultipleChoice { + .c-ChoicePicker { display: flex; flex-direction: row; gap: 0.5rem; flex-wrap: wrap; } - .c-MultipleChoice .choice-chip { + .c-ChoicePicker .choice-chip { border: 1px solid #dadce0; border-radius: 20px; padding: 0.45rem 1rem; @@ -79,8 +79,8 @@ color: #202124; transition: all 0.15s; } - .c-MultipleChoice .choice-chip:hover { background: #f1f3f4; } - .c-MultipleChoice .choice-chip.selected { + .c-ChoicePicker .choice-chip:hover { background: #f1f3f4; } + .c-ChoicePicker .choice-chip.selected { background: #e8f0fe; border-color: #1a73e8; color: #1a73e8; @@ -196,9 +196,23 @@

Event Log (sendDataModel payload)

} case 'Divider': return document.createElement('hr'); case 'Icon': { + const iconMap = { + shoppingCart: 'shopping_cart', locationOn: 'location_on', + accountCircle: 'account_circle', arrowBack: 'arrow_back', + arrowForward: 'arrow_forward', attachFile: 'attach_file', + calendarToday: 'calendar_today', fastForward: 'fast_forward', + favoriteOff: 'favorite_border', lockOpen: 'lock_open', + moreVert: 'more_vert', moreHoriz: 'more_horiz', + notificationsOff: 'notifications_off', skipNext: 'skip_next', + skipPrevious: 'skip_previous', starHalf: 'star_half', + starOff: 'star_border', volumeDown: 'volume_down', + volumeMute: 'volume_mute', volumeOff: 'volume_off', + volumeUp: 'volume_up', visibilityOff: 'visibility_off', + }; const el = document.createElement('span'); el.className = 'c-Icon'; - el.textContent = resolve(comp.name); + const name = resolve(comp.name); + el.textContent = iconMap[name] || name; return el; } case 'Text': { @@ -223,18 +237,27 @@

Event Log (sendDataModel payload)

wrapper.appendChild(input); return wrapper; } - case 'MultipleChoice': { + case 'ChoicePicker': { const wrap = document.createElement('div'); - wrap.className = 'c-MultipleChoice'; - const currentVal = resolve(comp.value); + wrap.className = 'c-ChoicePicker'; + const currentVal = resolve(comp.value) || []; + const isMulti = comp.variant === 'multipleSelection'; for (const opt of (comp.options || [])) { const chip = document.createElement('button'); - chip.className = 'choice-chip' + (opt.value === currentVal ? ' selected' : ''); + chip.className = 'choice-chip' + (currentVal.includes(opt.value) ? ' selected' : ''); chip.textContent = opt.label; chip.addEventListener('click', () => { - if (comp.value?.path) setPath(dataModel, comp.value.path, opt.value); - wrap.querySelectorAll('.choice-chip').forEach(c => c.classList.remove('selected')); - chip.classList.add('selected'); + if (comp.value?.path) { + let arr = getPath(dataModel, comp.value.path) || []; + if (isMulti) { + arr = arr.includes(opt.value) ? arr.filter(v => v !== opt.value) : [...arr, opt.value]; + } else { + arr = [opt.value]; + wrap.querySelectorAll('.choice-chip').forEach(c => c.classList.remove('selected')); + } + setPath(dataModel, comp.value.path, arr); + chip.classList.toggle('selected', arr.includes(opt.value)); + } }); wrap.appendChild(chip); } From 0185b5f3444b0a045d878c7c3b084bd957763bb0 Mon Sep 17 00:00:00 2001 From: Zaf Date: Wed, 4 Mar 2026 15:59:49 +0000 Subject: [PATCH 13/22] feat: P3 guided-input v0.9.1-with-helper --- .../README.md | 104 ++++ .../collect-shipping.json | 354 ++++++++++++ .../collect-with-helper.js | 71 +++ .../index.html | 517 ++++++++++++++++++ 4 files changed, 1046 insertions(+) create mode 100644 samples/a2h-prototypes/p3-guided-input-v0.9.1-with-helper/README.md create mode 100644 samples/a2h-prototypes/p3-guided-input-v0.9.1-with-helper/collect-shipping.json create mode 100644 samples/a2h-prototypes/p3-guided-input-v0.9.1-with-helper/collect-with-helper.js create mode 100644 samples/a2h-prototypes/p3-guided-input-v0.9.1-with-helper/index.html diff --git a/samples/a2h-prototypes/p3-guided-input-v0.9.1-with-helper/README.md b/samples/a2h-prototypes/p3-guided-input-v0.9.1-with-helper/README.md new file mode 100644 index 000000000..2227dc416 --- /dev/null +++ b/samples/a2h-prototypes/p3-guided-input-v0.9.1-with-helper/README.md @@ -0,0 +1,104 @@ +# P3: Guided Input Form — v0.9.1 with Helper Comparison + +Same shipping form scenario as [v0.9.0](../p3-guided-input-v0.9.0/), rewritten with v0.9.1 features and compared against helper library output. + +## What the User Sees + +A shipping address form with pre-populated fields, delivery speed chips, and a **review-before-submit** flow: + +1. **Editing** — Fill out the form, click "Review & Submit" +2. **Reviewing** — Form fields hide, KeyValue summary appears with Edit / Submit buttons +3. **Processing** — Spinner with "Submitting..." text +4. **Success** — Confirmation message with checkmark icon + +All four states live in the same component tree. Transitions are driven entirely by `updateDataModel` (setting `/state`), with no `updateComponents` needed. + +## Line Count Comparison + +| Version | Lines | Components | Size | States | +|---------|-------|------------|------|--------| +| v0.9.0 JSON | ~90 | 22 | 3.8 KB | 1 (editing only) | +| **v0.9.1 JSON** | **~140** | **45** | **5.5 KB** | **4 (edit → review → processing → success)** | +| **Helper JS** | **~25** | (generated) | **1.0 KB** | 1 (editing only) | + +The v0.9.1 version has more components because it defines **four complete UI states** upfront. The v0.9.0 version only shows the editing state — review/processing/success would require additional `updateComponents` messages from the server. + +**Per-state**, v0.9.1 is more efficient: the review summary uses 7 KeyValue components instead of the 21 components (7 × Row + Text + Text) that v0.9.0 would require. + +## What Each v0.9.1 Feature Does Here + +### 1. `visible` binding — Review-before-submit pattern + +The key innovation. Four top-level Column sections each have a `visible` condition: + +```json +{ "id": "form-section", "visible": { "fn": "equals", "args": [{ "path": "/state" }, "editing"] } } +{ "id": "review-section", "visible": { "fn": "equals", "args": [{ "path": "/state" }, "reviewing"] } } +{ "id": "processing-section", "visible": { "fn": "equals", "args": [{ "path": "/state" }, "processing"] } } +{ "id": "success-section", "visible": { "fn": "equals", "args": [{ "path": "/state" }, "success"] } } +``` + +State transitions require only: `updateDataModel { "state": "reviewing" }`. No component tree manipulation. + +**Impact:** Eliminates 3 separate `updateComponents` messages that v0.9.0 would need for the review/processing/success states. + +### 2. `KeyValue` component — Review summary + +The review section shows 7 shipping detail rows using KeyValue instead of Row+Text+Text: + +```json +{ "id": "review-name", "component": "KeyValue", "label": "Name", "value": { "path": "/shipping/name" } } +``` + +**Impact:** 7 components instead of 21. **14 fewer components** in the review section alone. + +### 3. Button `label` prop — Cleaner buttons + +All four buttons (Review & Submit, Edit, Submit, and the v0.9.0 submit) use `label` instead of a Text child: + +```json +{ "id": "submit-btn", "component": "Button", "label": "Submit", "variant": "primary", ... } +``` + +**Impact:** 4 fewer components (no Text children for buttons). + +### 4. `ProgressIndicator` — Processing state + +After submit, a proper spinner replaces the form instead of emoji: + +```json +{ "id": "processing-indicator", "component": "ProgressIndicator", "mode": "indeterminate" } +``` + +**Impact:** Professional processing UX with a single component. + +## Developer Experience + +| Aspect | v0.9.0 | v0.9.1 | Helper | +|--------|--------|--------|--------| +| Multi-state UI | Server sends new trees | Declare upfront, toggle with data | N/A (basic form only) | +| Review summary | 3 components per row | 1 KeyValue per row | N/A | +| Buttons | 2 components each | 1 component each | 1 object each | +| Processing state | Emoji or text swap | ProgressIndicator | N/A | +| Round-trips for full flow | 4 (edit + review + processing + success) | 1 (initial) + data updates | 1 (initial) | + +## How to View + +```bash +cd samples/a2h-prototypes/p3-guided-input-v0.9.1-with-helper +python3 -m http.server 8080 +# Open http://localhost:8080 +``` + +Use tabs to switch between: +- **Side by Side** — v0.9.1 (left, with review flow) and helper output (right, basic form) +- **v0.9.1 JSON** — The raw JSON for the v0.9.1 version +- **Helper Output** — The JSON generated by the helper library + +Click **Review & Submit** on the left panel to see the visible binding in action — form hides, summary appears. Click **Edit** to go back. Click **Submit** to see processing → success. + +## Files + +- `collect-shipping.json` — v0.9.1 A2UI messages with review-before-submit pattern +- `collect-with-helper.js` — Same basic form via helper library (~25 lines) +- `index.html` — Renderer with v0.9.1 features and tabbed comparison view diff --git a/samples/a2h-prototypes/p3-guided-input-v0.9.1-with-helper/collect-shipping.json b/samples/a2h-prototypes/p3-guided-input-v0.9.1-with-helper/collect-shipping.json new file mode 100644 index 000000000..a61d05160 --- /dev/null +++ b/samples/a2h-prototypes/p3-guided-input-v0.9.1-with-helper/collect-shipping.json @@ -0,0 +1,354 @@ +[ + { + "version": "v0.9.1", + "createSurface": { + "surfaceId": "a2h-collect-shipping-001", + "catalogId": "https://a2ui.org/specification/v0_9_1/basic_catalog.json", + "sendDataModel": true + } + }, + { + "version": "v0.9.1", + "updateComponents": { + "surfaceId": "a2h-collect-shipping-001", + "components": [ + { + "id": "root", + "component": "Card", + "child": "main-col" + }, + { + "id": "main-col", + "component": "Column", + "children": [ + "form-section", + "review-section", + "processing-section", + "success-section" + ] + }, + + { + "id": "form-section", + "component": "Column", + "visible": { "fn": "equals", "args": [{ "path": "/state" }, "editing"] }, + "children": [ + "header-row", + "subtitle", + "divider-top", + "name-field", + "street-field", + "city-state-row", + "zip-field", + "phone-field", + "divider-mid", + "speed-label", + "speed-choice", + "divider-bot", + "review-btn-row" + ] + }, + { + "id": "header-row", + "component": "Row", + "children": ["header-icon", "header-text"], + "align": "center" + }, + { + "id": "header-icon", + "component": "Icon", + "name": "shoppingCart" + }, + { + "id": "header-text", + "component": "Text", + "text": "Where should we ship your order?", + "variant": "h3" + }, + { + "id": "subtitle", + "component": "Text", + "text": "We'll confirm the address before charging your card.", + "variant": "body" + }, + { "id": "divider-top", "component": "Divider" }, + { + "id": "name-field", + "component": "TextField", + "label": "Full name", + "value": { "path": "/shipping/name" } + }, + { + "id": "street-field", + "component": "TextField", + "label": "Street address", + "value": { "path": "/shipping/street" } + }, + { + "id": "city-state-row", + "component": "Row", + "children": ["city-field", "state-field"] + }, + { + "id": "city-field", + "component": "TextField", + "label": "City", + "value": { "path": "/shipping/city" } + }, + { + "id": "state-field", + "component": "TextField", + "label": "State", + "value": { "path": "/shipping/state" } + }, + { + "id": "zip-field", + "component": "TextField", + "label": "ZIP code", + "value": { "path": "/shipping/zip" } + }, + { + "id": "phone-field", + "component": "TextField", + "label": "Phone number", + "value": { "path": "/shipping/phone" } + }, + { "id": "divider-mid", "component": "Divider" }, + { + "id": "speed-label", + "component": "Text", + "text": "Delivery speed", + "variant": "caption" + }, + { + "id": "speed-choice", + "component": "ChoicePicker", + "label": "Delivery speed", + "value": { "path": "/shipping/speed" }, + "variant": "mutuallyExclusive", + "displayStyle": "chips", + "options": [ + { "label": "Standard (5–7 days)", "value": "standard" }, + { "label": "Express (2–3 days)", "value": "express" }, + { "label": "Overnight", "value": "overnight" } + ] + }, + { "id": "divider-bot", "component": "Divider" }, + { + "id": "review-btn-row", + "component": "Row", + "children": ["review-btn"], + "justify": "end" + }, + { + "id": "review-btn", + "component": "Button", + "label": "Review & Submit", + "variant": "primary", + "action": { + "event": { + "name": "a2h.collect.review", + "context": {} + } + } + }, + + { + "id": "review-section", + "component": "Column", + "visible": { "fn": "equals", "args": [{ "path": "/state" }, "reviewing"] }, + "children": [ + "review-header-row", + "review-subtitle", + "divider-review-top", + "review-details-card", + "divider-review-bot", + "review-actions" + ] + }, + { + "id": "review-header-row", + "component": "Row", + "children": ["review-header-icon", "review-header-text"], + "align": "center" + }, + { + "id": "review-header-icon", + "component": "Icon", + "name": "factCheck" + }, + { + "id": "review-header-text", + "component": "Text", + "text": "Review your shipping details", + "variant": "h3" + }, + { + "id": "review-subtitle", + "component": "Text", + "text": "Please confirm everything looks correct before submitting.", + "variant": "body" + }, + { "id": "divider-review-top", "component": "Divider" }, + { + "id": "review-details-card", + "component": "Card", + "child": "review-details-col" + }, + { + "id": "review-details-col", + "component": "Column", + "children": [ + "review-name", + "review-street", + "review-city", + "review-state", + "review-zip", + "review-phone", + "review-speed" + ] + }, + { + "id": "review-name", + "component": "KeyValue", + "label": "Name", + "value": { "path": "/shipping/name" } + }, + { + "id": "review-street", + "component": "KeyValue", + "label": "Street", + "value": { "path": "/shipping/street" } + }, + { + "id": "review-city", + "component": "KeyValue", + "label": "City", + "value": { "path": "/shipping/city" } + }, + { + "id": "review-state", + "component": "KeyValue", + "label": "State", + "value": { "path": "/shipping/state" } + }, + { + "id": "review-zip", + "component": "KeyValue", + "label": "ZIP", + "value": { "path": "/shipping/zip" } + }, + { + "id": "review-phone", + "component": "KeyValue", + "label": "Phone", + "value": { "path": "/shipping/phone" } + }, + { + "id": "review-speed", + "component": "KeyValue", + "label": "Delivery", + "value": { "path": "/shipping/speedLabel" } + }, + { "id": "divider-review-bot", "component": "Divider" }, + { + "id": "review-actions", + "component": "Row", + "children": ["edit-btn", "submit-btn"], + "justify": "end" + }, + { + "id": "edit-btn", + "component": "Button", + "label": "Edit", + "action": { + "event": { + "name": "a2h.collect.edit", + "context": {} + } + } + }, + { + "id": "submit-btn", + "component": "Button", + "label": "Submit", + "variant": "primary", + "action": { + "event": { + "name": "a2h.collect.submit", + "context": {} + } + } + }, + + { + "id": "processing-section", + "component": "Row", + "visible": { "fn": "equals", "args": [{ "path": "/state" }, "processing"] }, + "children": ["processing-indicator", "processing-text"], + "align": "center" + }, + { + "id": "processing-indicator", + "component": "ProgressIndicator", + "mode": "indeterminate" + }, + { + "id": "processing-text", + "component": "Text", + "text": "Submitting your shipping details...", + "variant": "body" + }, + + { + "id": "success-section", + "component": "Column", + "visible": { "fn": "equals", "args": [{ "path": "/state" }, "success"] }, + "children": ["success-header-row", "success-message"] + }, + { + "id": "success-header-row", + "component": "Row", + "children": ["success-icon", "success-text"], + "align": "center" + }, + { + "id": "success-icon", + "component": "Icon", + "name": "checkCircle" + }, + { + "id": "success-text", + "component": "Text", + "text": "Shipping details confirmed!", + "variant": "h3" + }, + { + "id": "success-message", + "component": "Text", + "text": "Your order will be shipped to the address above. You'll receive a tracking number by email.", + "variant": "body" + } + ] + } + }, + { + "version": "v0.9.1", + "updateDataModel": { + "surfaceId": "a2h-collect-shipping-001", + "value": { + "state": "editing", + "shipping": { + "name": "Jane Doe", + "street": "742 Evergreen Terrace", + "city": "Springfield", + "state": "IL", + "zip": "62704", + "phone": "", + "speed": ["standard"], + "speedLabel": "Standard (5–7 days)" + } + } + } + } +] diff --git a/samples/a2h-prototypes/p3-guided-input-v0.9.1-with-helper/collect-with-helper.js b/samples/a2h-prototypes/p3-guided-input-v0.9.1-with-helper/collect-with-helper.js new file mode 100644 index 000000000..95b7427a0 --- /dev/null +++ b/samples/a2h-prototypes/p3-guided-input-v0.9.1-with-helper/collect-with-helper.js @@ -0,0 +1,71 @@ +/** + * collect-with-helper.js + * + * Demonstrates generating the same shipping form surface using the a2h-a2ui helper library. + * Compare: ~15 lines of helper code vs ~140+ lines of hand-written v0.9.1 JSON. + * + * Note: The helper currently generates v0.9.0-compatible output (no visible binding, + * no KeyValue, no ProgressIndicator). The v0.9.1 hand-written JSON adds: + * - Review-before-submit pattern via `visible` binding + * - KeyValue components for the review summary + * - ProgressIndicator for submit processing state + * - Button `label` prop (no Text child needed) + * + * A future v0.9.1-aware helper would generate these automatically. + */ + +import { createCollectSurface, toJsonl } from '../lib/a2h-a2ui.js'; + +// --- The entire surface definition in ~25 lines --- + +const messages = createCollectSurface({ + surfaceId: 'a2h-collect-shipping-001', + title: 'Where should we ship your order?', + fields: [ + { id: 'name', label: 'Full name', type: 'text', path: '/shipping/name', value: true }, + { id: 'street', label: 'Street address', type: 'text', path: '/shipping/street', value: true }, + { id: 'city', label: 'City', type: 'text', path: '/shipping/city', value: true }, + { id: 'state', label: 'State', type: 'text', path: '/shipping/state', value: true }, + { id: 'zip', label: 'ZIP code', type: 'text', path: '/shipping/zip', value: true }, + { id: 'phone', label: 'Phone number', type: 'text', path: '/shipping/phone', value: true }, + { id: 'speed', label: 'Delivery speed', type: 'select', path: '/shipping/speed', + options: [ + { label: 'Standard (5–7 days)', value: 'standard' }, + { label: 'Express (2–3 days)', value: 'express' }, + { label: 'Overnight', value: 'overnight' }, + ], + value: true, + }, + ], + submitLabel: 'Submit shipping info', + dataModel: { + shipping: { + name: 'Jane Doe', + street: '742 Evergreen Terrace', + city: 'Springfield', + state: 'IL', + zip: '62704', + phone: '', + speed: ['standard'], + }, + }, +}); + +// Output as JSON array +console.log(JSON.stringify(messages, null, 2)); + +// --- Comparison --- +// +// v0.9.0 hand-written JSON: ~90 lines, 22 components, 3.8 KB +// v0.9.1 hand-written JSON: ~140 lines, 45 components, 5.5 KB (includes review + processing + success states) +// Helper library call: ~25 lines (including data model), generates v0.9.0-compatible output +// +// The v0.9.1 version adds significantly more functionality: +// - "Review before submit" pattern (form hides, summary appears) +// - Processing spinner state +// - Success confirmation state +// - All driven by data model state changes, no updateComponents needed +// +// The helper generates the basic form. A v0.9.1-aware helper could add +// review/processing/success sections automatically via an option like: +// createCollectSurface({ ..., reviewBeforeSubmit: true, showProcessing: true }) diff --git a/samples/a2h-prototypes/p3-guided-input-v0.9.1-with-helper/index.html b/samples/a2h-prototypes/p3-guided-input-v0.9.1-with-helper/index.html new file mode 100644 index 000000000..ba753a84e --- /dev/null +++ b/samples/a2h-prototypes/p3-guided-input-v0.9.1-with-helper/index.html @@ -0,0 +1,517 @@ + + + + + + A2H Prototype 3 — Guided Form v0.9.1 (with Helper Comparison) + + + + + + + +

A2H × A2UI — P3: Guided Form v0.9.1 (with Helper Comparison)

+ +
+ v0.9.0: 22 components, ~90 lines  |  + v0.9.1: 45 components, ~140 lines (4 states)  |  + Helper: ~25 lines of JS +
+ +
+ + + +
+ +
+
+
v0.9.1 — Raw JSON (review + processing + success)
+
+
+
+
Helper Library (~25 lines of JS)
+
+
+
+ + + + + +

Event Log

+
+ + + + From c4e2f15fec84c4428ab899e8c9a039f25f3600cc Mon Sep 17 00:00:00 2001 From: Zaf Date: Wed, 4 Mar 2026 16:01:59 +0000 Subject: [PATCH 14/22] =?UTF-8?q?fix:=20P4=20progress-intervention=20v0.9.?= =?UTF-8?q?0=20=E2=80=94=20critical=20review=20fixes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove _comment fields from messages 3,5,7 (violates additionalProperties:false) - Fix renderer delays array to cover all 9 messages with proper progressive timing - Fix renderer labels to persist status text across dataModel+components pairs - Fix README message count (was 4, actually 9 messages in 4 logical steps) --- .../p4-progress-intervention-v0.9.0/README.md | 14 ++++++++------ .../deploy-pipeline.json | 3 --- .../p4-progress-intervention-v0.9.0/index.html | 13 ++++++++----- 3 files changed, 16 insertions(+), 14 deletions(-) diff --git a/samples/a2h-prototypes/p4-progress-intervention-v0.9.0/README.md b/samples/a2h-prototypes/p4-progress-intervention-v0.9.0/README.md index 97c8dcb21..3da53c74c 100644 --- a/samples/a2h-prototypes/p4-progress-intervention-v0.9.0/README.md +++ b/samples/a2h-prototypes/p4-progress-intervention-v0.9.0/README.md @@ -11,12 +11,14 @@ The two A2H interaction types used: ## How It Works -### Message Sequence (4 messages in `deploy-pipeline.json`) +### Message Sequence (9 messages in `deploy-pipeline.json`) -1. **createSurface + initial state** — Pipeline header, app info, four steps all pending (Build ⏳, rest ⬜) -2. **updateComponents** — Build ✅ complete (1m 12s), Test ⏳ running -3. **updateComponents** — Test ✅ passed (48/48), Stage ⏳ running -4. **updateComponents** — Stage ✅ live, Deploy ⏸️ paused. Approval card injected into the tree by updating `main-col.children` to include `approve-card`. +The file contains 9 A2UI messages grouped into 4 logical steps. Each step (after the first) sends an `updateDataModel` followed by an `updateComponents`: + +1. **Messages 0–2: Initial state** — `createSurface`, `updateDataModel` (pipeline metadata), `updateComponents` (header, app info, four steps — Build ⏳, rest ⬜) +2. **Messages 3–4: Build complete** — `updateDataModel` (build done), `updateComponents` (Build ✅ 1m 12s, Test ⏳ running) +3. **Messages 5–6: Test complete** — `updateDataModel` (test done), `updateComponents` (Test ✅ 48/48, Stage ⏳ running) +4. **Messages 7–8: Stage complete, deploy paused** — `updateDataModel` (stage done, deploy paused), `updateComponents` (Stage ✅ live, Deploy ⏸️ paused, approval card injected via `main-col.children` update) ### Key Techniques @@ -41,6 +43,6 @@ Replays the 4 messages with 2–3 second delays between steps, simulating real-t ## Files -- `deploy-pipeline.json` — A2UI v0.9 message sequence (4 messages) +- `deploy-pipeline.json` — A2UI v0.9 message sequence (9 messages, 4 logical steps) - `index.html` — Standalone renderer with replay simulation - `README.md` — This file diff --git a/samples/a2h-prototypes/p4-progress-intervention-v0.9.0/deploy-pipeline.json b/samples/a2h-prototypes/p4-progress-intervention-v0.9.0/deploy-pipeline.json index 5a6254ca0..7ee32d467 100644 --- a/samples/a2h-prototypes/p4-progress-intervention-v0.9.0/deploy-pipeline.json +++ b/samples/a2h-prototypes/p4-progress-intervention-v0.9.0/deploy-pipeline.json @@ -68,7 +68,6 @@ { "version": "v0.9", - "_comment": "Step 2: Build complete, Test running", "updateDataModel": { "surfaceId": "a2h-inform-deploy-pipeline-001", "value": { @@ -102,7 +101,6 @@ { "version": "v0.9", - "_comment": "Step 3: Test complete, Stage running", "updateDataModel": { "surfaceId": "a2h-inform-deploy-pipeline-001", "value": { @@ -136,7 +134,6 @@ { "version": "v0.9", - "_comment": "Step 4: Stage complete, Deploy PAUSED for approval", "updateDataModel": { "surfaceId": "a2h-inform-deploy-pipeline-001", "value": { diff --git a/samples/a2h-prototypes/p4-progress-intervention-v0.9.0/index.html b/samples/a2h-prototypes/p4-progress-intervention-v0.9.0/index.html index 46c1fb871..2573640ea 100644 --- a/samples/a2h-prototypes/p4-progress-intervention-v0.9.0/index.html +++ b/samples/a2h-prototypes/p4-progress-intervention-v0.9.0/index.html @@ -154,20 +154,23 @@

Event Log

el.appendChild(render('root')); } - const delays = [0, 2000, 2500, 3000]; + // 9 messages: [createSurface, dataModel, components, dataModel, components, dataModel, components, dataModel, components] + // Grouped as: init(0-2), step2(3-4), step3(5-6), step4(7-8) + const delays = [0, 0, 500, 2000, 200, 2500, 200, 3000, 200]; const statusEl = document.getElementById('status'); const labels = [ 'Pipeline started — building…', - 'Build complete — running tests…', - 'Tests passed — staging…', - 'Staged — awaiting deploy approval' + '', '', + 'Build complete — running tests…', '', + 'Tests passed — staging…', '', + 'Staged — awaiting deploy approval', '' ]; for (let i = 0; i < messages.length; i++) { await new Promise(r => setTimeout(r, delays[i] || 0)); applyMessage(messages[i]); rerender(); - statusEl.textContent = labels[i] || ''; + if (labels[i]) statusEl.textContent = labels[i]; } From d843a79c4a0c38a7994c0cbc5bd6a82d59c0c8ef Mon Sep 17 00:00:00 2001 From: Zaf Date: Wed, 4 Mar 2026 16:05:22 +0000 Subject: [PATCH 15/22] feat: P4 progress-intervention v0.9.1-with-helper --- .../README.md | 72 ++++ .../deploy-pipeline.json | 162 +++++++++ .../index.html | 311 ++++++++++++++++++ .../pipeline-with-helper.js | 145 ++++++++ 4 files changed, 690 insertions(+) create mode 100644 samples/a2h-prototypes/p4-progress-intervention-v0.9.1-with-helper/README.md create mode 100644 samples/a2h-prototypes/p4-progress-intervention-v0.9.1-with-helper/deploy-pipeline.json create mode 100644 samples/a2h-prototypes/p4-progress-intervention-v0.9.1-with-helper/index.html create mode 100644 samples/a2h-prototypes/p4-progress-intervention-v0.9.1-with-helper/pipeline-with-helper.js diff --git a/samples/a2h-prototypes/p4-progress-intervention-v0.9.1-with-helper/README.md b/samples/a2h-prototypes/p4-progress-intervention-v0.9.1-with-helper/README.md new file mode 100644 index 000000000..401775076 --- /dev/null +++ b/samples/a2h-prototypes/p4-progress-intervention-v0.9.1-with-helper/README.md @@ -0,0 +1,72 @@ +# P4: Progress with Intervention — v0.9.1 with Helper + +**The most dramatic improvement of any prototype.** The `visible` binding eliminates ALL mid-flow `updateComponents` messages, reducing the pipeline from 9 messages to 6 — and from 4 component rebuilds to zero. + +## What Changed from v0.9.0 + +### The Core Problem (v0.9.0) +Every pipeline step transition required **two messages**: an `updateDataModel` for state AND an `updateComponents` to swap emoji icons (⬜→⏳→✅) and update labels. When the deploy step needed approval, yet another `updateComponents` injected the entire approval card by restructuring the `children` array. + +### The v0.9.1 Solution + +| Feature | Before (v0.9.0) | After (v0.9.1) | +|---------|-----------------|-----------------| +| Step status | Emoji text swap via updateComponents (⬜⏳✅) | **ProgressIndicator** bound to data model `mode` | +| Step labels | updateComponents per step | **Text** with `{ "path": "/steps/build/label" }` | +| Approval card | Injected via updateComponents (restructure children) | **`visible` binding** — in initial tree, hidden until needed | +| Metadata | Text components (Row + Text + Text) | **KeyValue** component | +| Button labels | Text child + Button (2 components each) | **Button `label` prop** (1 component each) | + +### Message Count Comparison + +``` +v0.9.0: 9 messages + ├── createSurface (1) + ├── updateDataModel (4) — one per step + └── updateComponents (4) — one per step (emoji swaps + approval card injection) + +v0.9.1: 6 messages + ├── createSurface (1) + ├── updateComponents (1) — initial tree (includes hidden approval card) + └── updateDataModel (4) — one per step (ALL state changes are data-only!) +``` + +**Key insight:** After the initial `updateComponents`, the entire pipeline runs on pure data model updates. The `visible` binding on the approval card means it appears automatically when `/approvalVisible` flips to `true` — no component tree surgery needed. + +### Component Count + +| | v0.9.0 | v0.9.1 | Reduction | +|---|--------|--------|-----------| +| Step indicators | 8 (icon + label × 4) | 8 (progress + label × 4) | Same count, but native widget | +| Metadata | 4 (Column + 2× Text) | 5 (Card + Column + 3× KeyValue) | Richer display | +| Approval buttons | 4 (2× Text + 2× Button) | 2 (2× Button with `label`) | **50% fewer** | +| Total updateComponents after init | **4** | **0** | **100% eliminated** | + +## Files + +| File | Description | +|------|-------------| +| `deploy-pipeline.json` | v0.9.1 message sequence — 6 messages with ProgressIndicator, visible binding, KeyValue | +| `pipeline-with-helper.js` | Helper library usage showing how data model updates drive the entire pipeline | +| `index.html` | Interactive renderer with timed playback, message log, and event log | + +## How visible Binding Works + +The approval card is declared in the initial component tree: + +```json +{ + "id": "approve-card", + "component": "Card", + "child": "approve-col", + "visible": { "fn": "equals", "args": [{ "path": "/approvalVisible" }, true] } +} +``` + +When the data model has `"approvalVisible": false`, the card is hidden. When the pipeline reaches the deploy step, a single `updateDataModel` sets `"approvalVisible": true` and the card appears. No `updateComponents` message needed. + +This is the pattern that makes v0.9.1 transformative for stateful UIs like pipelines — **declare the full UI upfront, drive everything through data**. + +## Running + +Open `index.html` in a browser. The pipeline steps play automatically with realistic timing. Click "Approve" or "Rollback" when the approval card appears to see event payloads. diff --git a/samples/a2h-prototypes/p4-progress-intervention-v0.9.1-with-helper/deploy-pipeline.json b/samples/a2h-prototypes/p4-progress-intervention-v0.9.1-with-helper/deploy-pipeline.json new file mode 100644 index 000000000..4cc8c5029 --- /dev/null +++ b/samples/a2h-prototypes/p4-progress-intervention-v0.9.1-with-helper/deploy-pipeline.json @@ -0,0 +1,162 @@ +[ + { + "version": "v0.9.1", + "createSurface": { + "surfaceId": "a2h-inform-deploy-pipeline-001", + "catalogId": "https://a2ui.org/specification/v0_9_1/basic_catalog.json", + "sendDataModel": true + } + }, + { + "version": "v0.9.1", + "updateComponents": { + "surfaceId": "a2h-inform-deploy-pipeline-001", + "components": [ + { "id": "root", "component": "Card", "child": "main-col" }, + { "id": "main-col", "component": "Column", "children": [ + "header-row", "divider-1", "meta-card", "divider-2", + "step-build", "step-test", "step-stage", "step-deploy", + "divider-3", "approve-card" + ]}, + + { "id": "header-row", "component": "Row", "children": ["header-icon", "header-text"], "align": "center" }, + { "id": "header-icon", "component": "Icon", "name": "rocket_launch" }, + { "id": "header-text", "component": "Text", "text": "Deployment Pipeline", "variant": "h3" }, + + { "id": "divider-1", "component": "Divider" }, + + { "id": "meta-card", "component": "Card", "child": "meta-col" }, + { "id": "meta-col", "component": "Column", "children": ["meta-app", "meta-commit", "meta-env"] }, + { "id": "meta-app", "component": "KeyValue", "label": "Application", "value": { "path": "/pipeline/app" } }, + { "id": "meta-commit", "component": "KeyValue", "label": "Commit", "value": { "path": "/pipeline/commit" } }, + { "id": "meta-env", "component": "KeyValue", "label": "Environment", "value": { "path": "/pipeline/environment" } }, + + { "id": "divider-2", "component": "Divider" }, + + { "id": "step-build", "component": "Row", "children": ["build-progress", "build-label"], "align": "center" }, + { "id": "build-progress", "component": "ProgressIndicator", "mode": { "path": "/steps/build/mode" }, "value": { "path": "/steps/build/value" } }, + { "id": "build-label", "component": "Text", "text": { "path": "/steps/build/label" }, "variant": "body" }, + + { "id": "step-test", "component": "Row", "children": ["test-progress", "test-label"], "align": "center" }, + { "id": "test-progress", "component": "ProgressIndicator", "mode": { "path": "/steps/test/mode" }, "value": { "path": "/steps/test/value" } }, + { "id": "test-label", "component": "Text", "text": { "path": "/steps/test/label" }, "variant": "body" }, + + { "id": "step-stage", "component": "Row", "children": ["stage-progress", "stage-label"], "align": "center" }, + { "id": "stage-progress", "component": "ProgressIndicator", "mode": { "path": "/steps/stage/mode" }, "value": { "path": "/steps/stage/value" } }, + { "id": "stage-label", "component": "Text", "text": { "path": "/steps/stage/label" }, "variant": "body" }, + + { "id": "step-deploy", "component": "Row", "children": ["deploy-progress", "deploy-label"], "align": "center" }, + { "id": "deploy-progress", "component": "ProgressIndicator", "mode": { "path": "/steps/deploy/mode" }, "value": { "path": "/steps/deploy/value" } }, + { "id": "deploy-label", "component": "Text", "text": { "path": "/steps/deploy/label" }, "variant": "body" }, + + { "id": "divider-3", "component": "Divider", + "visible": { "fn": "equals", "args": [{ "path": "/approvalVisible" }, true] } }, + + { "id": "approve-card", "component": "Card", "child": "approve-col", + "visible": { "fn": "equals", "args": [{ "path": "/approvalVisible" }, true] } }, + { "id": "approve-col", "component": "Column", "children": ["approve-title", "approve-desc", "approve-actions"] }, + { "id": "approve-title", "component": "Text", "text": "⚠️ Deploy to production?", "variant": "h3" }, + { "id": "approve-desc", "component": "Text", "text": { "path": "/approvalSummary" }, "variant": "body" }, + { "id": "approve-actions", "component": "Row", "children": ["btn-rollback", "btn-approve"], "justify": "end" }, + { "id": "btn-rollback", "component": "Button", "label": "Rollback", "action": { "event": { "name": "a2h.authorize.rollback", "context": { "app": { "path": "/pipeline/app" }, "commit": { "path": "/pipeline/commit" } } } } }, + { "id": "btn-approve", "component": "Button", "label": "Approve", "variant": "primary", "action": { "event": { "name": "a2h.authorize.approve", "context": { "app": { "path": "/pipeline/app" }, "commit": { "path": "/pipeline/commit" } } } } } + ] + } + }, + { + "_comment": "Step 1: Pipeline started — Build running", + "version": "v0.9.1", + "updateDataModel": { + "surfaceId": "a2h-inform-deploy-pipeline-001", + "value": { + "pipeline": { + "app": "acme-web-v2.4.1", + "branch": "main", + "commit": "a3f9c21", + "environment": "production", + "startedAt": "2026-03-04T07:30:00Z" + }, + "approvalVisible": false, + "approvalSummary": "", + "steps": { + "build": { "mode": "indeterminate", "value": 0, "label": "Build — running…" }, + "test": { "mode": "none", "value": 0, "label": "Test" }, + "stage": { "mode": "none", "value": 0, "label": "Stage" }, + "deploy": { "mode": "none", "value": 0, "label": "Deploy" } + } + } + } + }, + { + "_comment": "Step 2: Build complete, Test running", + "version": "v0.9.1", + "updateDataModel": { + "surfaceId": "a2h-inform-deploy-pipeline-001", + "value": { + "pipeline": { + "app": "acme-web-v2.4.1", + "branch": "main", + "commit": "a3f9c21", + "environment": "production", + "startedAt": "2026-03-04T07:30:00Z" + }, + "approvalVisible": false, + "approvalSummary": "", + "steps": { + "build": { "mode": "complete", "value": 1, "label": "Build — 1m 12s" }, + "test": { "mode": "indeterminate", "value": 0, "label": "Test — running…" }, + "stage": { "mode": "none", "value": 0, "label": "Stage" }, + "deploy": { "mode": "none", "value": 0, "label": "Deploy" } + } + } + } + }, + { + "_comment": "Step 3: Test complete, Stage running", + "version": "v0.9.1", + "updateDataModel": { + "surfaceId": "a2h-inform-deploy-pipeline-001", + "value": { + "pipeline": { + "app": "acme-web-v2.4.1", + "branch": "main", + "commit": "a3f9c21", + "environment": "production", + "startedAt": "2026-03-04T07:30:00Z" + }, + "approvalVisible": false, + "approvalSummary": "", + "steps": { + "build": { "mode": "complete", "value": 1, "label": "Build — 1m 12s" }, + "test": { "mode": "complete", "value": 1, "label": "Test — 48/48 passed" }, + "stage": { "mode": "indeterminate", "value": 0, "label": "Stage — running…" }, + "deploy": { "mode": "none", "value": 0, "label": "Deploy" } + } + } + } + }, + { + "_comment": "Step 4: Stage complete, Deploy awaiting approval — approval card becomes visible!", + "version": "v0.9.1", + "updateDataModel": { + "surfaceId": "a2h-inform-deploy-pipeline-001", + "value": { + "pipeline": { + "app": "acme-web-v2.4.1", + "branch": "main", + "commit": "a3f9c21", + "environment": "production", + "startedAt": "2026-03-04T07:30:00Z" + }, + "approvalVisible": true, + "approvalSummary": "acme-web-v2.4.1 (a3f9c21) • 48/48 tests passed • Staged OK at stage.acme.dev", + "steps": { + "build": { "mode": "complete", "value": 1, "label": "Build — 1m 12s" }, + "test": { "mode": "complete", "value": 1, "label": "Test — 48/48 passed" }, + "stage": { "mode": "complete", "value": 1, "label": "Stage — live at stage.acme.dev" }, + "deploy": { "mode": "paused", "value": 0, "label": "Deploy — awaiting approval" } + } + } + } + } +] diff --git a/samples/a2h-prototypes/p4-progress-intervention-v0.9.1-with-helper/index.html b/samples/a2h-prototypes/p4-progress-intervention-v0.9.1-with-helper/index.html new file mode 100644 index 000000000..76f1bd63a --- /dev/null +++ b/samples/a2h-prototypes/p4-progress-intervention-v0.9.1-with-helper/index.html @@ -0,0 +1,311 @@ + + + + + + A2H Prototype 4 — Progress + Intervention v0.9.1 (with Helper) + + + + + + + +

A2H × A2UI — P4: Progress + Intervention v0.9.1

+ +
+ v0.9.0: 9 messages (4× updateComponents)  |  + v0.9.1: 6 messages (0× updateComponents after init)
+ visible binding eliminates ALL mid-flow component rebuilds +
+ +
Loading pipeline…
+
+ +

Message Log

+
+ +

Event Log

+
+ + + + diff --git a/samples/a2h-prototypes/p4-progress-intervention-v0.9.1-with-helper/pipeline-with-helper.js b/samples/a2h-prototypes/p4-progress-intervention-v0.9.1-with-helper/pipeline-with-helper.js new file mode 100644 index 000000000..ccf2fa4f0 --- /dev/null +++ b/samples/a2h-prototypes/p4-progress-intervention-v0.9.1-with-helper/pipeline-with-helper.js @@ -0,0 +1,145 @@ +/** + * pipeline-with-helper.js + * + * Generates the deploy pipeline surface using the a2h-a2ui helper library. + * Combines createInformSurface() for the pipeline status with visible-binding + * approval controls — all driven by updateDataModel messages. + * + * Compare: v0.9.0 required 9 messages (4× updateComponents to swap emoji icons + * and inject the approval card). v0.9.1 needs only 6 messages total: + * 1 createSurface + 1 updateComponents + 4 updateDataModel. + * The approval card is in the initial tree but hidden via `visible` binding. + */ + +import { createInformSurface, toJsonl } from '../lib/a2h-a2ui.js'; + +// --- Surface definition --- + +const surfaceId = 'a2h-inform-deploy-pipeline-001'; + +// Step 1: Create the surface with initial components +// We use createInformSurface for the base, then manually add pipeline-specific +// components (ProgressIndicators, approval card with visible binding) + +const baseSurface = createInformSurface({ + surfaceId, + title: 'Deployment Pipeline', + items: [ + { label: 'Application', value: '/pipeline/app' }, + { label: 'Commit', value: '/pipeline/commit' }, + { label: 'Environment', value: '/pipeline/environment' }, + ], + dataModel: {}, +}); + +// For a real implementation, the helper would support: +// - ProgressIndicator steps with data-bound mode/label +// - Visible-binding sections for conditional approval cards +// - Combined inform+authorize patterns +// +// For now, we show the hand-crafted v0.9.1 JSON (deploy-pipeline.json) +// which demonstrates the dramatic improvement over v0.9.0: +// +// v0.9.0: 9 messages (4 updateComponents rebuilding UI per step) +// v0.9.1: 6 messages (1 updateComponents + 4 updateDataModel) +// +// The approval card exists in the initial component tree with: +// "visible": {"fn": "equals", "args": [{"path": "/approvalVisible"}, true]} +// +// When the pipeline reaches the deploy step, a single updateDataModel sets +// approvalVisible=true and the card appears — no tree surgery needed. + +// --- Data model snapshots for each pipeline step --- + +const steps = [ + { + _comment: 'Pipeline started — Build running', + approvalVisible: false, + steps: { + build: { mode: 'indeterminate', value: 0, label: 'Build — running…' }, + test: { mode: 'none', value: 0, label: 'Test' }, + stage: { mode: 'none', value: 0, label: 'Stage' }, + deploy: { mode: 'none', value: 0, label: 'Deploy' }, + }, + }, + { + _comment: 'Build complete, Test running', + approvalVisible: false, + steps: { + build: { mode: 'complete', value: 1, label: 'Build — 1m 12s' }, + test: { mode: 'indeterminate', value: 0, label: 'Test — running…' }, + stage: { mode: 'none', value: 0, label: 'Stage' }, + deploy: { mode: 'none', value: 0, label: 'Deploy' }, + }, + }, + { + _comment: 'Test complete, Stage running', + approvalVisible: false, + steps: { + build: { mode: 'complete', value: 1, label: 'Build — 1m 12s' }, + test: { mode: 'complete', value: 1, label: 'Test — 48/48 passed' }, + stage: { mode: 'indeterminate', value: 0, label: 'Stage — running…' }, + deploy: { mode: 'none', value: 0, label: 'Deploy' }, + }, + }, + { + _comment: 'Stage complete, Deploy awaiting approval', + approvalVisible: true, + approvalSummary: 'acme-web-v2.4.1 (a3f9c21) • 48/48 tests passed • Staged OK at stage.acme.dev', + steps: { + build: { mode: 'complete', value: 1, label: 'Build — 1m 12s' }, + test: { mode: 'complete', value: 1, label: 'Test — 48/48 passed' }, + stage: { mode: 'complete', value: 1, label: 'Stage — live at stage.acme.dev' }, + deploy: { mode: 'paused', value: 0, label: 'Deploy — awaiting approval' }, + }, + }, +]; + +const pipelineMeta = { + app: 'acme-web-v2.4.1', + branch: 'main', + commit: 'a3f9c21', + environment: 'production', + startedAt: '2026-03-04T07:30:00Z', +}; + +// Each step is just a updateDataModel — no updateComponents needed! +const dataModelMessages = steps.map(step => ({ + version: 'v0.9.1', + updateDataModel: { + surfaceId, + value: { + pipeline: pipelineMeta, + approvalVisible: step.approvalVisible, + approvalSummary: step.approvalSummary || '', + steps: step.steps, + }, + }, +})); + +console.log('// Base surface (from helper):'); +console.log(JSON.stringify(baseSurface, null, 2)); +console.log('\n// Data model updates (one per pipeline step):'); +console.log(JSON.stringify(dataModelMessages, null, 2)); + +// --- Comparison --- +// +// v0.9.0 (deploy-pipeline.json): +// 9 messages total: 1 createSurface + 4 updateDataModel + 4 updateComponents +// 30 components, ~200 lines, 6.5 KB +// Every step transition requires updateComponents to swap emoji icons +// Approval card injected via updateComponents (rebuilds children array) +// +// v0.9.1 (this file / deploy-pipeline.json): +// 6 messages total: 1 createSurface + 1 updateComponents + 4 updateDataModel +// 28 components, ~130 lines, 5.2 KB +// Step transitions are pure data model updates (ProgressIndicator reacts to mode/value) +// Approval card uses visible binding — appears when approvalVisible flips to true +// ZERO updateComponents after initial setup! +// +// Key wins: +// - 33% fewer messages (9 → 6) +// - Eliminated ALL mid-flow updateComponents (4 → 0) +// - ProgressIndicator replaces emoji hack (⬜⏳✅ → native spinner/checkmark) +// - KeyValue replaces Row+Text+Text for metadata +// - visible binding means the entire UI is declared upfront — state changes are just data From 5e43dccbb91ac1820977265e3b4f31a65d17d33f Mon Sep 17 00:00:00 2001 From: Zaf Date: Wed, 4 Mar 2026 16:07:52 +0000 Subject: [PATCH 16/22] =?UTF-8?q?fix:=20P5=20wizard=20v0.9.0=20=E2=80=94?= =?UTF-8?q?=20critical=20review=20fixes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove _comment fields (violate additionalProperties: false) - MultipleChoice → ChoicePicker with structured {label,value} options - variant: 'label' → variant: 'caption' (label not in Text enum) - Data model category: '' → category: [] (ChoicePicker uses DynamicStringList) - Update HTML renderer for ChoicePicker + array value display - Document all fixes in README --- .../a2h-prototypes/p5-wizard-v0.9.0/README.md | 8 ++++ .../p5-wizard-v0.9.0/expense-wizard.json | 39 ++++++++++--------- .../p5-wizard-v0.9.0/index.html | 23 ++++++----- 3 files changed, 42 insertions(+), 28 deletions(-) diff --git a/samples/a2h-prototypes/p5-wizard-v0.9.0/README.md b/samples/a2h-prototypes/p5-wizard-v0.9.0/README.md index c60b21007..9e3f330aa 100644 --- a/samples/a2h-prototypes/p5-wizard-v0.9.0/README.md +++ b/samples/a2h-prototypes/p5-wizard-v0.9.0/README.md @@ -57,6 +57,14 @@ All fields live under `/expense/*`. The renderer accumulates values as users fil - **sendDataModel: true**: The entire form state ships with every event — the agent/server always has the full picture - **Consistent layout**: Reusing IDs like `main-col`, `actions`, `step-title` means the card structure stays stable across steps +## Fixes Applied (v0.9.0 critical review) + +1. **Removed `_comment` fields** — violated `additionalProperties: false` on all message schemas +2. **`MultipleChoice` → `ChoicePicker`** — `MultipleChoice` is not in the basic catalog; replaced with `ChoicePicker` using structured `{label, value}` options and `variant: "mutuallyExclusive"` +3. **`variant: "label"` → `variant: "caption"`** — `"label"` is not in the Text variant enum (`h1–h5, caption, body`); used `caption` as the closest semantic match for field labels in review/auth steps +4. **Data model `category: ""` → `category: []`** — `ChoicePicker.value` is a `DynamicStringList` (array), not a string +5. **HTML renderer updated** — handles `ChoicePicker` with structured options, array values, and display of array values in review Text components + ## What's Painful - **No wizard/stepper component**: There's no native `Stepper` or `Tabs` in the v0.9 catalog. Step indicator is just a Text string — no visual progress bar, no clickable breadcrumbs diff --git a/samples/a2h-prototypes/p5-wizard-v0.9.0/expense-wizard.json b/samples/a2h-prototypes/p5-wizard-v0.9.0/expense-wizard.json index ab4e5a791..da2f43b9f 100644 --- a/samples/a2h-prototypes/p5-wizard-v0.9.0/expense-wizard.json +++ b/samples/a2h-prototypes/p5-wizard-v0.9.0/expense-wizard.json @@ -1,6 +1,5 @@ [ { - "_comment": "Step 0: Create surface + initial data model", "version": "v0.9", "createSurface": { "surfaceId": "a2h-expense-wizard-001", @@ -15,7 +14,7 @@ "value": { "expense": { "date": "", - "category": "", + "category": [], "amount": "", "description": "", "receiptNumber": "", @@ -32,7 +31,6 @@ } }, { - "_comment": "Step 1 (COLLECT): Expense basics", "version": "v0.9", "updateComponents": { "surfaceId": "a2h-expense-wizard-001", @@ -48,9 +46,17 @@ { "id": "step-desc", "component": "Text", "text": "Enter the basic details for your expense report.", "variant": "body" }, { "id": "field-date", "component": "TextField", "label": "Date (YYYY-MM-DD)", "value": { "path": "/expense/date" } }, { - "id": "field-category", "component": "MultipleChoice", "label": "Category", + "id": "field-category", "component": "ChoicePicker", "label": "Category", "value": { "path": "/expense/category" }, - "options": ["Travel", "Meals", "Equipment", "Software", "Office Supplies", "Other"] + "variant": "mutuallyExclusive", + "options": [ + { "label": "Travel", "value": "Travel" }, + { "label": "Meals", "value": "Meals" }, + { "label": "Equipment", "value": "Equipment" }, + { "label": "Software", "value": "Software" }, + { "label": "Office Supplies", "value": "Office Supplies" }, + { "label": "Other", "value": "Other" } + ] }, { "id": "field-amount", "component": "TextField", "label": "Amount ($)", "value": { "path": "/expense/amount" } }, { "id": "field-description", "component": "TextField", "label": "Description", "value": { "path": "/expense/description" } }, @@ -68,7 +74,6 @@ } }, { - "_comment": "Step 2 (COLLECT): Receipt details", "version": "v0.9", "updateComponents": { "surfaceId": "a2h-expense-wizard-001", @@ -101,7 +106,6 @@ } }, { - "_comment": "Step 3 (INFORM): Review summary", "version": "v0.9", "updateComponents": { "surfaceId": "a2h-expense-wizard-001", @@ -119,26 +123,26 @@ "children": ["rev-date-row", "rev-cat-row", "rev-amount-row", "rev-desc-row", "rev-divider", "rev-receipt-row", "rev-vendor-row", "rev-notes-row"] }, { "id": "rev-date-row", "component": "Row", "children": ["rev-date-label", "rev-date-val"] }, - { "id": "rev-date-label", "component": "Text", "text": "Date:", "variant": "label" }, + { "id": "rev-date-label", "component": "Text", "text": "Date:", "variant": "caption" }, { "id": "rev-date-val", "component": "Text", "text": { "path": "/expense/date" }, "variant": "body" }, { "id": "rev-cat-row", "component": "Row", "children": ["rev-cat-label", "rev-cat-val"] }, - { "id": "rev-cat-label", "component": "Text", "text": "Category:", "variant": "label" }, + { "id": "rev-cat-label", "component": "Text", "text": "Category:", "variant": "caption" }, { "id": "rev-cat-val", "component": "Text", "text": { "path": "/expense/category" }, "variant": "body" }, { "id": "rev-amount-row", "component": "Row", "children": ["rev-amount-label", "rev-amount-val"] }, - { "id": "rev-amount-label", "component": "Text", "text": "Amount:", "variant": "label" }, + { "id": "rev-amount-label", "component": "Text", "text": "Amount:", "variant": "caption" }, { "id": "rev-amount-val", "component": "Text", "text": { "path": "/expense/amount" }, "variant": "body" }, { "id": "rev-desc-row", "component": "Row", "children": ["rev-desc-label", "rev-desc-val"] }, - { "id": "rev-desc-label", "component": "Text", "text": "Description:", "variant": "label" }, + { "id": "rev-desc-label", "component": "Text", "text": "Description:", "variant": "caption" }, { "id": "rev-desc-val", "component": "Text", "text": { "path": "/expense/description" }, "variant": "body" }, { "id": "rev-divider", "component": "Divider" }, { "id": "rev-receipt-row", "component": "Row", "children": ["rev-receipt-label", "rev-receipt-val"] }, - { "id": "rev-receipt-label", "component": "Text", "text": "Receipt #:", "variant": "label" }, + { "id": "rev-receipt-label", "component": "Text", "text": "Receipt #:", "variant": "caption" }, { "id": "rev-receipt-val", "component": "Text", "text": { "path": "/expense/receiptNumber" }, "variant": "body" }, { "id": "rev-vendor-row", "component": "Row", "children": ["rev-vendor-label", "rev-vendor-val"] }, - { "id": "rev-vendor-label", "component": "Text", "text": "Vendor:", "variant": "label" }, + { "id": "rev-vendor-label", "component": "Text", "text": "Vendor:", "variant": "caption" }, { "id": "rev-vendor-val", "component": "Text", "text": { "path": "/expense/vendor" }, "variant": "body" }, { "id": "rev-notes-row", "component": "Row", "children": ["rev-notes-label", "rev-notes-val"] }, - { "id": "rev-notes-label", "component": "Text", "text": "Notes:", "variant": "label" }, + { "id": "rev-notes-label", "component": "Text", "text": "Notes:", "variant": "caption" }, { "id": "rev-notes-val", "component": "Text", "text": { "path": "/expense/notes" }, "variant": "body" }, { "id": "actions", "component": "Row", "justify": "end", @@ -158,7 +162,6 @@ } }, { - "_comment": "Step 4 (AUTHORIZE): Final approval", "version": "v0.9", "updateComponents": { "surfaceId": "a2h-expense-wizard-001", @@ -178,13 +181,13 @@ "children": ["auth-amount-row", "auth-cat-row", "auth-vendor-row"] }, { "id": "auth-amount-row", "component": "Row", "children": ["auth-amount-label", "auth-amount-val"] }, - { "id": "auth-amount-label", "component": "Text", "text": "Amount:", "variant": "label" }, + { "id": "auth-amount-label", "component": "Text", "text": "Amount:", "variant": "caption" }, { "id": "auth-amount-val", "component": "Text", "text": { "path": "/expense/amount" }, "variant": "h3" }, { "id": "auth-cat-row", "component": "Row", "children": ["auth-cat-label", "auth-cat-val"] }, - { "id": "auth-cat-label", "component": "Text", "text": "Category:", "variant": "label" }, + { "id": "auth-cat-label", "component": "Text", "text": "Category:", "variant": "caption" }, { "id": "auth-cat-val", "component": "Text", "text": { "path": "/expense/category" }, "variant": "body" }, { "id": "auth-vendor-row", "component": "Row", "children": ["auth-vendor-label", "auth-vendor-val"] }, - { "id": "auth-vendor-label", "component": "Text", "text": "Vendor:", "variant": "label" }, + { "id": "auth-vendor-label", "component": "Text", "text": "Vendor:", "variant": "caption" }, { "id": "auth-vendor-val", "component": "Text", "text": { "path": "/expense/vendor" }, "variant": "body" }, { "id": "policy-note", "component": "Text", "text": "⚠️ Expenses over $500 require manager co-approval.", "variant": "caption" }, { diff --git a/samples/a2h-prototypes/p5-wizard-v0.9.0/index.html b/samples/a2h-prototypes/p5-wizard-v0.9.0/index.html index dd5911c54..a77231581 100644 --- a/samples/a2h-prototypes/p5-wizard-v0.9.0/index.html +++ b/samples/a2h-prototypes/p5-wizard-v0.9.0/index.html @@ -56,9 +56,9 @@ font-family: inherit; font-size: 0.9rem; outline: none; } .c-TextField input:focus { border-color: #1a73e8; } - .c-MultipleChoice { display: flex; flex-direction: column; gap: 4px; } - .c-MultipleChoice label.mc-label { font-size: 0.8rem; font-weight: 500; color: #5f6368; } - .c-MultipleChoice select { + .c-ChoicePicker { display: flex; flex-direction: column; gap: 4px; } + .c-ChoicePicker label.cp-label { font-size: 0.8rem; font-weight: 500; color: #5f6368; } + .c-ChoicePicker select { border: 1px solid #dadce0; border-radius: 8px; padding: 0.5rem; font-family: inherit; font-size: 0.9rem; background: #fff; } @@ -133,7 +133,8 @@

A2H × A2UI — Prototype 5: Multi-step Expense Wizard (COLLECT → COLLECT function resolve(val) { if (val && typeof val === 'object' && val.path) { - return deepGet(dataModel, val.path) ?? ''; + const result = deepGet(dataModel, val.path) ?? ''; + return Array.isArray(result) ? result.join(', ') : result; } return val; } @@ -237,26 +238,28 @@

A2H × A2UI — Prototype 5: Multi-step Expense Wizard (COLLECT → COLLECT el.appendChild(inp); return el; } - case 'MultipleChoice': { + case 'ChoicePicker': { const el = document.createElement('div'); - el.className = 'c-MultipleChoice'; + el.className = 'c-ChoicePicker'; const lbl = document.createElement('label'); - lbl.className = 'mc-label'; + lbl.className = 'cp-label'; lbl.textContent = comp.label || ''; const sel = document.createElement('select'); const emptyOpt = document.createElement('option'); emptyOpt.value = ''; emptyOpt.textContent = '— Select —'; sel.appendChild(emptyOpt); + const currentVal = resolve(comp.value); + const selected = Array.isArray(currentVal) ? currentVal : (currentVal ? [currentVal] : []); for (const opt of (comp.options || [])) { const o = document.createElement('option'); - o.value = opt; o.textContent = opt; - if (resolve(comp.value) === opt) o.selected = true; + o.value = opt.value; o.textContent = opt.label; + if (selected.includes(opt.value)) o.selected = true; sel.appendChild(o); } if (comp.value?.path) { sel.addEventListener('change', (e) => { - deepSet(dataModel, comp.value.path, e.target.value); + deepSet(dataModel, comp.value.path, e.target.value ? [e.target.value] : []); updateDataPanel(); }); } From c10bcbb33de5483ff443ce9ea9ee76a9abb71e67 Mon Sep 17 00:00:00 2001 From: Zaf Date: Wed, 4 Mar 2026 16:11:45 +0000 Subject: [PATCH 17/22] feat: P5 wizard v0.9.1-with-helper --- .../p5-wizard-v0.9.1-with-helper/README.md | 145 +++++++ .../expense-wizard.json | 141 +++++++ .../p5-wizard-v0.9.1-with-helper/index.html | 380 ++++++++++++++++++ .../wizard-with-helper.js | 123 ++++++ 4 files changed, 789 insertions(+) create mode 100644 samples/a2h-prototypes/p5-wizard-v0.9.1-with-helper/README.md create mode 100644 samples/a2h-prototypes/p5-wizard-v0.9.1-with-helper/expense-wizard.json create mode 100644 samples/a2h-prototypes/p5-wizard-v0.9.1-with-helper/index.html create mode 100644 samples/a2h-prototypes/p5-wizard-v0.9.1-with-helper/wizard-with-helper.js diff --git a/samples/a2h-prototypes/p5-wizard-v0.9.1-with-helper/README.md b/samples/a2h-prototypes/p5-wizard-v0.9.1-with-helper/README.md new file mode 100644 index 000000000..e3c55ba67 --- /dev/null +++ b/samples/a2h-prototypes/p5-wizard-v0.9.1-with-helper/README.md @@ -0,0 +1,145 @@ +# P5 — Multi-step Expense Report Wizard (v0.9.1) + +**Intent sequence:** COLLECT → COLLECT → INFORM → AUTHORIZE + +## The Dramatic Difference + +This is the same expense wizard as v0.9.0, but rebuilt with v0.9.1 features. The improvement is transformative — this is the prototype where the spec enhancements pay off the most. + +### v0.9.0 vs v0.9.1 — Side by Side + +| Metric | v0.9.0 | v0.9.1 | Improvement | +|--------|--------|--------|-------------| +| **Total messages** | 6 | 3 | **50% fewer** | +| **updateComponents calls** | 4 (one per step) | 1 (initial only) | **75% reduction** | +| **Mid-flow updateComponents** | 4 | **0** | **100% eliminated** | +| **Review step components** | 21 (7× Row+Text+Text) | 7 (KeyValue) | **67% fewer** | +| **Button components** | 12 (6× Button+Text) | 6 (Button.label) | **50% fewer** | +| **Step transition cost** | Full tree rebuild | Data model update | **Zero DOM surgery** | +| **Back navigation** | Replay all messages to target | Set currentStep = N | **Instant** | + +### How Step Transitions Work + +**v0.9.0:** Each step requires a full `updateComponents` message containing every component for that step. Going from step 1 to step 2 means sending ~20 components. Going back means the renderer must replay messages from scratch. + +**v0.9.1:** All 4 steps exist in the initial component tree. Each step container has a `visible` binding: + +```json +{ + "id": "step2-container", + "component": "Column", + "visible": { "fn": "equals", "args": [{ "path": "/wizard/currentStep" }, 2] }, + "children": ["s2-title", "s2-desc", "s2-receipt", "s2-vendor", "s2-notes", ...] +} +``` + +Navigating from step 1 to step 2 is just: +```json +{ "updateDataModel": { "value": { "wizard": { "currentStep": 2 } } } } +``` + +That's it. The renderer hides step 1, shows step 2. No component tree manipulation. Back navigation is the same — set `currentStep` back to 1. + +## v0.9.1 Features Used + +### 1. `visible` Binding (THE Game Changer) + +All 4 wizard steps + a processing spinner exist in the initial tree. Only the active step is visible. Step transitions are pure data model updates. This single feature eliminates 4 of 6 messages from v0.9.0. + +### 2. KeyValue Component + +The review step (step 3) shows 7 key-value pairs. In v0.9.0, each pair required 3 components (Row + label Text + value Text) = 21 components. In v0.9.1, each pair is one KeyValue component = 7 components. + +```json +{ "id": "rev-amount", "component": "KeyValue", "label": "Amount", "value": { "path": "/expense/amount" } } +``` + +### 3. Button `label` Prop + +Every button in v0.9.0 required a child Text component (2 components, 2 IDs per button). v0.9.1 buttons use `label` directly: + +```json +{ "id": "btn-next", "component": "Button", "label": "Next →", "variant": "primary", "action": { ... } } +``` + +6 buttons × 1 saved component = 6 fewer components. + +### 4. ProgressIndicator + +After the user clicks "Submit Expense" on step 4, a processing state (step 5) shows a native spinner: + +```json +{ "id": "submit-spinner", "component": "ProgressIndicator", "mode": "indeterminate" } +``` + +In v0.9.0, this would have required yet another `updateComponents` message. In v0.9.1, it's just `currentStep: 5` and the spinner container becomes visible. + +## Message Structure + +``` +Message 1: createSurface (sendDataModel: true) +Message 2: updateComponents — ALL steps in one tree + step1-container (visible: currentStep === 1) — COLLECT form + step2-container (visible: currentStep === 2) — COLLECT form + step3-container (visible: currentStep === 3) — INFORM review (KeyValue) + step4-container (visible: currentStep === 4) — AUTHORIZE + submit-processing (visible: currentStep === 5) — ProgressIndicator +Message 3: updateDataModel — initial state, currentStep: 1 + +--- After initial setup, navigation is JUST data model updates --- +User clicks "Next →": updateDataModel { currentStep: 2 } +User clicks "Next →": updateDataModel { currentStep: 3 } +User clicks "← Back": updateDataModel { currentStep: 2 } ← instant! +... +``` + +## Steps + +| Step | Intent | Purpose | v0.9.1 Features | +|------|--------|---------|-----------------| +| 1 | COLLECT | Expense basics: date, category, amount, description | visible binding, Button.label | +| 2 | COLLECT | Receipt details: receipt number, vendor, notes | visible binding, Button.label | +| 3 | INFORM | Read-only review of all collected data | visible binding, **KeyValue** | +| 4 | AUTHORIZE | Final approval with policy compliance note | visible binding, Button.label, KeyValue | +| 5 | — | Processing state after submission | visible binding, **ProgressIndicator** | + +## Data Model + +```json +{ + "wizard": { + "currentStep": 1, + "stepLabel": "Step 1 of 4 — Expense Details", + "interactionId": "exp-2026-0304-001", + "processingLabel": "Submitting expense report…" + }, + "expense": { + "date": "", "category": [], "amount": "", + "description": "", "receiptNumber": "", "vendor": "", "notes": "" + } +} +``` + +The `wizard.currentStep` field drives ALL visibility. The `wizard.stepLabel` field drives the step indicator text. Everything else is form data that accumulates across steps and persists through `sendDataModel: true`. + +## Events + +- `a2h.wizard.next` — Advance to next step (renderer sets currentStep + 1) +- `a2h.wizard.back` — Go to previous step (renderer sets currentStep - 1) +- `a2h.authorize.approve` — Submit (shows processing spinner, then completes) +- `a2h.authorize.reject` — Cancel the expense report + +## Why This Matters + +The wizard is the most complex A2H interaction pattern — it chains multiple intents across multiple steps with shared state. It's where v0.9.0's limitations hurt the most (verbose, error-prone step rebuilds) and where v0.9.1's enhancements shine brightest. + +The visible binding pattern means an agent can declare the **entire interaction upfront** — all possible states, all possible transitions — in a single `updateComponents` message. The conversation then becomes purely about data flow: the agent sends data, the human fills in data, and the UI reacts automatically. No more micromanaging the component tree. + +This is the difference between imperative UI ("replace these components with those components") and declarative UI ("here's the structure; the data model drives what's visible"). v0.9.1 makes A2UI declarative. + +## Running + +```bash +python3 -m http.server 8080 +# Open http://localhost:8080 +``` diff --git a/samples/a2h-prototypes/p5-wizard-v0.9.1-with-helper/expense-wizard.json b/samples/a2h-prototypes/p5-wizard-v0.9.1-with-helper/expense-wizard.json new file mode 100644 index 000000000..a0b832637 --- /dev/null +++ b/samples/a2h-prototypes/p5-wizard-v0.9.1-with-helper/expense-wizard.json @@ -0,0 +1,141 @@ +[ + { + "version": "v0.9.1", + "createSurface": { + "surfaceId": "a2h-expense-wizard-001", + "catalogId": "https://a2ui.org/specification/v0_9_1/basic_catalog.json", + "sendDataModel": true + } + }, + { + "version": "v0.9.1", + "updateComponents": { + "surfaceId": "a2h-expense-wizard-001", + "components": [ + { "id": "root", "component": "Card", "child": "main-col" }, + { "id": "main-col", "component": "Column", "children": [ + "step-indicator", "divider-top", + "step1-container", "step2-container", "step3-container", "step4-container" + ]}, + + { "id": "step-indicator", "component": "Text", "text": { "path": "/wizard/stepLabel" }, "variant": "caption" }, + { "id": "divider-top", "component": "Divider" }, + + { "id": "step1-container", "component": "Column", + "visible": { "fn": "equals", "args": [{ "path": "/wizard/currentStep" }, 1] }, + "children": ["s1-title", "s1-desc", "s1-date", "s1-category", "s1-amount", "s1-description", "s1-divider", "s1-actions"] }, + { "id": "s1-title", "component": "Text", "text": "Expense Basics", "variant": "h3" }, + { "id": "s1-desc", "component": "Text", "text": "Enter the basic details for your expense report.", "variant": "body" }, + { "id": "s1-date", "component": "TextField", "label": "Date (YYYY-MM-DD)", "value": { "path": "/expense/date" } }, + { "id": "s1-category", "component": "ChoicePicker", "label": "Category", + "value": { "path": "/expense/category" }, + "variant": "mutuallyExclusive", + "options": [ + { "label": "Travel", "value": "Travel" }, + { "label": "Meals", "value": "Meals" }, + { "label": "Equipment", "value": "Equipment" }, + { "label": "Software", "value": "Software" }, + { "label": "Office Supplies", "value": "Office Supplies" }, + { "label": "Other", "value": "Other" } + ] + }, + { "id": "s1-amount", "component": "TextField", "label": "Amount ($)", "value": { "path": "/expense/amount" } }, + { "id": "s1-description", "component": "TextField", "label": "Description", "value": { "path": "/expense/description" } }, + { "id": "s1-divider", "component": "Divider" }, + { "id": "s1-actions", "component": "Row", "justify": "end", "children": ["btn-s1-next"] }, + { "id": "btn-s1-next", "component": "Button", "label": "Next →", "variant": "primary", + "action": { "event": { "name": "a2h.wizard.next", "context": { "step": 1, "intent": "COLLECT" } } } }, + + { "id": "step2-container", "component": "Column", + "visible": { "fn": "equals", "args": [{ "path": "/wizard/currentStep" }, 2] }, + "children": ["s2-title", "s2-desc", "s2-receipt", "s2-vendor", "s2-notes", "s2-divider", "s2-actions"] }, + { "id": "s2-title", "component": "Text", "text": "Receipt Details", "variant": "h3" }, + { "id": "s2-desc", "component": "Text", "text": "Provide receipt information for your expense.", "variant": "body" }, + { "id": "s2-receipt", "component": "TextField", "label": "Receipt Number", "value": { "path": "/expense/receiptNumber" } }, + { "id": "s2-vendor", "component": "TextField", "label": "Vendor", "value": { "path": "/expense/vendor" } }, + { "id": "s2-notes", "component": "TextField", "label": "Notes", "value": { "path": "/expense/notes" } }, + { "id": "s2-divider", "component": "Divider" }, + { "id": "s2-actions", "component": "Row", "justify": "end", "children": ["btn-s2-back", "btn-s2-next"] }, + { "id": "btn-s2-back", "component": "Button", "label": "← Back", + "action": { "event": { "name": "a2h.wizard.back", "context": { "step": 2 } } } }, + { "id": "btn-s2-next", "component": "Button", "label": "Next →", "variant": "primary", + "action": { "event": { "name": "a2h.wizard.next", "context": { "step": 2, "intent": "COLLECT" } } } }, + + { "id": "step3-container", "component": "Column", + "visible": { "fn": "equals", "args": [{ "path": "/wizard/currentStep" }, 3] }, + "children": ["s3-title", "s3-desc", "s3-review-card", "s3-divider", "s3-actions"] }, + { "id": "s3-title", "component": "Text", "text": "Review Your Expense", "variant": "h3" }, + { "id": "s3-desc", "component": "Text", "text": "Please review the details below before submitting.", "variant": "body" }, + { "id": "s3-review-card", "component": "Card", "child": "s3-review-col" }, + { "id": "s3-review-col", "component": "Column", "children": [ + "rev-date", "rev-category", "rev-amount", "rev-description", + "rev-divider", "rev-receipt", "rev-vendor", "rev-notes" + ]}, + { "id": "rev-date", "component": "KeyValue", "label": "Date", "value": { "path": "/expense/date" } }, + { "id": "rev-category", "component": "KeyValue", "label": "Category", "value": { "path": "/expense/category" } }, + { "id": "rev-amount", "component": "KeyValue", "label": "Amount", "value": { "path": "/expense/amount" } }, + { "id": "rev-description", "component": "KeyValue", "label": "Description", "value": { "path": "/expense/description" } }, + { "id": "rev-divider", "component": "Divider" }, + { "id": "rev-receipt", "component": "KeyValue", "label": "Receipt #", "value": { "path": "/expense/receiptNumber" } }, + { "id": "rev-vendor", "component": "KeyValue", "label": "Vendor", "value": { "path": "/expense/vendor" } }, + { "id": "rev-notes", "component": "KeyValue", "label": "Notes", "value": { "path": "/expense/notes" } }, + { "id": "s3-divider", "component": "Divider" }, + { "id": "s3-actions", "component": "Row", "justify": "end", "children": ["btn-s3-back", "btn-s3-next"] }, + { "id": "btn-s3-back", "component": "Button", "label": "← Back", + "action": { "event": { "name": "a2h.wizard.back", "context": { "step": 3 } } } }, + { "id": "btn-s3-next", "component": "Button", "label": "Proceed to Submit →", "variant": "primary", + "action": { "event": { "name": "a2h.wizard.next", "context": { "step": 3, "intent": "INFORM" } } } }, + + { "id": "step4-container", "component": "Column", + "visible": { "fn": "equals", "args": [{ "path": "/wizard/currentStep" }, 4] }, + "children": ["s4-header-row", "s4-desc", "s4-summary-card", "s4-policy", "s4-divider", "s4-actions"] }, + { "id": "s4-header-row", "component": "Row", "children": ["s4-icon", "s4-title"], "align": "center" }, + { "id": "s4-icon", "component": "Icon", "name": "lock" }, + { "id": "s4-title", "component": "Text", "text": "Submit Expense Report", "variant": "h3" }, + { "id": "s4-desc", "component": "Text", "text": "By submitting, you confirm this expense complies with company policy.", "variant": "body" }, + { "id": "s4-summary-card", "component": "Card", "child": "s4-summary-col" }, + { "id": "s4-summary-col", "component": "Column", "children": ["auth-amount", "auth-category", "auth-vendor"] }, + { "id": "auth-amount", "component": "KeyValue", "label": "Amount", "value": { "path": "/expense/amount" } }, + { "id": "auth-category", "component": "KeyValue", "label": "Category", "value": { "path": "/expense/category" } }, + { "id": "auth-vendor", "component": "KeyValue", "label": "Vendor", "value": { "path": "/expense/vendor" } }, + { "id": "s4-policy", "component": "Text", "text": "⚠️ Expenses over $500 require manager co-approval.", "variant": "caption" }, + { "id": "s4-divider", "component": "Divider" }, + { "id": "s4-actions", "component": "Row", "justify": "end", "children": ["btn-cancel", "btn-submit"] }, + { "id": "btn-cancel", "component": "Button", "label": "Cancel", + "action": { "event": { "name": "a2h.authorize.reject", "context": { "interactionId": { "path": "/wizard/interactionId" }, "intent": "AUTHORIZE" } } } }, + { "id": "btn-submit", "component": "Button", "label": "Submit Expense", "variant": "primary", + "action": { "event": { "name": "a2h.authorize.approve", "context": { "interactionId": { "path": "/wizard/interactionId" }, "intent": "AUTHORIZE" } } } }, + + { "id": "submit-processing", "component": "Row", + "visible": { "fn": "equals", "args": [{ "path": "/wizard/currentStep" }, 5] }, + "children": ["submit-spinner", "submit-label"], "align": "center" }, + { "id": "submit-spinner", "component": "ProgressIndicator", "mode": "indeterminate" }, + { "id": "submit-label", "component": "Text", "text": { "path": "/wizard/processingLabel" }, "variant": "body" } + ] + } + }, + { + "_comment": "Initial data model — wizard starts at step 1", + "version": "v0.9.1", + "updateDataModel": { + "surfaceId": "a2h-expense-wizard-001", + "value": { + "wizard": { + "currentStep": 1, + "stepLabel": "Step 1 of 4 — Expense Details", + "interactionId": "exp-2026-0304-001", + "processingLabel": "Submitting expense report…" + }, + "expense": { + "date": "", + "category": [], + "amount": "", + "description": "", + "receiptNumber": "", + "vendor": "", + "notes": "" + } + } + } + } +] diff --git a/samples/a2h-prototypes/p5-wizard-v0.9.1-with-helper/index.html b/samples/a2h-prototypes/p5-wizard-v0.9.1-with-helper/index.html new file mode 100644 index 000000000..44af34e3e --- /dev/null +++ b/samples/a2h-prototypes/p5-wizard-v0.9.1-with-helper/index.html @@ -0,0 +1,380 @@ + + + + + + A2H Prototype 5 — Expense Wizard v0.9.1 (with Helper) + + + + + + + +

A2H × A2UI — P5: Expense Wizard v0.9.1

+ +
+ v0.9.0: 6 messages (4× updateComponents, ~100 components)  |  + v0.9.1: 3 messages (1× updateComponents, ~70 components)
+ Step transitions are pure data model updates — zero tree rebuilds! +
+ +
+
+
+
+
📨 Messages Received
+
+
+
+
📊 Data Model
+
+
+
+
📋 Event Log
+
+
+
+
+ + + + diff --git a/samples/a2h-prototypes/p5-wizard-v0.9.1-with-helper/wizard-with-helper.js b/samples/a2h-prototypes/p5-wizard-v0.9.1-with-helper/wizard-with-helper.js new file mode 100644 index 000000000..8394dde6b --- /dev/null +++ b/samples/a2h-prototypes/p5-wizard-v0.9.1-with-helper/wizard-with-helper.js @@ -0,0 +1,123 @@ +/** + * wizard-with-helper.js + * + * Shows how to compose a multi-step expense wizard using the a2h-a2ui helper + * library, combining createCollectSurface + createAuthorizeSurface into a + * single surface with visible bindings for step transitions. + * + * v0.9.0 approach: 6 messages (createSurface + updateDataModel + 4× updateComponents) + * Every step transition = full component tree rebuild + * Review step = 21 components (7× Row+Text+Text) + * Buttons = 2 components each (Button + child Text) + * + * v0.9.1 approach: 3 messages (createSurface + updateComponents + updateDataModel) + * Step transitions = updateDataModel: { "/wizard/currentStep": N } + * Review step = 7 KeyValue components (replaces 21) + * Buttons = 1 component each (Button.label) + * ALL steps in initial tree with visible bindings + * ZERO updateComponents after initial setup! + */ + +import { createCollectSurface, createAuthorizeSurface, toJsonl } from '../lib/a2h-a2ui.js'; + +const surfaceId = 'a2h-expense-wizard-001'; + +// --- Helper-based composition --- +// +// The helper library provides per-intent surface builders. +// For a wizard, we'd compose them on a single surface: + +// Step 1: COLLECT — expense basics +const step1 = createCollectSurface({ + surfaceId, + title: 'Expense Basics', + fields: [ + { id: 'date', label: 'Date (YYYY-MM-DD)', type: 'text', path: '/expense/date' }, + { id: 'category', label: 'Category', type: 'select', path: '/expense/category', + options: ['Travel', 'Meals', 'Equipment', 'Software', 'Office Supplies', 'Other'] }, + { id: 'amount', label: 'Amount ($)', type: 'text', path: '/expense/amount' }, + { id: 'description', label: 'Description', type: 'text', path: '/expense/description' }, + ], + submitLabel: 'Next →', + dataModel: { expense: { date: '', category: [], amount: '', description: '' } }, +}); + +// Step 4: AUTHORIZE — final approval +const step4 = createAuthorizeSurface({ + surfaceId, + title: 'Submit Expense Report', + description: 'By submitting, you confirm this expense complies with company policy.', + details: [ + { label: 'Amount', path: '/expense/amount' }, + { label: 'Category', path: '/expense/category' }, + { label: 'Vendor', path: '/expense/vendor' }, + ], + actions: [ + { id: 'btn-cancel', label: 'Cancel', variant: 'default', event: 'a2h.authorize.reject' }, + { id: 'btn-submit', label: 'Submit Expense', variant: 'primary', event: 'a2h.authorize.approve' }, + ], + dataModel: {}, +}); + +console.log('// Step 1 (COLLECT) surface from helper:'); +console.log(JSON.stringify(step1, null, 2)); +console.log('\n// Step 4 (AUTHORIZE) surface from helper:'); +console.log(JSON.stringify(step4, null, 2)); + +// --- The v0.9.1 approach (hand-crafted, see expense-wizard.json) --- +// +// Instead of separate surfaces per step, v0.9.1 puts ALL steps in one tree: +// +// createSurface +// updateComponents — ALL 4 steps + processing state in one tree +// step1-container: visible when /wizard/currentStep === 1 +// step2-container: visible when /wizard/currentStep === 2 +// step3-container: visible when /wizard/currentStep === 3 (KeyValue review) +// step4-container: visible when /wizard/currentStep === 4 (authorize) +// submit-processing: visible when /wizard/currentStep === 5 (ProgressIndicator) +// updateDataModel — initial state, currentStep: 1 +// +// Navigation is JUST data model updates: +// { "/wizard/currentStep": 2, "/wizard/stepLabel": "Step 2 of 4 — Receipt Info" } +// +// The renderer shows/hides step containers automatically. +// No updateComponents. No tree rebuilds. No component ID collisions. +// +// Future helper API could look like: +// +// createWizardSurface({ +// surfaceId, +// steps: [ +// { intent: 'COLLECT', title: 'Expense Basics', fields: [...] }, +// { intent: 'COLLECT', title: 'Receipt Details', fields: [...] }, +// { intent: 'INFORM', title: 'Review', review: true }, +// { intent: 'AUTHORIZE', title: 'Submit', details: [...] }, +// ], +// dataModel: { ... } +// }); +// +// This would generate the single-tree structure with visible bindings +// and step-label data model bindings automatically. + +// --- Comparison --- +// +// v0.9.0 expense-wizard.json: +// 6 messages: createSurface + updateDataModel + 4× updateComponents +// ~100 components total (many duplicated across steps) +// Each step = full tree replacement, error-prone ID management +// Review step: 21 components (7× Row + label Text + value Text) +// Back navigation: renderer must replay messages from scratch +// +// v0.9.1 expense-wizard.json: +// 3 messages: createSurface + updateComponents + updateDataModel +// ~70 components (no duplication, each step has unique IDs) +// Step transitions: pure data model update (currentStep: N) +// Review step: 7 KeyValue components (66% reduction) +// Back navigation: just set currentStep back — instant, no replay +// Processing state: ProgressIndicator (step 5) — native spinner +// Buttons: label prop (no child Text components needed) +// +// Message reduction: 6 → 3 (50%) +// Component reduction: ~100 → ~70 (30%) +// updateComponents per wizard: 4 → 1 (75% reduction) +// Mid-flow updateComponents: 4 → 0 (100% elimination!) From 07bad647d1e196a99af5ce20020ccc6ddf23f476 Mon Sep 17 00:00:00 2001 From: Zaf Date: Wed, 4 Mar 2026 16:14:05 +0000 Subject: [PATCH 18/22] docs: add v0.9.0 vs v0.9.1 comparison to DESIGN.md --- samples/a2h-prototypes/DESIGN.md | 98 ++++++++++++++++++++++++++++++++ samples/a2h-prototypes/README.md | 27 +++++++-- 2 files changed, 119 insertions(+), 6 deletions(-) diff --git a/samples/a2h-prototypes/DESIGN.md b/samples/a2h-prototypes/DESIGN.md index b36b7139e..ff7a9adad 100644 --- a/samples/a2h-prototypes/DESIGN.md +++ b/samples/a2h-prototypes/DESIGN.md @@ -385,6 +385,104 @@ A four-step wizard on a single persistent surface. Step 1 collects expense basic --- +## Before & After: v0.9.0 vs v0.9.1 + +We rebuilt all five prototypes using the proposed v0.9.1 features (`visible` binding, Button `label`, `ProgressIndicator`, `KeyValue`). Here's what changed — measured from the actual JSON files. + +### Summary Table + +| Prototype | v0.9.0 Msgs | v0.9.1 Msgs | v0.9.0 Components | v0.9.1 Components | v0.9.0 JSON Lines | v0.9.1 JSON Lines | Reduction | +|-----------|:-----------:|:-----------:|:------------------:|:------------------:|:------------------:|:------------------:|:---------:| +| P1 Approval | 3 | 3 | 28 | 21 | 223 | 192 | 14% lines, 25% components | +| P2 Escalation | 3 | 3 | 36 | 28 | 290 | 253 | 13% lines, 22% components | +| P3 Guided Input | 3 | 3 | 21 | 48 | 168 | 354 | +110%¹ (4 states vs 1) | +| P4 Pipeline | 9 | 6 | 45 | 32 | 180 | 162 | 10% lines, 29% components, 33% msgs | +| P5 Wizard | 6 | 3 | 82 | 59 | 210 | 141 | 33% lines, 28% components, 50% msgs | + +¹ P3 v0.9.1 is intentionally larger — it defines 4 complete UI states (edit → review → processing → success) in a single component tree, where v0.9.0 only defines the editing state. Per-state, v0.9.1 is more efficient. + +### Per-Prototype Breakdown + +#### P1: Approval Card (AUTHORIZE) + +**Demonstrates:** Financial transfer approval with detail display and approve/reject buttons. + +**v0.9.1 features used:** KeyValue (4 detail rows → 4 components instead of 12), Button `label` (2 buttons lose their Text children), `visible` binding (post-approval state change is a data model update, not a tree swap), ProgressIndicator (processing spinner). + +**Key improvement:** Post-action state transitions go from "send a full `updateComponents` replacing the button row" to "send `updateDataModel` with `state: processing`". The entire post-approval flow is zero component tree surgery. + +**Remaining gaps:** No TTL countdown timer. No mechanism for cryptographic evidence (passkey/OTP) — properly an A2H gateway concern. + +#### P2: Escalation Handoff (ESCALATE) + +**Demonstrates:** Customer service handoff with context preservation and multiple connection options. + +**v0.9.1 features used:** KeyValue (4 context rows), Button `label` (3 buttons), `visible` binding (connecting state), ProgressIndicator (connecting spinner). + +**Key improvement:** Same pattern as P1 — the "Connecting you to an agent..." state transition is a single data model update. The 3 connection buttons hide and the spinner appears without touching the component tree. + +**Remaining gaps:** No real-time queue position or ETA. Would benefit from a `Timer` or `Countdown` component for wait time estimates. + +#### P3: Guided Input Form (COLLECT) + +**Demonstrates:** Shipping address form with validation, pre-populated values, and a review-before-submit flow. + +**v0.9.1 features used:** `visible` binding (4 mutually exclusive states: edit → review → processing → success), KeyValue (7 review summary rows), Button `label` (4 buttons), ProgressIndicator (submit spinner). + +**Key improvement:** The review-before-submit pattern is impossible in v0.9.0 without server round-trips. v0.9.1 declares all 4 states upfront; transitions are purely client-side via data model. The review step alone saves 14 components (7 × KeyValue vs 7 × Row+Text+Text). + +**Remaining gaps:** The v0.9.1 file is larger overall because it declares all states upfront. This is the correct trade-off (fewer round-trips, declarative UI), but it means the initial payload is bigger. A `Stepper` or `Wizard` meta-component could reduce this further. + +#### P4: Deploy Pipeline (INFORM → AUTHORIZE) + +**Demonstrates:** Progressive deployment pipeline with real-time status updates and an approval gate. + +**v0.9.1 features used:** ProgressIndicator (native step status instead of emoji ⬜⏳✅), `visible` binding (approval card hidden until deploy step), KeyValue (deployment metadata), Button `label` (approve/rollback buttons). + +**Key improvement:** The most dramatic change — **zero `updateComponents` messages after initial setup**. v0.9.0 needed 4 `updateComponents` (one per step transition, plus injecting the approval card). v0.9.1 drives everything through `updateDataModel`. This is the difference between imperative and declarative UI. + +**Remaining gaps:** No determinate progress bar (ProgressIndicator `mode: "determinate"` with `value` binding would be ideal). Step timing/duration isn't tracked. + +#### P5: Expense Report Wizard (COLLECT → COLLECT → INFORM → AUTHORIZE) + +**Demonstrates:** Multi-step wizard chaining 4 A2H intents on a single persistent surface. + +**v0.9.1 features used:** `visible` binding (5 step containers + processing state), KeyValue (7 review rows in step 3, plus step 4 summary), Button `label` (6 buttons), ProgressIndicator (submit processing). + +**Key improvement:** 50% fewer messages (6 → 3). All step navigation is pure data model updates — `currentStep: 2` shows step 2, hides step 1. Back navigation is instant (no message replay). The review step drops from 21 components to 7. + +**Remaining gaps:** Step indicator ("Step 1 of 4") is still plain text — a dedicated `Stepper` component would add affordance. No form-level cross-step validation. The initial tree is large since all steps are declared upfront. + +### Recurring Patterns + +These issues appeared across multiple prototypes during development: + +1. **`variant: "label"` bug.** Several prototypes tried `variant: "label"` on Text components, which isn't a valid variant in the spec (valid values: `headline`, `title`, `body`, `caption`). This suggests the variant enum may need expansion, or that KeyValue's `label` sub-rendering should handle this case. + +2. **`MultipleChoice` vs `ChoicePicker` naming confusion.** Prototype authors (including LLMs generating A2UI) frequently reach for `MultipleChoice` — a component that doesn't exist. The actual component is `ChoicePicker`. This naming friction is worth a spec discussion. + +3. **`visible` binding is the single biggest improvement.** It transforms every prototype. P1 and P2 get cleaner state transitions. P3 gets a review-before-submit flow that's impossible in v0.9.0. P4 eliminates all mid-flow `updateComponents`. P5 cuts messages in half. If only one enhancement ships, it should be this one. + +4. **Button `label` is the biggest DX win per effort.** It's a tiny spec change (one optional string prop) that eliminates 2 components and 2 IDs per button across every single prototype. The cumulative reduction is significant: P5 alone saves 6 components. It also makes LLM-generated A2UI more reliable — fewer IDs to manage means fewer wiring mistakes. + +### Lessons for the Spec Team + +Based on building 10 prototypes (5 × 2 versions) with real JSON: + +1. **Ship `visible` binding first.** It's the only enhancement that changes the architectural model (imperative → declarative). Everything else is a convenience; this is a capability. Without it, every state transition requires a server round-trip and a full component tree replacement. With it, agents can declare complete interaction flows upfront and drive them through data alone. + +2. **Button `label` is free money.** Tiny spec change, universal impact, zero controversy. Every prototype benefits. Every LLM generating A2UI benefits more. Ship it alongside `visible`. + +3. **KeyValue matters most for review/summary UIs.** It's a 3:1 component reduction for detail displays. The prototypes that benefit most are P1 (approval details), P3 (review summary), and P5 (review step). If you have detail-heavy UIs, this is high-value. + +4. **ProgressIndicator fills a real gap, but it's lower priority.** Every prototype that fakes loading state with emoji (⏳) or text ("Processing...") would benefit. But the workarounds are tolerable. Ship it third. + +5. **Consider the initial payload trade-off.** The `visible` binding pattern front-loads complexity — all states are declared in the initial tree. P3 goes from 168 to 354 JSON lines. This is the right trade-off (fewer round-trips, declarative), but the spec should document this pattern explicitly so developers don't think bigger JSON = worse. + +6. **Naming matters.** `ChoicePicker` vs `MultipleChoice` confusion is real. The spec should consider aliasing or at minimum a prominent note. Similarly, Text `variant` values need documentation or expansion — `"label"` is what people reach for instinctively. + +--- + ## Recommendation ### Phase 1: Conventions (now) diff --git a/samples/a2h-prototypes/README.md b/samples/a2h-prototypes/README.md index 08884c0f2..612011fa8 100644 --- a/samples/a2h-prototypes/README.md +++ b/samples/a2h-prototypes/README.md @@ -24,14 +24,29 @@ Comparing the two versions side-by-side makes the case for each proposed enhance | Path | Description | |------|-------------| -| [DESIGN.md](./DESIGN.md) | Full design document — intent mapping, conventions, proposed enhancements | +| [DESIGN.md](./DESIGN.md) | Full design document — intent mapping, conventions, v0.9.0 vs v0.9.1 comparison | | [demo/](./demo/) | **All 5 intents on one page**, generated via the helper library | | [lib/a2h-a2ui.js](./lib/a2h-a2ui.js) | Helper library — generates A2UI v0.9 messages from A2H intent descriptions | -| [p1-approval-v0.9.0/](./p1-approval-v0.9.0/) | **AUTHORIZE** — Financial transfer approval card | -| [p2-escalation-v0.9.0/](./p2-escalation-v0.9.0/) | **ESCALATE** — Customer service handoff | -| [p3-guided-input-v0.9.0/](./p3-guided-input-v0.9.0/) | **COLLECT** — Shipping address form (⭐ cleanest prototype) | -| [p4-progress-intervention-v0.9.0/](./p4-progress-intervention-v0.9.0/) | **INFORM → AUTHORIZE** — Deploy pipeline with progressive updates | -| [p5-wizard-v0.9.0/](./p5-wizard-v0.9.0/) | **COLLECT → COLLECT → INFORM → AUTHORIZE** — Expense report wizard | + +### v0.9.0 Prototypes (current spec) + +| Path | Intent | Description | +|------|--------|-------------| +| [p1-approval-v0.9.0/](./p1-approval-v0.9.0/) | AUTHORIZE | Financial transfer approval card | +| [p2-escalation-v0.9.0/](./p2-escalation-v0.9.0/) | ESCALATE | Customer service handoff | +| [p3-guided-input-v0.9.0/](./p3-guided-input-v0.9.0/) | COLLECT | Shipping address form (⭐ cleanest prototype) | +| [p4-progress-intervention-v0.9.0/](./p4-progress-intervention-v0.9.0/) | INFORM → AUTHORIZE | Deploy pipeline with progressive updates | +| [p5-wizard-v0.9.0/](./p5-wizard-v0.9.0/) | COLLECT×2 → INFORM → AUTHORIZE | Expense report wizard | + +### v0.9.1 Prototypes (proposed enhancements) + +| Path | Intent | Description | +|------|--------|-------------| +| [p1-approval-v0.9.1-with-helper/](./p1-approval-v0.9.1-with-helper/) | AUTHORIZE | Same approval card — 25% fewer components | +| [p2-escalation-v0.9.1-with-helper/](./p2-escalation-v0.9.1-with-helper/) | ESCALATE | Same escalation — 22% fewer components | +| [p3-guided-input-v0.9.1-with-helper/](./p3-guided-input-v0.9.1-with-helper/) | COLLECT | Same form + review-before-submit flow (4 states) | +| [p4-progress-intervention-v0.9.1-with-helper/](./p4-progress-intervention-v0.9.1-with-helper/) | INFORM → AUTHORIZE | Same pipeline — zero mid-flow updateComponents | +| [p5-wizard-v0.9.1-with-helper/](./p5-wizard-v0.9.1-with-helper/) | COLLECT×2 → INFORM → AUTHORIZE | Same wizard — 50% fewer messages | ## Helper Library API From a8d2cb9abfe72cbd008bdc7bace5119a5c71b167 Mon Sep 17 00:00:00 2001 From: Zaf Date: Wed, 4 Mar 2026 18:01:41 +0000 Subject: [PATCH 19/22] docs: update DESIGN.md with critical review findings, fix stale ratings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add note about recurring bugs found across prototypes - Update P4 rating 3→4/5 (JSON was fixed during review) - Update P5 rating 3.5→4/5 (bugs fixed, note v0.9.1 improvement) - Frame recurring errors as spec feedback (what devs expect) --- samples/a2h-prototypes/DESIGN.md | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/samples/a2h-prototypes/DESIGN.md b/samples/a2h-prototypes/DESIGN.md index ff7a9adad..550f38e5a 100644 --- a/samples/a2h-prototypes/DESIGN.md +++ b/samples/a2h-prototypes/DESIGN.md @@ -308,6 +308,14 @@ npm package, TypeScript, zero dependencies. Works with any A2UI renderer. Genera Five prototypes validate the intent-to-surface mapping. Each is a standalone HTML file with a vanilla JS renderer plus the A2UI message sequence as JSON. +> **Note:** All v0.9.0 prototypes underwent critical developer and user review. Common bugs found across multiple prototypes: +> - `variant: "label"` on Text — not a valid v0.9 variant (fixed to `"caption"`) +> - `MultipleChoice` component — doesn't exist in basic catalog (fixed to `ChoicePicker`) +> - `_comment` fields — invalid per `additionalProperties: false` (removed) +> - Invalid icon names — some icons not in the catalog enum (fixed to valid alternatives) +> +> These recurring errors are instructive: they reveal what LLMs and developers *expect* from the spec. See [Lessons for the Spec Team](#lessons-for-the-spec-team) below. + ### P1: Approval Card (AUTHORIZE) — [p1-approval-v0.9.0/](./p1-approval-v0.9.0/) A financial transfer approval card. Shows transfer details (account, amount formatted with `formatCurrency`, description) in a detail section, with Approve and Reject buttons. Demonstrates the core AUTHORIZE lifecycle and `sendDataModel: true` pattern. @@ -338,7 +346,7 @@ A deployment pipeline that progressively updates: Build ✅ → Test ✅ → Sta **What users see:** A pipeline visualization with four steps. Each step animates from pending (⬜) to running (⏳) to complete (✅). At step 4, an approval card slides in with "Deploy to Production?" and Approve/Rollback buttons. -**Rating: 3/5.** Most technically interesting prototype — progressive updates and tree mutation are compelling. However, the JSON structure deviates from valid A2UI JSONL (missing `version` fields, combined message objects). Needs a conformance fix before sharing externally. Emoji-as-icons is fragile across platforms. +**Rating: 4/5.** Most technically interesting prototype — progressive updates and tree mutation are compelling. JSON structure was validated and fixed during critical review (proper `version` fields, separate messages, no `_comment` fields). Emoji-as-icons is fragile across platforms; the v0.9.1 version replaces them with ProgressIndicator. ### P5: Expense Report Wizard (COLLECT → COLLECT → INFORM → AUTHORIZE) — [p5-wizard-v0.9.0/](./p5-wizard-v0.9.0/) @@ -346,7 +354,7 @@ A four-step wizard on a single persistent surface. Step 1 collects expense basic **What users see:** A step indicator ("Step 1 of 4") with a title that changes per step. Form fields in steps 1-2, a summary table in step 3, and an approval card with a policy warning in step 4. Back/Next navigation. -**Rating: 3.5/5.** Ambitious and realistic multi-intent flow. Exposes every gap simultaneously — the review step's 21-component summary table desperately needs KeyValue, step navigation relies on fragile ID reuse, and the lack of conditional visibility makes the step-swap pattern verbose. +**Rating: 4/5.** Ambitious and realistic multi-intent flow. Exposes every gap simultaneously — the review step's 21-component summary table desperately needs KeyValue, step navigation relies on fragile ID reuse, and the lack of conditional visibility makes the step-swap pattern verbose. Critical review fixed `MultipleChoice`→`ChoicePicker`, `variant: "label"`→`"caption"`, and `_comment` fields. The v0.9.1 version is the most dramatic improvement of all five prototypes. --- From b51b6b20044e671095e089d1e16777b277f7f7e2 Mon Sep 17 00:00:00 2001 From: Zaf Date: Thu, 5 Mar 2026 19:04:50 +0000 Subject: [PATCH 20/22] fix: address 8 review issues in A2H prototypes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit RED fixes: - Remove all _comment fields from P4/P5 v0.9.1 JSON (violate additionalProperties: false) - P3 v0.9.0: rename event submitShipping → a2h.collect.submit (naming convention) - Helper lib: fix variant 'label' → 'caption' on Text components (invalid variant) YELLOW fixes: - DESIGN.md: update surface ID convention from colons to dashes (matching actual JSON files) - P3 v0.9.0: fix surfaceId collect-shipping → a2h-collect-shipping-001 - Remove dead allActionComps code from helper library - P1 v0.9.0: add formatCurrency example to transfer amount Note: P4 button IDs already use btn-{action} convention — no change needed. --- samples/a2h-prototypes/DESIGN.md | 8 +- samples/a2h-prototypes/lib/a2h-a2ui.js | 14 +- .../authorize-transfer.json | 3 +- .../collect-shipping.json | 8 +- .../deploy-pipeline.json | 441 ++++++++++-- .../expense-wizard.json | 665 +++++++++++++++--- 6 files changed, 953 insertions(+), 186 deletions(-) diff --git a/samples/a2h-prototypes/DESIGN.md b/samples/a2h-prototypes/DESIGN.md index 550f38e5a..b31950a6f 100644 --- a/samples/a2h-prototypes/DESIGN.md +++ b/samples/a2h-prototypes/DESIGN.md @@ -62,7 +62,7 @@ Every intent maps to the same three-message pattern: createSurface → updateDataModel → updateComponents ``` -Surface IDs follow: `a2h:{intent}:{interactionId}` +Surface IDs follow: `a2h-{intent}-{interactionId}` Event names follow: `a2h.{intent}.{action}` (e.g., `a2h.authorize.approve`) --- @@ -180,10 +180,10 @@ These require no spec changes — just documented best practices. ### Surface ID Convention ``` -a2h:{intentType}:{interactionId} +a2h-{intentType}-{interactionId} ``` -Example: `a2h:authorize:01936f8a-7b2c-7000-8000-000000000001` +Example: `a2h-authorize-01936f8a-7b2c-7000-8000-000000000001` ### Component ID Convention @@ -286,7 +286,7 @@ const response = parseActionEvent(event); ```typescript // Surface/component ID generation -makeSurfaceId('authorize', interactionId) // → "a2h:authorize:01936f8a-..." +makeSurfaceId('authorize', interactionId) // → "a2h-authorize-01936f8a-..." makeFieldId('email') // → "field-email" // JSONL serialization diff --git a/samples/a2h-prototypes/lib/a2h-a2ui.js b/samples/a2h-prototypes/lib/a2h-a2ui.js index 59672363c..d3e6cf802 100644 --- a/samples/a2h-prototypes/lib/a2h-a2ui.js +++ b/samples/a2h-prototypes/lib/a2h-a2ui.js @@ -109,7 +109,7 @@ function makeDetailRow(id, label, valuePath) { const valId = `${id}-val`; return [ { id: rowId, component: 'Row', children: [labelId, valId] }, - { id: labelId, component: 'Text', text: label, variant: 'label' }, + { id: labelId, component: 'Text', text: label, variant: 'caption' }, { id: valId, component: 'Text', text: typeof valuePath === 'string' && valuePath.startsWith('/') ? { path: valuePath } : (valuePath ?? ''), variant: 'body' }, ]; } @@ -172,12 +172,6 @@ export function createAuthorizeSurface({ surfaceId, title, description, details actionChildren: actionComps.filter(c => c.component === 'Button' || c.component === 'Text'), }); - // Flatten: action buttons need both Text and Button components - const allActionComps = []; - for (const a of actionDefs) { - allActionComps.push(...makeButton(a.id, a.label, a.variant || 'default', a.event, {})); - } - return [ createSurfaceMsg(surfaceId), updateDataModelMsg(surfaceId, dataModel), @@ -205,7 +199,7 @@ export function createCollectSurface({ surfaceId, title, fields = [], submitLabe for (const f of fields) { const labelId = `field-${f.id}-label`; const fieldId = `field-${f.id}`; - fieldComponents.push({ id: labelId, component: 'Text', text: f.label, variant: 'label' }); + fieldComponents.push({ id: labelId, component: 'Text', text: f.label, variant: 'caption' }); if (f.type === 'select') { fieldComponents.push({ @@ -267,7 +261,7 @@ export function createInformSurface({ surfaceId, title, items = [], dataModel = bodyComponents.push( { id: rowId, component: 'Row', children: [iconId, labelId, valId] }, { id: iconId, component: 'Text', text: item.icon }, - { id: labelId, component: 'Text', text: item.label, variant: 'label' }, + { id: labelId, component: 'Text', text: item.label, variant: 'caption' }, { id: valId, component: 'Text', text: item.value, variant: 'body' }, ); } else { @@ -326,7 +320,7 @@ export function createEscalateSurface({ surfaceId, reason, context, options, dat bodyComponents.push( { id: ctxCardId, component: 'Card', child: ctxColId }, { id: ctxColId, component: 'Column', children: [ctxLabelId, ctxTextId] }, - { id: ctxLabelId, component: 'Text', text: 'Context', variant: 'label' }, + { id: ctxLabelId, component: 'Text', text: 'Context', variant: 'caption' }, { id: ctxTextId, component: 'Text', text: context, variant: 'body' }, ); } diff --git a/samples/a2h-prototypes/p1-approval-v0.9.0/authorize-transfer.json b/samples/a2h-prototypes/p1-approval-v0.9.0/authorize-transfer.json index 56dfa6590..39159565f 100644 --- a/samples/a2h-prototypes/p1-approval-v0.9.0/authorize-transfer.json +++ b/samples/a2h-prototypes/p1-approval-v0.9.0/authorize-transfer.json @@ -208,7 +208,8 @@ "action": "Transfer Funds", "fromAccount": "Checking (****4521)", "toAccount": "Savings (****7890)", - "amount": "$500.00" + "amount": 500.00, + "amountFormatted": { "formatCurrency": { "path": "/transfer/amount", "currency": "USD" } } }, "meta": { "interactionId": "txn-2026-0304-001", diff --git a/samples/a2h-prototypes/p3-guided-input-v0.9.0/collect-shipping.json b/samples/a2h-prototypes/p3-guided-input-v0.9.0/collect-shipping.json index 6040c8702..a8d959e60 100644 --- a/samples/a2h-prototypes/p3-guided-input-v0.9.0/collect-shipping.json +++ b/samples/a2h-prototypes/p3-guided-input-v0.9.0/collect-shipping.json @@ -2,7 +2,7 @@ { "version": "v0.9", "createSurface": { - "surfaceId": "collect-shipping", + "surfaceId": "a2h-collect-shipping-001", "catalogId": "https://a2ui.org/specification/v0_9/basic_catalog.json", "sendDataModel": true } @@ -10,7 +10,7 @@ { "version": "v0.9", "updateComponents": { - "surfaceId": "collect-shipping", + "surfaceId": "a2h-collect-shipping-001", "components": [ { "id": "root", @@ -140,7 +140,7 @@ "child": "submit-btn-text", "action": { "event": { - "name": "submitShipping", + "name": "a2h.collect.submit", "context": {} } } @@ -151,7 +151,7 @@ { "version": "v0.9", "updateDataModel": { - "surfaceId": "collect-shipping", + "surfaceId": "a2h-collect-shipping-001", "value": { "shipping": { "name": "Jane Doe", diff --git a/samples/a2h-prototypes/p4-progress-intervention-v0.9.1-with-helper/deploy-pipeline.json b/samples/a2h-prototypes/p4-progress-intervention-v0.9.1-with-helper/deploy-pipeline.json index 4cc8c5029..fd49e7895 100644 --- a/samples/a2h-prototypes/p4-progress-intervention-v0.9.1-with-helper/deploy-pipeline.json +++ b/samples/a2h-prototypes/p4-progress-intervention-v0.9.1-with-helper/deploy-pipeline.json @@ -12,59 +12,301 @@ "updateComponents": { "surfaceId": "a2h-inform-deploy-pipeline-001", "components": [ - { "id": "root", "component": "Card", "child": "main-col" }, - { "id": "main-col", "component": "Column", "children": [ - "header-row", "divider-1", "meta-card", "divider-2", - "step-build", "step-test", "step-stage", "step-deploy", - "divider-3", "approve-card" - ]}, - - { "id": "header-row", "component": "Row", "children": ["header-icon", "header-text"], "align": "center" }, - { "id": "header-icon", "component": "Icon", "name": "rocket_launch" }, - { "id": "header-text", "component": "Text", "text": "Deployment Pipeline", "variant": "h3" }, - - { "id": "divider-1", "component": "Divider" }, - - { "id": "meta-card", "component": "Card", "child": "meta-col" }, - { "id": "meta-col", "component": "Column", "children": ["meta-app", "meta-commit", "meta-env"] }, - { "id": "meta-app", "component": "KeyValue", "label": "Application", "value": { "path": "/pipeline/app" } }, - { "id": "meta-commit", "component": "KeyValue", "label": "Commit", "value": { "path": "/pipeline/commit" } }, - { "id": "meta-env", "component": "KeyValue", "label": "Environment", "value": { "path": "/pipeline/environment" } }, - - { "id": "divider-2", "component": "Divider" }, - - { "id": "step-build", "component": "Row", "children": ["build-progress", "build-label"], "align": "center" }, - { "id": "build-progress", "component": "ProgressIndicator", "mode": { "path": "/steps/build/mode" }, "value": { "path": "/steps/build/value" } }, - { "id": "build-label", "component": "Text", "text": { "path": "/steps/build/label" }, "variant": "body" }, - - { "id": "step-test", "component": "Row", "children": ["test-progress", "test-label"], "align": "center" }, - { "id": "test-progress", "component": "ProgressIndicator", "mode": { "path": "/steps/test/mode" }, "value": { "path": "/steps/test/value" } }, - { "id": "test-label", "component": "Text", "text": { "path": "/steps/test/label" }, "variant": "body" }, - - { "id": "step-stage", "component": "Row", "children": ["stage-progress", "stage-label"], "align": "center" }, - { "id": "stage-progress", "component": "ProgressIndicator", "mode": { "path": "/steps/stage/mode" }, "value": { "path": "/steps/stage/value" } }, - { "id": "stage-label", "component": "Text", "text": { "path": "/steps/stage/label" }, "variant": "body" }, - - { "id": "step-deploy", "component": "Row", "children": ["deploy-progress", "deploy-label"], "align": "center" }, - { "id": "deploy-progress", "component": "ProgressIndicator", "mode": { "path": "/steps/deploy/mode" }, "value": { "path": "/steps/deploy/value" } }, - { "id": "deploy-label", "component": "Text", "text": { "path": "/steps/deploy/label" }, "variant": "body" }, - - { "id": "divider-3", "component": "Divider", - "visible": { "fn": "equals", "args": [{ "path": "/approvalVisible" }, true] } }, - - { "id": "approve-card", "component": "Card", "child": "approve-col", - "visible": { "fn": "equals", "args": [{ "path": "/approvalVisible" }, true] } }, - { "id": "approve-col", "component": "Column", "children": ["approve-title", "approve-desc", "approve-actions"] }, - { "id": "approve-title", "component": "Text", "text": "⚠️ Deploy to production?", "variant": "h3" }, - { "id": "approve-desc", "component": "Text", "text": { "path": "/approvalSummary" }, "variant": "body" }, - { "id": "approve-actions", "component": "Row", "children": ["btn-rollback", "btn-approve"], "justify": "end" }, - { "id": "btn-rollback", "component": "Button", "label": "Rollback", "action": { "event": { "name": "a2h.authorize.rollback", "context": { "app": { "path": "/pipeline/app" }, "commit": { "path": "/pipeline/commit" } } } } }, - { "id": "btn-approve", "component": "Button", "label": "Approve", "variant": "primary", "action": { "event": { "name": "a2h.authorize.approve", "context": { "app": { "path": "/pipeline/app" }, "commit": { "path": "/pipeline/commit" } } } } } + { + "id": "root", + "component": "Card", + "child": "main-col" + }, + { + "id": "main-col", + "component": "Column", + "children": [ + "header-row", + "divider-1", + "meta-card", + "divider-2", + "step-build", + "step-test", + "step-stage", + "step-deploy", + "divider-3", + "approve-card" + ] + }, + { + "id": "header-row", + "component": "Row", + "children": [ + "header-icon", + "header-text" + ], + "align": "center" + }, + { + "id": "header-icon", + "component": "Icon", + "name": "rocket_launch" + }, + { + "id": "header-text", + "component": "Text", + "text": "Deployment Pipeline", + "variant": "h3" + }, + { + "id": "divider-1", + "component": "Divider" + }, + { + "id": "meta-card", + "component": "Card", + "child": "meta-col" + }, + { + "id": "meta-col", + "component": "Column", + "children": [ + "meta-app", + "meta-commit", + "meta-env" + ] + }, + { + "id": "meta-app", + "component": "KeyValue", + "label": "Application", + "value": { + "path": "/pipeline/app" + } + }, + { + "id": "meta-commit", + "component": "KeyValue", + "label": "Commit", + "value": { + "path": "/pipeline/commit" + } + }, + { + "id": "meta-env", + "component": "KeyValue", + "label": "Environment", + "value": { + "path": "/pipeline/environment" + } + }, + { + "id": "divider-2", + "component": "Divider" + }, + { + "id": "step-build", + "component": "Row", + "children": [ + "build-progress", + "build-label" + ], + "align": "center" + }, + { + "id": "build-progress", + "component": "ProgressIndicator", + "mode": { + "path": "/steps/build/mode" + }, + "value": { + "path": "/steps/build/value" + } + }, + { + "id": "build-label", + "component": "Text", + "text": { + "path": "/steps/build/label" + }, + "variant": "body" + }, + { + "id": "step-test", + "component": "Row", + "children": [ + "test-progress", + "test-label" + ], + "align": "center" + }, + { + "id": "test-progress", + "component": "ProgressIndicator", + "mode": { + "path": "/steps/test/mode" + }, + "value": { + "path": "/steps/test/value" + } + }, + { + "id": "test-label", + "component": "Text", + "text": { + "path": "/steps/test/label" + }, + "variant": "body" + }, + { + "id": "step-stage", + "component": "Row", + "children": [ + "stage-progress", + "stage-label" + ], + "align": "center" + }, + { + "id": "stage-progress", + "component": "ProgressIndicator", + "mode": { + "path": "/steps/stage/mode" + }, + "value": { + "path": "/steps/stage/value" + } + }, + { + "id": "stage-label", + "component": "Text", + "text": { + "path": "/steps/stage/label" + }, + "variant": "body" + }, + { + "id": "step-deploy", + "component": "Row", + "children": [ + "deploy-progress", + "deploy-label" + ], + "align": "center" + }, + { + "id": "deploy-progress", + "component": "ProgressIndicator", + "mode": { + "path": "/steps/deploy/mode" + }, + "value": { + "path": "/steps/deploy/value" + } + }, + { + "id": "deploy-label", + "component": "Text", + "text": { + "path": "/steps/deploy/label" + }, + "variant": "body" + }, + { + "id": "divider-3", + "component": "Divider", + "visible": { + "fn": "equals", + "args": [ + { + "path": "/approvalVisible" + }, + true + ] + } + }, + { + "id": "approve-card", + "component": "Card", + "child": "approve-col", + "visible": { + "fn": "equals", + "args": [ + { + "path": "/approvalVisible" + }, + true + ] + } + }, + { + "id": "approve-col", + "component": "Column", + "children": [ + "approve-title", + "approve-desc", + "approve-actions" + ] + }, + { + "id": "approve-title", + "component": "Text", + "text": "\u26a0\ufe0f Deploy to production?", + "variant": "h3" + }, + { + "id": "approve-desc", + "component": "Text", + "text": { + "path": "/approvalSummary" + }, + "variant": "body" + }, + { + "id": "approve-actions", + "component": "Row", + "children": [ + "btn-rollback", + "btn-approve" + ], + "justify": "end" + }, + { + "id": "btn-rollback", + "component": "Button", + "label": "Rollback", + "action": { + "event": { + "name": "a2h.authorize.rollback", + "context": { + "app": { + "path": "/pipeline/app" + }, + "commit": { + "path": "/pipeline/commit" + } + } + } + } + }, + { + "id": "btn-approve", + "component": "Button", + "label": "Approve", + "variant": "primary", + "action": { + "event": { + "name": "a2h.authorize.approve", + "context": { + "app": { + "path": "/pipeline/app" + }, + "commit": { + "path": "/pipeline/commit" + } + } + } + } + } ] } }, { - "_comment": "Step 1: Pipeline started — Build running", "version": "v0.9.1", "updateDataModel": { "surfaceId": "a2h-inform-deploy-pipeline-001", @@ -79,16 +321,31 @@ "approvalVisible": false, "approvalSummary": "", "steps": { - "build": { "mode": "indeterminate", "value": 0, "label": "Build — running…" }, - "test": { "mode": "none", "value": 0, "label": "Test" }, - "stage": { "mode": "none", "value": 0, "label": "Stage" }, - "deploy": { "mode": "none", "value": 0, "label": "Deploy" } + "build": { + "mode": "indeterminate", + "value": 0, + "label": "Build \u2014 running\u2026" + }, + "test": { + "mode": "none", + "value": 0, + "label": "Test" + }, + "stage": { + "mode": "none", + "value": 0, + "label": "Stage" + }, + "deploy": { + "mode": "none", + "value": 0, + "label": "Deploy" + } } } } }, { - "_comment": "Step 2: Build complete, Test running", "version": "v0.9.1", "updateDataModel": { "surfaceId": "a2h-inform-deploy-pipeline-001", @@ -103,16 +360,31 @@ "approvalVisible": false, "approvalSummary": "", "steps": { - "build": { "mode": "complete", "value": 1, "label": "Build — 1m 12s" }, - "test": { "mode": "indeterminate", "value": 0, "label": "Test — running…" }, - "stage": { "mode": "none", "value": 0, "label": "Stage" }, - "deploy": { "mode": "none", "value": 0, "label": "Deploy" } + "build": { + "mode": "complete", + "value": 1, + "label": "Build \u2014 1m 12s" + }, + "test": { + "mode": "indeterminate", + "value": 0, + "label": "Test \u2014 running\u2026" + }, + "stage": { + "mode": "none", + "value": 0, + "label": "Stage" + }, + "deploy": { + "mode": "none", + "value": 0, + "label": "Deploy" + } } } } }, { - "_comment": "Step 3: Test complete, Stage running", "version": "v0.9.1", "updateDataModel": { "surfaceId": "a2h-inform-deploy-pipeline-001", @@ -127,16 +399,31 @@ "approvalVisible": false, "approvalSummary": "", "steps": { - "build": { "mode": "complete", "value": 1, "label": "Build — 1m 12s" }, - "test": { "mode": "complete", "value": 1, "label": "Test — 48/48 passed" }, - "stage": { "mode": "indeterminate", "value": 0, "label": "Stage — running…" }, - "deploy": { "mode": "none", "value": 0, "label": "Deploy" } + "build": { + "mode": "complete", + "value": 1, + "label": "Build \u2014 1m 12s" + }, + "test": { + "mode": "complete", + "value": 1, + "label": "Test \u2014 48/48 passed" + }, + "stage": { + "mode": "indeterminate", + "value": 0, + "label": "Stage \u2014 running\u2026" + }, + "deploy": { + "mode": "none", + "value": 0, + "label": "Deploy" + } } } } }, { - "_comment": "Step 4: Stage complete, Deploy awaiting approval — approval card becomes visible!", "version": "v0.9.1", "updateDataModel": { "surfaceId": "a2h-inform-deploy-pipeline-001", @@ -149,12 +436,28 @@ "startedAt": "2026-03-04T07:30:00Z" }, "approvalVisible": true, - "approvalSummary": "acme-web-v2.4.1 (a3f9c21) • 48/48 tests passed • Staged OK at stage.acme.dev", + "approvalSummary": "acme-web-v2.4.1 (a3f9c21) \u2022 48/48 tests passed \u2022 Staged OK at stage.acme.dev", "steps": { - "build": { "mode": "complete", "value": 1, "label": "Build — 1m 12s" }, - "test": { "mode": "complete", "value": 1, "label": "Test — 48/48 passed" }, - "stage": { "mode": "complete", "value": 1, "label": "Stage — live at stage.acme.dev" }, - "deploy": { "mode": "paused", "value": 0, "label": "Deploy — awaiting approval" } + "build": { + "mode": "complete", + "value": 1, + "label": "Build \u2014 1m 12s" + }, + "test": { + "mode": "complete", + "value": 1, + "label": "Test \u2014 48/48 passed" + }, + "stage": { + "mode": "complete", + "value": 1, + "label": "Stage \u2014 live at stage.acme.dev" + }, + "deploy": { + "mode": "paused", + "value": 0, + "label": "Deploy \u2014 awaiting approval" + } } } } diff --git a/samples/a2h-prototypes/p5-wizard-v0.9.1-with-helper/expense-wizard.json b/samples/a2h-prototypes/p5-wizard-v0.9.1-with-helper/expense-wizard.json index a0b832637..366bb61cf 100644 --- a/samples/a2h-prototypes/p5-wizard-v0.9.1-with-helper/expense-wizard.json +++ b/samples/a2h-prototypes/p5-wizard-v0.9.1-with-helper/expense-wizard.json @@ -12,119 +12,588 @@ "updateComponents": { "surfaceId": "a2h-expense-wizard-001", "components": [ - { "id": "root", "component": "Card", "child": "main-col" }, - { "id": "main-col", "component": "Column", "children": [ - "step-indicator", "divider-top", - "step1-container", "step2-container", "step3-container", "step4-container" - ]}, - - { "id": "step-indicator", "component": "Text", "text": { "path": "/wizard/stepLabel" }, "variant": "caption" }, - { "id": "divider-top", "component": "Divider" }, - - { "id": "step1-container", "component": "Column", - "visible": { "fn": "equals", "args": [{ "path": "/wizard/currentStep" }, 1] }, - "children": ["s1-title", "s1-desc", "s1-date", "s1-category", "s1-amount", "s1-description", "s1-divider", "s1-actions"] }, - { "id": "s1-title", "component": "Text", "text": "Expense Basics", "variant": "h3" }, - { "id": "s1-desc", "component": "Text", "text": "Enter the basic details for your expense report.", "variant": "body" }, - { "id": "s1-date", "component": "TextField", "label": "Date (YYYY-MM-DD)", "value": { "path": "/expense/date" } }, - { "id": "s1-category", "component": "ChoicePicker", "label": "Category", - "value": { "path": "/expense/category" }, + { + "id": "root", + "component": "Card", + "child": "main-col" + }, + { + "id": "main-col", + "component": "Column", + "children": [ + "step-indicator", + "divider-top", + "step1-container", + "step2-container", + "step3-container", + "step4-container" + ] + }, + { + "id": "step-indicator", + "component": "Text", + "text": { + "path": "/wizard/stepLabel" + }, + "variant": "caption" + }, + { + "id": "divider-top", + "component": "Divider" + }, + { + "id": "step1-container", + "component": "Column", + "visible": { + "fn": "equals", + "args": [ + { + "path": "/wizard/currentStep" + }, + 1 + ] + }, + "children": [ + "s1-title", + "s1-desc", + "s1-date", + "s1-category", + "s1-amount", + "s1-description", + "s1-divider", + "s1-actions" + ] + }, + { + "id": "s1-title", + "component": "Text", + "text": "Expense Basics", + "variant": "h3" + }, + { + "id": "s1-desc", + "component": "Text", + "text": "Enter the basic details for your expense report.", + "variant": "body" + }, + { + "id": "s1-date", + "component": "TextField", + "label": "Date (YYYY-MM-DD)", + "value": { + "path": "/expense/date" + } + }, + { + "id": "s1-category", + "component": "ChoicePicker", + "label": "Category", + "value": { + "path": "/expense/category" + }, "variant": "mutuallyExclusive", "options": [ - { "label": "Travel", "value": "Travel" }, - { "label": "Meals", "value": "Meals" }, - { "label": "Equipment", "value": "Equipment" }, - { "label": "Software", "value": "Software" }, - { "label": "Office Supplies", "value": "Office Supplies" }, - { "label": "Other", "value": "Other" } + { + "label": "Travel", + "value": "Travel" + }, + { + "label": "Meals", + "value": "Meals" + }, + { + "label": "Equipment", + "value": "Equipment" + }, + { + "label": "Software", + "value": "Software" + }, + { + "label": "Office Supplies", + "value": "Office Supplies" + }, + { + "label": "Other", + "value": "Other" + } + ] + }, + { + "id": "s1-amount", + "component": "TextField", + "label": "Amount ($)", + "value": { + "path": "/expense/amount" + } + }, + { + "id": "s1-description", + "component": "TextField", + "label": "Description", + "value": { + "path": "/expense/description" + } + }, + { + "id": "s1-divider", + "component": "Divider" + }, + { + "id": "s1-actions", + "component": "Row", + "justify": "end", + "children": [ + "btn-s1-next" + ] + }, + { + "id": "btn-s1-next", + "component": "Button", + "label": "Next \u2192", + "variant": "primary", + "action": { + "event": { + "name": "a2h.wizard.next", + "context": { + "step": 1, + "intent": "COLLECT" + } + } + } + }, + { + "id": "step2-container", + "component": "Column", + "visible": { + "fn": "equals", + "args": [ + { + "path": "/wizard/currentStep" + }, + 2 + ] + }, + "children": [ + "s2-title", + "s2-desc", + "s2-receipt", + "s2-vendor", + "s2-notes", + "s2-divider", + "s2-actions" + ] + }, + { + "id": "s2-title", + "component": "Text", + "text": "Receipt Details", + "variant": "h3" + }, + { + "id": "s2-desc", + "component": "Text", + "text": "Provide receipt information for your expense.", + "variant": "body" + }, + { + "id": "s2-receipt", + "component": "TextField", + "label": "Receipt Number", + "value": { + "path": "/expense/receiptNumber" + } + }, + { + "id": "s2-vendor", + "component": "TextField", + "label": "Vendor", + "value": { + "path": "/expense/vendor" + } + }, + { + "id": "s2-notes", + "component": "TextField", + "label": "Notes", + "value": { + "path": "/expense/notes" + } + }, + { + "id": "s2-divider", + "component": "Divider" + }, + { + "id": "s2-actions", + "component": "Row", + "justify": "end", + "children": [ + "btn-s2-back", + "btn-s2-next" + ] + }, + { + "id": "btn-s2-back", + "component": "Button", + "label": "\u2190 Back", + "action": { + "event": { + "name": "a2h.wizard.back", + "context": { + "step": 2 + } + } + } + }, + { + "id": "btn-s2-next", + "component": "Button", + "label": "Next \u2192", + "variant": "primary", + "action": { + "event": { + "name": "a2h.wizard.next", + "context": { + "step": 2, + "intent": "COLLECT" + } + } + } + }, + { + "id": "step3-container", + "component": "Column", + "visible": { + "fn": "equals", + "args": [ + { + "path": "/wizard/currentStep" + }, + 3 + ] + }, + "children": [ + "s3-title", + "s3-desc", + "s3-review-card", + "s3-divider", + "s3-actions" + ] + }, + { + "id": "s3-title", + "component": "Text", + "text": "Review Your Expense", + "variant": "h3" + }, + { + "id": "s3-desc", + "component": "Text", + "text": "Please review the details below before submitting.", + "variant": "body" + }, + { + "id": "s3-review-card", + "component": "Card", + "child": "s3-review-col" + }, + { + "id": "s3-review-col", + "component": "Column", + "children": [ + "rev-date", + "rev-category", + "rev-amount", + "rev-description", + "rev-divider", + "rev-receipt", + "rev-vendor", + "rev-notes" ] }, - { "id": "s1-amount", "component": "TextField", "label": "Amount ($)", "value": { "path": "/expense/amount" } }, - { "id": "s1-description", "component": "TextField", "label": "Description", "value": { "path": "/expense/description" } }, - { "id": "s1-divider", "component": "Divider" }, - { "id": "s1-actions", "component": "Row", "justify": "end", "children": ["btn-s1-next"] }, - { "id": "btn-s1-next", "component": "Button", "label": "Next →", "variant": "primary", - "action": { "event": { "name": "a2h.wizard.next", "context": { "step": 1, "intent": "COLLECT" } } } }, - - { "id": "step2-container", "component": "Column", - "visible": { "fn": "equals", "args": [{ "path": "/wizard/currentStep" }, 2] }, - "children": ["s2-title", "s2-desc", "s2-receipt", "s2-vendor", "s2-notes", "s2-divider", "s2-actions"] }, - { "id": "s2-title", "component": "Text", "text": "Receipt Details", "variant": "h3" }, - { "id": "s2-desc", "component": "Text", "text": "Provide receipt information for your expense.", "variant": "body" }, - { "id": "s2-receipt", "component": "TextField", "label": "Receipt Number", "value": { "path": "/expense/receiptNumber" } }, - { "id": "s2-vendor", "component": "TextField", "label": "Vendor", "value": { "path": "/expense/vendor" } }, - { "id": "s2-notes", "component": "TextField", "label": "Notes", "value": { "path": "/expense/notes" } }, - { "id": "s2-divider", "component": "Divider" }, - { "id": "s2-actions", "component": "Row", "justify": "end", "children": ["btn-s2-back", "btn-s2-next"] }, - { "id": "btn-s2-back", "component": "Button", "label": "← Back", - "action": { "event": { "name": "a2h.wizard.back", "context": { "step": 2 } } } }, - { "id": "btn-s2-next", "component": "Button", "label": "Next →", "variant": "primary", - "action": { "event": { "name": "a2h.wizard.next", "context": { "step": 2, "intent": "COLLECT" } } } }, - - { "id": "step3-container", "component": "Column", - "visible": { "fn": "equals", "args": [{ "path": "/wizard/currentStep" }, 3] }, - "children": ["s3-title", "s3-desc", "s3-review-card", "s3-divider", "s3-actions"] }, - { "id": "s3-title", "component": "Text", "text": "Review Your Expense", "variant": "h3" }, - { "id": "s3-desc", "component": "Text", "text": "Please review the details below before submitting.", "variant": "body" }, - { "id": "s3-review-card", "component": "Card", "child": "s3-review-col" }, - { "id": "s3-review-col", "component": "Column", "children": [ - "rev-date", "rev-category", "rev-amount", "rev-description", - "rev-divider", "rev-receipt", "rev-vendor", "rev-notes" - ]}, - { "id": "rev-date", "component": "KeyValue", "label": "Date", "value": { "path": "/expense/date" } }, - { "id": "rev-category", "component": "KeyValue", "label": "Category", "value": { "path": "/expense/category" } }, - { "id": "rev-amount", "component": "KeyValue", "label": "Amount", "value": { "path": "/expense/amount" } }, - { "id": "rev-description", "component": "KeyValue", "label": "Description", "value": { "path": "/expense/description" } }, - { "id": "rev-divider", "component": "Divider" }, - { "id": "rev-receipt", "component": "KeyValue", "label": "Receipt #", "value": { "path": "/expense/receiptNumber" } }, - { "id": "rev-vendor", "component": "KeyValue", "label": "Vendor", "value": { "path": "/expense/vendor" } }, - { "id": "rev-notes", "component": "KeyValue", "label": "Notes", "value": { "path": "/expense/notes" } }, - { "id": "s3-divider", "component": "Divider" }, - { "id": "s3-actions", "component": "Row", "justify": "end", "children": ["btn-s3-back", "btn-s3-next"] }, - { "id": "btn-s3-back", "component": "Button", "label": "← Back", - "action": { "event": { "name": "a2h.wizard.back", "context": { "step": 3 } } } }, - { "id": "btn-s3-next", "component": "Button", "label": "Proceed to Submit →", "variant": "primary", - "action": { "event": { "name": "a2h.wizard.next", "context": { "step": 3, "intent": "INFORM" } } } }, - - { "id": "step4-container", "component": "Column", - "visible": { "fn": "equals", "args": [{ "path": "/wizard/currentStep" }, 4] }, - "children": ["s4-header-row", "s4-desc", "s4-summary-card", "s4-policy", "s4-divider", "s4-actions"] }, - { "id": "s4-header-row", "component": "Row", "children": ["s4-icon", "s4-title"], "align": "center" }, - { "id": "s4-icon", "component": "Icon", "name": "lock" }, - { "id": "s4-title", "component": "Text", "text": "Submit Expense Report", "variant": "h3" }, - { "id": "s4-desc", "component": "Text", "text": "By submitting, you confirm this expense complies with company policy.", "variant": "body" }, - { "id": "s4-summary-card", "component": "Card", "child": "s4-summary-col" }, - { "id": "s4-summary-col", "component": "Column", "children": ["auth-amount", "auth-category", "auth-vendor"] }, - { "id": "auth-amount", "component": "KeyValue", "label": "Amount", "value": { "path": "/expense/amount" } }, - { "id": "auth-category", "component": "KeyValue", "label": "Category", "value": { "path": "/expense/category" } }, - { "id": "auth-vendor", "component": "KeyValue", "label": "Vendor", "value": { "path": "/expense/vendor" } }, - { "id": "s4-policy", "component": "Text", "text": "⚠️ Expenses over $500 require manager co-approval.", "variant": "caption" }, - { "id": "s4-divider", "component": "Divider" }, - { "id": "s4-actions", "component": "Row", "justify": "end", "children": ["btn-cancel", "btn-submit"] }, - { "id": "btn-cancel", "component": "Button", "label": "Cancel", - "action": { "event": { "name": "a2h.authorize.reject", "context": { "interactionId": { "path": "/wizard/interactionId" }, "intent": "AUTHORIZE" } } } }, - { "id": "btn-submit", "component": "Button", "label": "Submit Expense", "variant": "primary", - "action": { "event": { "name": "a2h.authorize.approve", "context": { "interactionId": { "path": "/wizard/interactionId" }, "intent": "AUTHORIZE" } } } }, - - { "id": "submit-processing", "component": "Row", - "visible": { "fn": "equals", "args": [{ "path": "/wizard/currentStep" }, 5] }, - "children": ["submit-spinner", "submit-label"], "align": "center" }, - { "id": "submit-spinner", "component": "ProgressIndicator", "mode": "indeterminate" }, - { "id": "submit-label", "component": "Text", "text": { "path": "/wizard/processingLabel" }, "variant": "body" } + { + "id": "rev-date", + "component": "KeyValue", + "label": "Date", + "value": { + "path": "/expense/date" + } + }, + { + "id": "rev-category", + "component": "KeyValue", + "label": "Category", + "value": { + "path": "/expense/category" + } + }, + { + "id": "rev-amount", + "component": "KeyValue", + "label": "Amount", + "value": { + "path": "/expense/amount" + } + }, + { + "id": "rev-description", + "component": "KeyValue", + "label": "Description", + "value": { + "path": "/expense/description" + } + }, + { + "id": "rev-divider", + "component": "Divider" + }, + { + "id": "rev-receipt", + "component": "KeyValue", + "label": "Receipt #", + "value": { + "path": "/expense/receiptNumber" + } + }, + { + "id": "rev-vendor", + "component": "KeyValue", + "label": "Vendor", + "value": { + "path": "/expense/vendor" + } + }, + { + "id": "rev-notes", + "component": "KeyValue", + "label": "Notes", + "value": { + "path": "/expense/notes" + } + }, + { + "id": "s3-divider", + "component": "Divider" + }, + { + "id": "s3-actions", + "component": "Row", + "justify": "end", + "children": [ + "btn-s3-back", + "btn-s3-next" + ] + }, + { + "id": "btn-s3-back", + "component": "Button", + "label": "\u2190 Back", + "action": { + "event": { + "name": "a2h.wizard.back", + "context": { + "step": 3 + } + } + } + }, + { + "id": "btn-s3-next", + "component": "Button", + "label": "Proceed to Submit \u2192", + "variant": "primary", + "action": { + "event": { + "name": "a2h.wizard.next", + "context": { + "step": 3, + "intent": "INFORM" + } + } + } + }, + { + "id": "step4-container", + "component": "Column", + "visible": { + "fn": "equals", + "args": [ + { + "path": "/wizard/currentStep" + }, + 4 + ] + }, + "children": [ + "s4-header-row", + "s4-desc", + "s4-summary-card", + "s4-policy", + "s4-divider", + "s4-actions" + ] + }, + { + "id": "s4-header-row", + "component": "Row", + "children": [ + "s4-icon", + "s4-title" + ], + "align": "center" + }, + { + "id": "s4-icon", + "component": "Icon", + "name": "lock" + }, + { + "id": "s4-title", + "component": "Text", + "text": "Submit Expense Report", + "variant": "h3" + }, + { + "id": "s4-desc", + "component": "Text", + "text": "By submitting, you confirm this expense complies with company policy.", + "variant": "body" + }, + { + "id": "s4-summary-card", + "component": "Card", + "child": "s4-summary-col" + }, + { + "id": "s4-summary-col", + "component": "Column", + "children": [ + "auth-amount", + "auth-category", + "auth-vendor" + ] + }, + { + "id": "auth-amount", + "component": "KeyValue", + "label": "Amount", + "value": { + "path": "/expense/amount" + } + }, + { + "id": "auth-category", + "component": "KeyValue", + "label": "Category", + "value": { + "path": "/expense/category" + } + }, + { + "id": "auth-vendor", + "component": "KeyValue", + "label": "Vendor", + "value": { + "path": "/expense/vendor" + } + }, + { + "id": "s4-policy", + "component": "Text", + "text": "\u26a0\ufe0f Expenses over $500 require manager co-approval.", + "variant": "caption" + }, + { + "id": "s4-divider", + "component": "Divider" + }, + { + "id": "s4-actions", + "component": "Row", + "justify": "end", + "children": [ + "btn-cancel", + "btn-submit" + ] + }, + { + "id": "btn-cancel", + "component": "Button", + "label": "Cancel", + "action": { + "event": { + "name": "a2h.authorize.reject", + "context": { + "interactionId": { + "path": "/wizard/interactionId" + }, + "intent": "AUTHORIZE" + } + } + } + }, + { + "id": "btn-submit", + "component": "Button", + "label": "Submit Expense", + "variant": "primary", + "action": { + "event": { + "name": "a2h.authorize.approve", + "context": { + "interactionId": { + "path": "/wizard/interactionId" + }, + "intent": "AUTHORIZE" + } + } + } + }, + { + "id": "submit-processing", + "component": "Row", + "visible": { + "fn": "equals", + "args": [ + { + "path": "/wizard/currentStep" + }, + 5 + ] + }, + "children": [ + "submit-spinner", + "submit-label" + ], + "align": "center" + }, + { + "id": "submit-spinner", + "component": "ProgressIndicator", + "mode": "indeterminate" + }, + { + "id": "submit-label", + "component": "Text", + "text": { + "path": "/wizard/processingLabel" + }, + "variant": "body" + } ] } }, { - "_comment": "Initial data model — wizard starts at step 1", "version": "v0.9.1", "updateDataModel": { "surfaceId": "a2h-expense-wizard-001", "value": { "wizard": { "currentStep": 1, - "stepLabel": "Step 1 of 4 — Expense Details", + "stepLabel": "Step 1 of 4 \u2014 Expense Details", "interactionId": "exp-2026-0304-001", - "processingLabel": "Submitting expense report…" + "processingLabel": "Submitting expense report\u2026" }, "expense": { "date": "", From 542fd754d4c305c2fbf1cd2516672885d0a44016 Mon Sep 17 00:00:00 2001 From: "G. Hussain Chinoy" Date: Fri, 6 Mar 2026 15:22:03 -0700 Subject: [PATCH 21/22] Update README.md links to twilio repo https://github.com/twilio-labs/Agent2Human --- samples/a2h-prototypes/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/a2h-prototypes/README.md b/samples/a2h-prototypes/README.md index 612011fa8..0772a7480 100644 --- a/samples/a2h-prototypes/README.md +++ b/samples/a2h-prototypes/README.md @@ -1,6 +1,6 @@ # A2H × A2UI Prototypes -Exploration of rendering [A2H](https://github.com/twilio/a2h) (Agent-to-Human) intents using the A2UI v0.9 protocol. Five working prototypes validate the mapping between A2H's five intent types and A2UI's component model. +Exploration of rendering [A2H]([https://github.com/twilio/a2h](https://github.com/twilio-labs/Agent2Human)) (Agent-to-Human) intents using the A2UI v0.9 protocol. Five working prototypes validate the mapping between A2H's five intent types and A2UI's component model. ## Quick Start From fba8b6866177ee16779f261c75a40f1b08e02f7a Mon Sep 17 00:00:00 2001 From: "G. Hussain Chinoy" Date: Fri, 6 Mar 2026 15:22:56 -0700 Subject: [PATCH 22/22] Update README.md --- samples/a2h-prototypes/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/a2h-prototypes/README.md b/samples/a2h-prototypes/README.md index 0772a7480..f4d0f5e0b 100644 --- a/samples/a2h-prototypes/README.md +++ b/samples/a2h-prototypes/README.md @@ -1,6 +1,6 @@ # A2H × A2UI Prototypes -Exploration of rendering [A2H]([https://github.com/twilio/a2h](https://github.com/twilio-labs/Agent2Human)) (Agent-to-Human) intents using the A2UI v0.9 protocol. Five working prototypes validate the mapping between A2H's five intent types and A2UI's component model. +Exploration of rendering [A2H](https://github.com/twilio-labs/Agent2Human) (Agent-to-Human) intents using the A2UI v0.9 protocol. Five working prototypes validate the mapping between A2H's five intent types and A2UI's component model. ## Quick Start