diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 000000000..3b52e6ee1 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,22 @@ +# Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +root = true + +[*] +indent_style = space +indent_size = 2 +end_of_line = lf +charset = utf-8 +insert_final_newline = true diff --git a/.gemini/GEMINI.md b/.gemini/GEMINI.md index c168bb3ff..9aa3ab703 100644 --- a/.gemini/GEMINI.md +++ b/.gemini/GEMINI.md @@ -6,40 +6,40 @@ This document serves as a guide for using the Gemini agent within the A2UI repos The A2UI repository is organized into several key directories: -- `specification/`: Contains the A2UI protocol specifications. - - `v0_8/`: The stable protocol version. - - `docs/`: Human-readable documentation. - - `json/`: JSON schema definitions. - - `eval/`: Genkit-based evaluation framework. - - `v0_9/`: The draft protocol version (in development). - - `docs/`: Human-readable documentation. - - `json/`: JSON schema definitions. - - `eval/`: Genkit-based evaluation framework. - - `v0_10/`: The proposed protocol version (next version). - - `docs/`: Human-readable documentation. - - `json/`: JSON schema definitions. - - `eval/`: Genkit-based evaluation framework. -- `samples/`: Contains sample implementations. - - `agent/`: - - `adk/`: Python-based ADK agent samples (e.g., `contact_lookup`, `restaurant_finder`, `rizzcharts`, `orchestrator`). - - `mcp/`: MCP server sample (A2UI over MCP). - - `client/`: Web client implementations. - - `lit/`: Clients using Lit and Vite (e.g., `contact`, `shell`). - - `angular/`: Clients using Angular (e.g., `contact`, `orchestrator`). - - `personalized_learning/`: Personalized learning sample implementation. -- `agent_sdks/`: Contains source code for Agent integrations. - - `python/`: Python implementation of the A2UI agent library. - - `kotlin/`: Kotlin implementation of the A2UI agent library. -- `renderers/`: Contains renderer libraries. - - `lit/`: The shared Lit renderer library used by the Lit clients. - - `angular/`: The shared Angular renderer library used by the Angular clients. - - `web_core/`: The shared core library used by web renderers. - - `markdown/`: Markdown rendering utilities. -- `tools/`: Helper tools for development. - - `editor/`: A web-based editor for generating and visualizing A2UI. - - `inspector/`: A web-based inspector for A2UI responses. - - `composer/`: Visual composer tool. - - `build_catalog/`: Catalog building utility. +- `specification/`: Contains the A2UI protocol specifications. + - `v0_8/`: The stable protocol version. + - `docs/`: Human-readable documentation. + - `json/`: JSON schema definitions. + - `eval/`: Genkit-based evaluation framework. + - `v0_9/`: The draft protocol version (in development). + - `docs/`: Human-readable documentation. + - `json/`: JSON schema definitions. + - `eval/`: Genkit-based evaluation framework. + - `v0_10/`: The proposed protocol version (next version). + - `docs/`: Human-readable documentation. + - `json/`: JSON schema definitions. + - `eval/`: Genkit-based evaluation framework. +- `samples/`: Contains sample implementations. + - `agent/`: + - `adk/`: Python-based ADK agent samples (e.g., `contact_lookup`, `restaurant_finder`, `rizzcharts`, `orchestrator`). + - `mcp/`: MCP server sample (A2UI over MCP). + - `client/`: Web client implementations. + - `lit/`: Clients using Lit and Vite (e.g., `contact`, `shell`). + - `angular/`: Clients using Angular (e.g., `contact`, `orchestrator`). + - `personalized_learning/`: Personalized learning sample implementation. +- `agent_sdks/`: Contains source code for Agent integrations. + - `python/`: Python implementation of the A2UI agent library. + - `kotlin/`: Kotlin implementation of the A2UI agent library. +- `renderers/`: Contains renderer libraries. + - `lit/`: The shared Lit renderer library used by the Lit clients. + - `angular/`: The shared Angular renderer library used by the Angular clients. + - `web_core/`: The shared core library used by web renderers. + - `markdown/`: Markdown rendering utilities. +- `tools/`: Helper tools for development. + - `editor/`: A web-based editor for generating and visualizing A2UI. + - `inspector/`: A web-based inspector for A2UI responses. + - `composer/`: Visual composer tool. + - `build_catalog/`: Catalog building utility. ## A2UI Specification Overview @@ -49,7 +49,7 @@ The A2UI protocol is a JSONL-based, streaming UI protocol designed to be easily The core concepts of the A2UI protocol are detailed in the main specification document. Refer to the authoritative source for the current version (0.9): -- **A2UI Protocol Specification**: `@specification/v0_9/docs/a2ui_protocol.md` +- **A2UI Protocol Specification**: `@specification/v0_9/docs/a2ui_protocol.md` This document covers the design philosophy, architecture, data flow, and core concepts of the protocol. @@ -57,9 +57,9 @@ This document covers the design philosophy, architecture, data flow, and core co The formal, machine-readable definitions of the protocol are maintained as JSON schemas. For version 0.9: -- **Server-to-Client Schema**: `@specification/v0_9/json/server_to_client.json` -- **Client-to-Server Schema**: `@specification/v0_9/json/client_to_server.json` -- **Basic Catalog**: `@specification/v0_9/json/basic_catalog.json` +- **Server-to-Client Schema**: `@specification/v0_9/json/server_to_client.json` +- **Client-to-Server Schema**: `@specification/v0_9/json/client_to_server.json` +- **Basic Catalog**: `@specification/v0_9/json/basic_catalog.json` ## Running the Demos @@ -82,6 +82,7 @@ The Lit clients are located in `samples/client/lit/`. 1. **Build the Renderer**: First, ensure the shared renderers are built: + ```bash cd renderers/markdown/markdown-it npm install @@ -109,6 +110,7 @@ The Lit clients are located in `samples/client/lit/`. The Angular clients are located in `samples/client/angular/`. First, ensure the shared renderers are built (if not already done): + ```bash cd renderers/markdown/markdown-it npm install @@ -124,6 +126,7 @@ npm run build ``` Then run the Angular client: + ```bash cd samples/client/angular npm install @@ -132,25 +135,25 @@ npm start -- contact # Replace 'contact' with the desired project name (e.g., r ### Running Tools -- **Editor**: Located in `tools/editor`. Run with `npm install && npm run dev`. - - Requires a Gemini API key in `.env` (`GEMINI_API_KEY=`). -- **Inspector**: Located in `tools/inspector`. Run with `npm install && npm run dev`. +- **Editor**: Located in `tools/editor`. Run with `npm install && npm run dev`. + - Requires a Gemini API key in `.env` (`GEMINI_API_KEY=`). +- **Inspector**: Located in `tools/inspector`. Run with `npm install && npm run dev`. ## Renderers There are three renderers available for A2UI: -- **Web (Lit)**: Located in `renderers/lit`, this is the primary web renderer used by the demos in `web/`. -- **Angular**: Located in `renderers/angular`, this is an alternative web renderer for Angular applications. -- **Flutter**: The Flutter renderer is in a separate repository: [https://github.com/flutter/genui](https://github.com/flutter/genui). There is a placeholder renderer folder with a README.md file at `renderers/flutter`. +- **Web (Lit)**: Located in `renderers/lit`, this is the primary web renderer used by the demos in `web/`. +- **Angular**: Located in `renderers/angular`, this is an alternative web renderer for Angular applications. +- **Flutter**: The Flutter renderer is in a separate repository: [https://github.com/flutter/genui](https://github.com/flutter/genui). There is a placeholder renderer folder with a README.md file at `renderers/flutter`. ## Keeping This Guide Updated This document is intended to be a living guide for the repository. As the repository evolves, it's important to keep this file up-to-date. When making changes to the repository, please consider the following: -- **New Demos or Clients**: If you add a new demo or client in `samples/`, add it to the "Running the Demos" section. -- **Specification Changes**: If you make significant changes to the A2UI protocol, ensure that the "A2UI Specification Overview" section is updated. -- **Repository Structure Changes**: If you change the directory structure of the repository, update the "Repository Structure" section. +- **New Demos or Clients**: If you add a new demo or client in `samples/`, add it to the "Running the Demos" section. +- **Specification Changes**: If you make significant changes to the A2UI protocol, ensure that the "A2UI Specification Overview" section is updated. +- **Repository Structure Changes**: If you change the directory structure of the repository, update the "Repository Structure" section. To get this file back in sync, you can run the following commands: @@ -160,4 +163,4 @@ To get this file back in sync, you can run the following commands: ## Change descriptions -If you (the agent) are generating a pull request summary, pull request description, or change description, avoid flowery or hyperbolic terms (e.g. "significantly improves", "greatly enhances", "is an incredible improvement"). Be factual and avoid marketing language: you're not selling the PR, you're describing it. \ No newline at end of file +If you (the agent) are generating a pull request summary, pull request description, or change description, avoid flowery or hyperbolic terms (e.g. "significantly improves", "greatly enhances", "is an incredible improvement"). Be factual and avoid marketing language: you're not selling the PR, you're describing it. diff --git a/.github/ISSUE_TEMPLATE/sprint-task.md b/.github/ISSUE_TEMPLATE/sprint-task.md index b677629a1..b1dc256a9 100644 --- a/.github/ISSUE_TEMPLATE/sprint-task.md +++ b/.github/ISSUE_TEMPLATE/sprint-task.md @@ -4,7 +4,4 @@ about: An issue to track a task used in a sprint title: '' labels: P2, sprint ready assignees: '' - --- - - diff --git a/.github/workflows/auto-assignment.yml b/.github/workflows/auto-assignment.yml index aed5122ec..2e81aaf92 100644 --- a/.github/workflows/auto-assignment.yml +++ b/.github/workflows/auto-assignment.yml @@ -41,4 +41,4 @@ jobs: const fileUrl = pathToFileURL(filePath).href; const importedModule = await import(fileUrl); - await importedModule.default({ github, context }); \ No newline at end of file + await importedModule.default({ github, context }); diff --git a/.github/workflows/check_license.yml b/.github/workflows/check_license.yml index bf41c23d8..5a2b577a8 100644 --- a/.github/workflows/check_license.yml +++ b/.github/workflows/check_license.yml @@ -31,7 +31,7 @@ jobs: - name: Set up Go uses: actions/setup-go@v6 with: - go-version: '1.21' + go-version: "1.21" - name: Install addlicense run: go install github.com/google/addlicense@latest diff --git a/.github/workflows/composer_build_and_test.yml b/.github/workflows/composer_build_and_test.yml index 491cccd5b..02f335ed0 100644 --- a/.github/workflows/composer_build_and_test.yml +++ b/.github/workflows/composer_build_and_test.yml @@ -17,16 +17,16 @@ name: Composer on: push: paths: - - 'tools/composer/**' - - 'renderers/react/**' - - 'renderers/web_core/**' - - '.github/workflows/composer_build_and_test.yml' + - "tools/composer/**" + - "renderers/react/**" + - "renderers/web_core/**" + - ".github/workflows/composer_build_and_test.yml" pull_request: paths: - - 'tools/composer/**' - - 'renderers/react/**' - - 'renderers/web_core/**' - - '.github/workflows/composer_build_and_test.yml' + - "tools/composer/**" + - "renderers/react/**" + - "renderers/web_core/**" + - ".github/workflows/composer_build_and_test.yml" jobs: build-and-test: @@ -43,9 +43,9 @@ jobs: - name: Set up Node.js uses: actions/setup-node@v6 with: - node-version: '22' - cache: 'pnpm' - cache-dependency-path: 'tools/composer/pnpm-lock.yaml' + node-version: "22" + cache: "pnpm" + cache-dependency-path: "tools/composer/pnpm-lock.yaml" - name: Build web_core (dependency of @a2ui/react) working-directory: ./renderers/web_core diff --git a/.github/workflows/cpp_agent_sdk_build_and_test.yml b/.github/workflows/cpp_agent_sdk_build_and_test.yml index bcf4e91a2..3ea1dc534 100644 --- a/.github/workflows/cpp_agent_sdk_build_and_test.yml +++ b/.github/workflows/cpp_agent_sdk_build_and_test.yml @@ -17,16 +17,16 @@ name: A2UI-agent C++ SDK on: push: branches: - - '*' + - "*" paths: - - 'agent_sdks/cpp/**' - - 'specification/**/json/**' - - 'agent_sdks/conformance/**' + - "agent_sdks/cpp/**" + - "specification/**/json/**" + - "agent_sdks/conformance/**" pull_request: paths: - - 'agent_sdks/cpp/**' - - 'specification/**/json/**' - - 'agent_sdks/conformance/**' + - "agent_sdks/cpp/**" + - "specification/**/json/**" + - "agent_sdks/conformance/**" jobs: build-and-test: diff --git a/.github/workflows/e2e_test.yaml b/.github/workflows/e2e_test.yaml index 2e820bb57..b33895614 100644 --- a/.github/workflows/e2e_test.yaml +++ b/.github/workflows/e2e_test.yaml @@ -5,7 +5,7 @@ name: E2E Tests on: - push: + push: # Workflow runs on push to any branch in the repo (not forks). schedule: - cron: "0 * * * *" # hourly diff --git a/.github/workflows/editor_build.yml b/.github/workflows/editor_build.yml index 93df54197..2d8ee283d 100644 --- a/.github/workflows/editor_build.yml +++ b/.github/workflows/editor_build.yml @@ -17,16 +17,16 @@ name: Editor on: push: paths: - - 'tools/editor/**' - - 'renderers/lit/**' - - 'renderers/web_core/**' - - '.github/workflows/editor_build.yml' + - "tools/editor/**" + - "renderers/lit/**" + - "renderers/web_core/**" + - ".github/workflows/editor_build.yml" pull_request: paths: - - 'tools/editor/**' - - 'renderers/lit/**' - - 'renderers/web_core/**' - - '.github/workflows/editor_build.yml' + - "tools/editor/**" + - "renderers/lit/**" + - "renderers/web_core/**" + - ".github/workflows/editor_build.yml" jobs: build: @@ -38,7 +38,7 @@ jobs: - name: Set up Node.js uses: actions/setup-node@v6 with: - node-version: '22' + node-version: "22" - name: Install web_core deps working-directory: ./renderers/web_core diff --git a/.github/workflows/enforce-formatting.yml b/.github/workflows/enforce-formatting.yml new file mode 100644 index 000000000..f3e3d4968 --- /dev/null +++ b/.github/workflows/enforce-formatting.yml @@ -0,0 +1,45 @@ +# Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +name: Enforce Formatting + +on: + push: + branches: [main] + pull_request: + +jobs: + enforce-formatting: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v6 + + - name: Set up Node.js + uses: actions/setup-node@v6 + with: + node-version: "22" + + - name: Set up Python + uses: actions/setup-python@v6 + with: + python-version: "3.x" + + - name: Install `uv` globally + run: | + python -m pip install --upgrade pip + pip install uv + + - name: Run enforce-formatting script + run: ./scripts/fix_format.sh --check diff --git a/.github/workflows/flutter_packages_test.yaml b/.github/workflows/flutter_packages_test.yaml index 7d41eca50..be80973c6 100644 --- a/.github/workflows/flutter_packages_test.yaml +++ b/.github/workflows/flutter_packages_test.yaml @@ -18,14 +18,14 @@ on: workflow_dispatch: push: branches: - - main + - main pull_request: branches: - main schedule: # Tests may fail due to new dependency releases. # Regular execution provides early detection of such regressions. - - cron: '0 * * * *' # hourly + - cron: "0 * * * *" # hourly concurrency: group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} diff --git a/.github/workflows/inspector_build.yml b/.github/workflows/inspector_build.yml index 311202be4..52c23b06e 100644 --- a/.github/workflows/inspector_build.yml +++ b/.github/workflows/inspector_build.yml @@ -16,18 +16,18 @@ name: Inspector on: push: - branches: [ main ] + branches: [main] paths: - - 'tools/inspector/**' - - 'renderers/lit/**' - - 'renderers/web_core/**' - - '.github/workflows/inspector_build.yml' + - "tools/inspector/**" + - "renderers/lit/**" + - "renderers/web_core/**" + - ".github/workflows/inspector_build.yml" pull_request: paths: - - 'tools/inspector/**' - - 'renderers/lit/**' - - 'renderers/web_core/**' - - '.github/workflows/inspector_build.yml' + - "tools/inspector/**" + - "renderers/lit/**" + - "renderers/web_core/**" + - ".github/workflows/inspector_build.yml" jobs: build: @@ -39,7 +39,7 @@ jobs: - name: Set up Node.js uses: actions/setup-node@v6 with: - node-version: '22' + node-version: "22" - name: Install web_core deps working-directory: ./renderers/web_core diff --git a/.github/workflows/kotlin_agent_sdk_build_and_test.yml b/.github/workflows/kotlin_agent_sdk_build_and_test.yml index b3c5197c4..dcb6749fd 100644 --- a/.github/workflows/kotlin_agent_sdk_build_and_test.yml +++ b/.github/workflows/kotlin_agent_sdk_build_and_test.yml @@ -17,16 +17,16 @@ name: A2UI-agent Kotlin SDK on: push: branches: - - '*' + - "*" paths: - - 'agent_sdks/kotlin/**' - - 'specification/**/json/**' - - 'agent_sdks/conformance/**' + - "agent_sdks/kotlin/**" + - "specification/**/json/**" + - "agent_sdks/conformance/**" pull_request: paths: - - 'agent_sdks/kotlin/**' - - 'specification/**/json/**' - - 'agent_sdks/conformance/**' + - "agent_sdks/kotlin/**" + - "specification/**/json/**" + - "agent_sdks/conformance/**" jobs: build-and-test: @@ -39,9 +39,9 @@ jobs: - name: Set up JDK 21 uses: actions/setup-java@v5 with: - java-version: '21' - distribution: 'temurin' - cache: 'gradle' + java-version: "21" + distribution: "temurin" + cache: "gradle" - name: Make gradlew executable working-directory: agent_sdks/kotlin diff --git a/.github/workflows/lit_build_and_test.yml b/.github/workflows/lit_build_and_test.yml index ec14ff993..f9edb0979 100644 --- a/.github/workflows/lit_build_and_test.yml +++ b/.github/workflows/lit_build_and_test.yml @@ -16,16 +16,16 @@ name: Lit renderer on: push: - branches: [ main ] + branches: [main] paths: - - 'renderers/lit/**' - - 'renderers/web_core/**' - - '.github/workflows/lit_build_and_test.yml' + - "renderers/lit/**" + - "renderers/web_core/**" + - ".github/workflows/lit_build_and_test.yml" pull_request: paths: - - 'renderers/lit/**' - - 'renderers/web_core/**' - - '.github/workflows/lit_build_and_test.yml' + - "renderers/lit/**" + - "renderers/web_core/**" + - ".github/workflows/lit_build_and_test.yml" jobs: build-and-test: @@ -37,7 +37,7 @@ jobs: - name: Set up Node.js uses: actions/setup-node@v6 with: - node-version: '22' + node-version: "22" - name: Install web_core dependencies working-directory: ./renderers/web_core diff --git a/.github/workflows/lit_samples_build.yml b/.github/workflows/lit_samples_build.yml index 85222b599..8734614c2 100644 --- a/.github/workflows/lit_samples_build.yml +++ b/.github/workflows/lit_samples_build.yml @@ -16,12 +16,12 @@ name: Lit samples on: push: - branches: [ main ] + branches: [main] paths-ignore: - - 'samples/agent/adk/**' + - "samples/agent/adk/**" pull_request: paths-ignore: - - 'samples/agent/adk/**' + - "samples/agent/adk/**" jobs: build: @@ -33,7 +33,7 @@ jobs: - name: Set up Node.js uses: actions/setup-node@v6 with: - node-version: '22' + node-version: "22" - name: Build lit renderer and its dependencies working-directory: ./samples/client/lit @@ -46,5 +46,3 @@ jobs: - name: Build all lit samples workspaces working-directory: ./samples/client/lit run: npm run build --workspaces - - diff --git a/.github/workflows/ng_build_and_test.yml b/.github/workflows/ng_build_and_test.yml index 457838a77..6b05d4853 100644 --- a/.github/workflows/ng_build_and_test.yml +++ b/.github/workflows/ng_build_and_test.yml @@ -16,12 +16,12 @@ name: Angular on: push: - branches: [ main ] + branches: [main] paths-ignore: - - 'samples/agent/adk/**' + - "samples/agent/adk/**" pull_request: paths-ignore: - - 'samples/agent/adk/**' + - "samples/agent/adk/**" jobs: build-and-test: @@ -33,7 +33,7 @@ jobs: - name: Set up Node.js uses: actions/setup-node@v6 with: - node-version: '22' + node-version: "22" - name: Install top-level deps working-directory: ./samples/client/angular diff --git a/.github/workflows/python_agent_sdk_build_and_test.yml b/.github/workflows/python_agent_sdk_build_and_test.yml index a5db2e8c5..859f08dbe 100644 --- a/.github/workflows/python_agent_sdk_build_and_test.yml +++ b/.github/workflows/python_agent_sdk_build_and_test.yml @@ -17,16 +17,16 @@ name: A2UI-agent Python SDK on: push: branches: - - '*' + - "*" paths: - - 'agent_sdks/python/**' - - 'specification/**/json/**' - - 'agent_sdks/conformance/**' + - "agent_sdks/python/**" + - "specification/**/json/**" + - "agent_sdks/conformance/**" pull_request: paths: - - 'agent_sdks/python/**' - - 'specification/**/json/**' - - 'agent_sdks/conformance/**' + - "agent_sdks/python/**" + - "specification/**/json/**" + - "agent_sdks/conformance/**" jobs: build-and-test: @@ -39,7 +39,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v6 with: - python-version: '3.x' + python-version: "3.x" - name: Install `uv` globally run: | diff --git a/.github/workflows/python_samples_build.yml b/.github/workflows/python_samples_build.yml index 7f0c504f2..c98e1ce83 100644 --- a/.github/workflows/python_samples_build.yml +++ b/.github/workflows/python_samples_build.yml @@ -19,14 +19,14 @@ on: branches: - main paths: - - 'samples/agent/adk/**' - - 'agent_sdks/python/**' - - 'specification/**/json/**' + - "samples/agent/adk/**" + - "agent_sdks/python/**" + - "specification/**/json/**" pull_request: paths: - - 'samples/agent/adk/**' - - 'agent_sdks/python/**' - - 'specification/**/json/**' + - "samples/agent/adk/**" + - "agent_sdks/python/**" + - "specification/**/json/**" jobs: build: @@ -40,7 +40,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v6 with: - python-version: '3.x' + python-version: "3.x" - name: Install `uv` globally run: | diff --git a/.github/workflows/react_renderer.yml b/.github/workflows/react_renderer.yml index 92e91b997..44f46821a 100644 --- a/.github/workflows/react_renderer.yml +++ b/.github/workflows/react_renderer.yml @@ -16,18 +16,18 @@ name: React renderer on: push: - branches: [ main ] + branches: [main] paths-ignore: - - 'renderers/angular/**' - - 'renderers/flutter/**' - - 'renderers/lit/**' - - 'samples/agent/adk/**' + - "renderers/angular/**" + - "renderers/flutter/**" + - "renderers/lit/**" + - "samples/agent/adk/**" pull_request: paths-ignore: - - 'renderers/angular/**' - - 'renderers/flutter/**' - - 'renderers/lit/**' - - 'samples/agent/adk/**' + - "renderers/angular/**" + - "renderers/flutter/**" + - "renderers/lit/**" + - "samples/agent/adk/**" jobs: build-and-test: @@ -39,7 +39,7 @@ jobs: - name: Set up Node.js uses: actions/setup-node@v6 with: - node-version: '22' + node-version: "22" - name: Build web_core dependency working-directory: ./renderers/web_core @@ -74,7 +74,7 @@ jobs: - name: Set up Node.js uses: actions/setup-node@v6 with: - node-version: '22' + node-version: "22" - name: Build web_core dependency working-directory: ./renderers/web_core @@ -109,7 +109,7 @@ jobs: - name: Set up Node.js uses: actions/setup-node@v6 with: - node-version: '22' + node-version: "22" - name: Build web_core dependency working-directory: ./renderers/web_core @@ -130,4 +130,3 @@ jobs: - name: Build React renderer and its dependencies working-directory: ./renderers/react run: npm run lint - diff --git a/.github/workflows/validate_specifications.yml b/.github/workflows/validate_specifications.yml index d142603d9..5a0fcdd6c 100644 --- a/.github/workflows/validate_specifications.yml +++ b/.github/workflows/validate_specifications.yml @@ -16,16 +16,16 @@ name: Validate A2UI specification examples on: push: - branches: [ main ] + branches: [main] paths: - - 'specification/**/json/**' - - '.github/workflows/validate_specifications.yml' - - 'specification/scripts/validate.py' + - "specification/**/json/**" + - ".github/workflows/validate_specifications.yml" + - "specification/scripts/validate.py" pull_request: paths: - - 'specification/**/json/**' - - '.github/workflows/validate_specifications.yml' - - 'specification/scripts/validate.py' + - "specification/**/json/**" + - ".github/workflows/validate_specifications.yml" + - "specification/scripts/validate.py" jobs: validate: @@ -42,12 +42,12 @@ jobs: - name: Set up Node.js uses: actions/setup-node@v6 with: - node-version: '22' + node-version: "22" - name: Set up Python uses: actions/setup-python@v6 with: - python-version: '3.12' + python-version: "3.12" - name: Install dependencies working-directory: ./specification/v0_9/test diff --git a/.github/workflows/web_build_and_test.yml b/.github/workflows/web_build_and_test.yml index 2c86d09e3..1c0ea4357 100644 --- a/.github/workflows/web_build_and_test.yml +++ b/.github/workflows/web_build_and_test.yml @@ -16,14 +16,14 @@ name: Web core on: push: - branches: [ main ] + branches: [main] paths: - - 'renderers/web_core/**/*' - - '.github/workflows/web_build_and_test.yml' + - "renderers/web_core/**/*" + - ".github/workflows/web_build_and_test.yml" pull_request: paths: - - 'renderers/web_core/**/*' - - '.github/workflows/web_build_and_test.yml' + - "renderers/web_core/**/*" + - ".github/workflows/web_build_and_test.yml" jobs: build-and-test: @@ -35,7 +35,7 @@ jobs: - name: Set up Node.js uses: actions/setup-node@v6 with: - node-version: '22' + node-version: "22" - name: Install web_core dependencies working-directory: ./renderers/web_core @@ -60,7 +60,7 @@ jobs: - name: Set up Node.js uses: actions/setup-node@v6 with: - node-version: '22' + node-version: "22" - name: Run publish script integration test run: node --test renderers/scripts/publish_npm.test.mjs diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 000000000..59f4ffca9 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,23 @@ +# Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +node_modules +dist +build +.git +.dart_tool +.venv +*.min.js +agent_sdks/conformance/test_data/** +**/pnpm-lock.yaml diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 000000000..5d5516ef2 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,24 @@ +{ + "printWidth": 100, + "singleQuote": true, + "semi": true, + "tabWidth": 2, + "trailingComma": "all", + "bracketSpacing": false, + "jsxSingleQuote": false, + "arrowParens": "avoid", + "overrides": [ + { + "files": "*.html", + "options": { + "parser": "angular" + } + }, + { + "files": ["*.yaml", "*.yml"], + "options": { + "singleQuote": false + } + } + ] +} diff --git a/.vscode/launch.json b/.vscode/launch.json index bccc61931..a9a992384 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -35,6 +35,6 @@ "request": "launch", "command": "./scripts/fix_licenses.sh", "cwd": "${workspaceFolder}" - }, + } ] -} \ No newline at end of file +} diff --git a/.vscode/settings.json b/.vscode/settings.json index a13f66c74..d67a4f0b2 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,3 +1,12 @@ { - "dart.projectSearchDepth": 10 + "dart.projectSearchDepth": 10, + "editor.formatOnSave": true, + "editor.defaultFormatter": "esbenp.prettier-vscode", + "[python]": { + "editor.defaultFormatter": "ms-python.black-formatter" + }, + "black-formatter.path": ["pyink"], + "[dart]": { + "editor.defaultFormatter": "Dart-Code.dart-code" + } } diff --git a/.vscode/tasks.json b/.vscode/tasks.json index aa77a9423..5b1ad1bd0 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -1,19 +1,19 @@ { - // See https://go.microsoft.com/fwlink/?LinkId=733558 - // for the documentation about the tasks.json format - "version": "2.0.0", - "tasks": [ - { - // To run a shell script, open it and press ⇧⌘B. - // See https://code.visualstudio.com/docs/debugtest/tasks - "label": "currently opened shell script", - "type": "shell", - "command": "/bin/bash ${file}", - "problemMatcher": [], - "group": { - "kind": "build", - "isDefault": true - } - } - ] + // See https://go.microsoft.com/fwlink/?LinkId=733558 + // for the documentation about the tasks.json format + "version": "2.0.0", + "tasks": [ + { + // To run a shell script, open it and press ⇧⌘B. + // See https://code.visualstudio.com/docs/debugtest/tasks + "label": "currently opened shell script", + "type": "shell", + "command": "/bin/bash ${file}", + "problemMatcher": [], + "group": { + "kind": "build", + "isDefault": true + } + } + ] } diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0543648ea..1f7d40724 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -41,7 +41,7 @@ You may follow these steps to contribute: 3. **Work on your forked repository's feature branch.** This is where you will make your changes to the code. 4. **Commit your updates on your forked repository's feature branch.** This will save your changes to your copy of the repository. 5. **Submit a pull request to the official repository's main branch.** This will request that your changes be merged into the official repository. -6. **Resolve any linting errors.** This will ensure that your changes are formatted correctly. +6. **Resolve any linting and formatting errors.** Run `./scripts/fix_format.sh` to fix formatting issues. Here are some additional things to keep in mind during the process: @@ -50,12 +50,36 @@ Here are some additional things to keep in mind during the process: ## Coding Style -To keep our codebase consistent and maintainable, we follow specific coding standards for Python and TypeScript. +To keep our codebase consistent and maintainable, we follow specific coding standards and use automated formatters. -Please refer to the following guidelines for detailed information on: -* **Python**: [Google Python Style Guide](https://google.github.io/styleguide/pyguide.html). -* **TypeScript**: usage of [`gts`](https://github.com/google/gts) and [Google TypeScript Style Guide](https://google.github.io/styleguide/tsguide.html). -* **License Headers**: required copyright notices. +### Formatters + +- **JavaScript / TypeScript / JSON / Markdown / CSS**: [Prettier](https://prettier.io/) +- **Python**: [Pyink](https://github.com/google/pyink) (Google style Black) +- **Dart**: `dart format` + +You can use the provided script to format the entire repo or check formatting: + +```bash +./scripts/fix_format.sh +./scripts/fix_format.sh --check +``` + +### IDE Recommendations (VS Code) + +We recommend using [VS Code](https://code.visualstudio.com/) for development. To help enforce formatting, please install the following extensions: + +- **Prettier - Code formatter** (`esbenp.prettier-vscode`) +- **Black Formatter** (`ms-python.black-formatter`) - configured to use `pyink` in workspace settings. +- **Dart** (`Dart-Code.dart-code`) + +Workspace settings are provided in `.vscode/settings.json` to use these formatters by default on save. + +Please refer to the following guidelines for detailed information on styles: + +- **Python**: [Google Python Style Guide](https://google.github.io/styleguide/pyguide.html). +- **TypeScript**: [Google TypeScript Style Guide](https://google.github.io/styleguide/tsguide.html). +- **License Headers**: required copyright notices. We expect all contributors to adhere to these styles. diff --git a/README.md b/README.md index 013fd0897..cc6b87980 100644 --- a/README.md +++ b/README.md @@ -7,14 +7,14 @@ to generate or populate rich user interfaces. Gallery of A2UI components -*A gallery of A2UI rendered cards, showing a variety of UI compositions that A2UI can achieve.* +_A gallery of A2UI rendered cards, showing a variety of UI compositions that A2UI can achieve._ ## ⚠️ Status: Early stage public preview > **Note:** A2UI is currently in **v0.8 (Public Preview)**. The specification and -implementations are functional but are still evolving. We are opening the project to -foster collaboration, gather feedback, and solicit contributions (e.g., on client renderers). -Expect changes. +> implementations are functional but are still evolving. We are opening the project to +> foster collaboration, gather feedback, and solicit contributions (e.g., on client renderers). +> Expect changes. ## Summary @@ -23,7 +23,7 @@ present rich, interactive interfaces to users, especially when those agents are remote or running across trust boundaries. **A2UI** is an open standard and set of libraries that allows agents to -"speak UI." Agents send a declarative JSON format describing the *intent* of +"speak UI." Agents send a declarative JSON format describing the _intent_ of the UI. The client application then renders this using its own native component library (Flutter, Angular, Lit, etc.). @@ -37,77 +37,77 @@ cross-platform, generative or template-based UI responses from agents. The project's core philosophies: -* **Security first**: Running arbitrary code generated by an LLM may present a -security risk. A2UI is a declarative data format, not executable -code. Your client application maintains a "catalog" of trusted, pre-approved -UI components (e.g., Card, Button, TextField), and the agent can only request -to render components from that catalog. -* **LLM-friendly and incrementally updatable**: The UI is represented as a flat -list of components with ID references which is easy for LLMs to generate -incrementally, allowing for progressive rendering and a responsive user -experience. An agent can efficiently make incremental changes to the UI based -on new user requests as the conversation progresses. -* **Framework-agnostic and portable**: A2UI separates the UI structure from -the UI implementation. The agent sends a description of the component tree -and its associated data model. Your client application is responsible for -mapping these abstract descriptions to its native widgets—be it web components, -Flutter widgets, React components, SwiftUI views or something else entirely. -The same A2UI JSON payload from an agent can be rendered on multiple different -clients built on top of different frameworks. -* **Flexibility**: A2UI also features an open registry pattern that allows -developers to map server-side types to custom client implementations, from -native mobile widgets to React components. By registering a "Smart Wrapper," -you can connect any existing UI component—including secure iframe containers -for legacy content—to A2UI's data binding and event system. Crucially, this -places security firmly in the developer's hands, enabling them to enforce -strict sandboxing policies and "trust ladders" directly within their custom -component logic rather than relying solely on the core system. +- **Security first**: Running arbitrary code generated by an LLM may present a + security risk. A2UI is a declarative data format, not executable + code. Your client application maintains a "catalog" of trusted, pre-approved + UI components (e.g., Card, Button, TextField), and the agent can only request + to render components from that catalog. +- **LLM-friendly and incrementally updatable**: The UI is represented as a flat + list of components with ID references which is easy for LLMs to generate + incrementally, allowing for progressive rendering and a responsive user + experience. An agent can efficiently make incremental changes to the UI based + on new user requests as the conversation progresses. +- **Framework-agnostic and portable**: A2UI separates the UI structure from + the UI implementation. The agent sends a description of the component tree + and its associated data model. Your client application is responsible for + mapping these abstract descriptions to its native widgets—be it web components, + Flutter widgets, React components, SwiftUI views or something else entirely. + The same A2UI JSON payload from an agent can be rendered on multiple different + clients built on top of different frameworks. +- **Flexibility**: A2UI also features an open registry pattern that allows + developers to map server-side types to custom client implementations, from + native mobile widgets to React components. By registering a "Smart Wrapper," + you can connect any existing UI component—including secure iframe containers + for legacy content—to A2UI's data binding and event system. Crucially, this + places security firmly in the developer's hands, enabling them to enforce + strict sandboxing policies and "trust ladders" directly within their custom + component logic rather than relying solely on the core system. ## Use cases Some of the use cases include: -* **Dynamic Data Collection:** An agent generates a bespoke form (date pickers, -sliders, inputs) based on the specific context of a conversation (e.g., -booking a specialized reservation). -* **Remote Sub-Agents:** An orchestrator agent delegates a task to a -remote specialized agent (e.g., a travel booking agent) which returns a -UI payload to be rendered inside the main chat window. -* **Adaptive Workflows:** Enterprise agents that generate approval -dashboards or data visualizations on the fly based on the user's query. +- **Dynamic Data Collection:** An agent generates a bespoke form (date pickers, + sliders, inputs) based on the specific context of a conversation (e.g., + booking a specialized reservation). +- **Remote Sub-Agents:** An orchestrator agent delegates a task to a + remote specialized agent (e.g., a travel booking agent) which returns a + UI payload to be rendered inside the main chat window. +- **Adaptive Workflows:** Enterprise agents that generate approval + dashboards or data visualizations on the fly based on the user's query. ## Architecture The A2UI flow disconnects the generation of UI from the execution of UI: 1. **Generation:** An Agent (using Gemini or another LLM) generates or uses -a pre-generated `A2UI Response`, a JSON payload describing the composition -of UI components and their properties. + a pre-generated `A2UI Response`, a JSON payload describing the composition + of UI components and their properties. 2. **Transport:** This message is sent to the client application -(via A2A, AG UI, etc.). + (via A2A, AG UI, etc.). 3. **Resolution:** The Client's **A2UI Renderer** parses the JSON. 4. **Rendering:** The Renderer maps the abstract components -(e.g., `type: 'text-field'`) to the concrete implementation in the client's codebase. + (e.g., `type: 'text-field'`) to the concrete implementation in the client's codebase. ## Dependencies A2UI is designed to be a lightweight format, but it fits into a larger ecosystem: -* **Transports:** Compatible with **A2A Protocol** and **AG UI**. -* **LLMs:** Can be generated by any model capable of generating JSON output. -* **Host Frameworks:** Requires a host application built in a supported framework -(currently: Web or Flutter). +- **Transports:** Compatible with **A2A Protocol** and **AG UI**. +- **LLMs:** Can be generated by any model capable of generating JSON output. +- **Host Frameworks:** Requires a host application built in a supported framework + (currently: Web or Flutter). ## Getting started Pick the path that matches where you want to start: -| Path | What you get | Time | -|------|--------------|------| -| 🍜 **[Quickstart Restaurant Finder Demo](https://a2ui.org/quickstart/)** | Full-stack A2UI running locally with a Gemini powered ADK agent and Lit renderer. Learn A2UI end-to-end and customize to your use case. | ~5 min | -| ⚛️ **[A2UI + AG-UI (CopilotKit)](docs/guides/a2ui-with-any-agent-framework.md)** | Set up CopilotKit with your agent framework of choice, then enable A2UI rendering. Ready to ship A2UI in a React app. | ~5 min | -| 🎨 **[A2UI Composer](https://a2ui-composer.ag-ui.com/)** · **[Widget Builder](https://go.copilotkit.ai/A2UI-widget-builder)** | Generate A2UI JSON from a visual editor and paste it into any agent prompt — no install required. | ~1 min | -| 🎬 **[A2UI Theater](https://a2ui-composer.ag-ui.com/theater)** | Step through pre-built A2UI streaming scenarios across Lit, React, and Angular renderers — no install required. | ~1 min | +| Path | What you get | Time | +| ----------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- | ------ | +| 🍜 **[Quickstart Restaurant Finder Demo](https://a2ui.org/quickstart/)** | Full-stack A2UI running locally with a Gemini powered ADK agent and Lit renderer. Learn A2UI end-to-end and customize to your use case. | ~5 min | +| ⚛️ **[A2UI + AG-UI (CopilotKit)](docs/guides/a2ui-with-any-agent-framework.md)** | Set up CopilotKit with your agent framework of choice, then enable A2UI rendering. Ready to ship A2UI in a React app. | ~5 min | +| 🎨 **[A2UI Composer](https://a2ui-composer.ag-ui.com/)** · **[Widget Builder](https://go.copilotkit.ai/A2UI-widget-builder)** | Generate A2UI JSON from a visual editor and paste it into any agent prompt — no install required. | ~1 min | +| 🎬 **[A2UI Theater](https://a2ui-composer.ag-ui.com/theater)** | Step through pre-built A2UI streaming scenarios across Lit, React, and Angular renderers — no install required. | ~1 min | ### Restaurant Finder demo — TL;DR @@ -139,10 +139,10 @@ For Flutter, check out the [GenUI SDK](https://github.com/flutter/genui), which We hope to work with the community on the following: -* **Spec stabilization:** Moving towards a v1.0 specification. -* **More renderers:** Adding official support for React, Jetpack Compose, iOS (SwiftUI), and more. -* **Additional transports:** Support for REST and more. -* **Additional Agent frameworks:** Genkit, LangGraph, and more. +- **Spec stabilization:** Moving towards a v1.0 specification. +- **More renderers:** Adding official support for React, Jetpack Compose, iOS (SwiftUI), and more. +- **Additional transports:** Support for REST and more. +- **Additional Agent frameworks:** Genkit, LangGraph, and more. ## Contribute diff --git a/agent_sdks/agent_sdk_guide.md b/agent_sdks/agent_sdk_guide.md index f6ed8b33a..82fc1ed1c 100644 --- a/agent_sdks/agent_sdk_guide.md +++ b/agent_sdks/agent_sdk_guide.md @@ -10,7 +10,7 @@ The A2UI Agent SDK architecture has a well-defined data flow that bridges langua 1. **Define Capabilities**: The SDK loads component schemas (usually from bundled package resources) and organizes them into **Catalogs**. 2. **Generate Prompts**: The SDK uses these catalogs to generate system instructions, automatically injecting the relevant JSON Schema and few-shot examples into the LLM's prompt. -3. **Streaming Parsing**: Support parsing the LLM's output *as it streams*, yielding partial or complete UI messages progressively. +3. **Streaming Parsing**: Support parsing the LLM's output _as it streams_, yielding partial or complete UI messages progressively. 4. **Validate Output**: When the LLM generates a response, the SDK parses it, extracts the A2UI JSON, and validates it against the schema. 5. **Serialize & Send**: The validated JSON is wrapped in a standard transport envelope (e.g., Agent-to-Agent/A2A DataPart) and streamed to the client. @@ -39,10 +39,10 @@ Represents a processed catalog. It provides methods for validation and LLM instr class A2uiCatalog: name: str validator: A2uiValidator - + def render_as_llm_instructions(self, options: InstructionOptions) -> str: """ - Generates a string representation of the catalog (schemas and examples) + Generates a string representation of the catalog (schemas and examples) suitable for inclusion in an LLM system prompt. """ ... @@ -75,15 +75,15 @@ class InferenceStrategy(ABC): #### Standard Implementations -* **`A2uiSchemaManager`**: Generates prompts by dynamically loading and organizing Component Schemas and examples from catalogs. -* **`A2uiTemplateManager`**: Generates prompts using predefined UI templates or static structures. +- **`A2uiSchemaManager`**: Generates prompts by dynamically loading and organizing Component Schemas and examples from catalogs. +- **`A2uiTemplateManager`**: Generates prompts using predefined UI templates or static structures. ### `A2uiValidator` & `PayloadFixer` The safety net of the SDK. -* **`PayloadFixer`**: Attempts to fix common LLM formatting errors (like trailing commas, missing quotes, or unterminated brackets) before structural parsing. -* **`A2uiValidator`**: Performs deep semantic and integrity validation beyond standard JSON Schema checks. +- **`PayloadFixer`**: Attempts to fix common LLM formatting errors (like trailing commas, missing quotes, or unterminated brackets) before structural parsing. +- **`A2uiValidator`**: Performs deep semantic and integrity validation beyond standard JSON Schema checks. #### Standard Validator Checks: @@ -97,7 +97,7 @@ The safety net of the SDK. ## 3. Schema Management & Loading -The SDK does not define component schemas programmatically in code. Instead, it **loads basic catalog JSON Schema definitions packed into the SDK resources** at runtime. Porting the SDK to a new language requires implementing a resource loader and a schema parser for that language's ecosystem (e.g., using `Pydantic` in Python or `kotlinx.serialization` in Kotlin). +The SDK does not define component schemas programmatically in code. Instead, it **loads basic catalog JSON Schema definitions packed into the SDK resources** at runtime. Porting the SDK to a new language requires implementing a resource loader and a schema parser for that language's ecosystem (e.g., using `Pydantic` in Python or `kotlinx.serialization` in Kotlin). Loading from the workspace's `specification/` directory is supported but should be treated as a **fallback for local development**. @@ -105,7 +105,7 @@ Loading from the workspace's `specification/` directory is supported but should 1. **Freestanding Catalogs**: Catalogs should be freestanding. They should define their own types or reference relative paths within the same directory tree. 2. **Version Awareness**: The schema manager must respect the A2UI protocol version (e.g., `v0.9`). If an agent requests `v0.8` schema, it should serve the `v0.8` definitions. -3. **Resource Bundling**: Standard schemas should be bundled with the SDK artifact. Use language-standard utilities to read from package resources (e.g., Python's `importlib.resources`). Fall back to scanning the local `/specification` filesystem path *only* if resource loading fails or if explicitly configured for development. +3. **Resource Bundling**: Standard schemas should be bundled with the SDK artifact. Use language-standard utilities to read from package resources (e.g., Python's `importlib.resources`). Fall back to scanning the local `/specification` filesystem path _only_ if resource loading fails or if explicitly configured for development. --- @@ -122,6 +122,7 @@ When generating prompts, the SDK should allow developers to: 3. **Standard Envelopes**: The prompt must instruct the LLM to wrap its A2UI output in standard tags to enable deterministic parsing. **Standard Prompt Tags:** + ``` CONVERSATIONAL TEXT RESPONSE @@ -160,17 +161,22 @@ for chunk in llm_stream: The parser buffers text and uses regex to extract content between tags. #### Chunk Buffering + Incoming text chunks are appended to an internal buffer. The parser passes through conversational text until it detects the `` opening tag. #### Regex Block Extraction + Once both the opening and closing tags are found in the buffer, the parser uses a regex pattern (e.g., `(.*?)` with `re.DOTALL`) to extract the raw JSON string. -* It yields any text preceding the tag as standard conversational text. -* It yields the JSON content as an A2UI JSON part. + +- It yields any text preceding the tag as standard conversational text. +- It yields the JSON content as an A2UI JSON part. #### Sanitization & Cleanup + Before parsing the JSON, it sanitizes the string to remove any unexpected markdown code block delimiters (e.g., ` ```json `) that the LLM might have inadvertently wrapped around the JSON inside the A2UI tags. #### Multi-Block Support + The parser searches for all occurrences of the tags in the buffer and splits the content into alternating text parts and A2UI JSON parts, clearing processed blocks from the buffer. --- @@ -205,8 +211,8 @@ Once validated, the A2UI payload must be transmitted over the network. In typica The SDK should provide an out-of-the-box configuration for the **A2UI Basic Catalog** (Button, Text, Row, Column, etc.). This ensures that "Hello, World" agents can be built without defining custom schemas. -* In Python, this is provided by `BasicCatalog.get_config()`. -* Your language SDK should provide a similar singleton or preset that points to the standard basic catalog files in the `specification` folder. +- In Python, this is provided by `BasicCatalog.get_config()`. +- Your language SDK should provide a similar singleton or preset that points to the standard basic catalog files in the `specification` folder. --- @@ -218,15 +224,15 @@ While an SDK can be standalone, it is most useful when it integrates with popula Provide a standard toolset (often called `SendA2uiToClientToolset`) that exposes tools to the LLM for sending rich UI. -* **Dynamic Providers**: The toolset should accept providers (callables or futures) to let the tool determine at runtime if A2UI is enabled, and which catalog/examples to use for the current session. -* **The UI Tool**: The actual tool exposed to the LLM (e.g., `send_a2ui_json_to_client`). It should validate the LLM's JSON arguments against the schema *before* returning success to the framework. +- **Dynamic Providers**: The toolset should accept providers (callables or futures) to let the tool determine at runtime if A2UI is enabled, and which catalog/examples to use for the current session. +- **The UI Tool**: The actual tool exposed to the LLM (e.g., `send_a2ui_json_to_client`). It should validate the LLM's JSON arguments against the schema _before_ returning success to the framework. ### 2. Part Converters A Part Converter translates the LLM's output (either tool calls or text tags) into standard transport Parts (like A2A DataParts). -* **Tool-to-Part**: When the LLM calls the UI tool, the converter intercepts the success response (which contains the validated JSON) and wraps it into an A2UI Part. -* **Text-to-Part**: When the LLM outputs text with standard delimiters (e.g., ``), the converter runs the text through the parser and emits A2UI Parts. +- **Tool-to-Part**: When the LLM calls the UI tool, the converter intercepts the success response (which contains the validated JSON) and wraps it into an A2UI Part. +- **Text-to-Part**: When the LLM outputs text with standard delimiters (e.g., ``), the converter runs the text through the parser and emits A2UI Parts. ### 3. Event Converters @@ -239,18 +245,23 @@ An Event Converter intercepts the agent framework's event stream and applies the If you are tasked with porting the `agent_sdk` to a new language (e.g., C++ or Kotlin), follow this strict, phased sequence: ### Step 1: Core Foundation (Non-UI) + Implement `CatalogConfig` (and its `Provider`), `A2uiCatalog`, and an `InferenceStrategy` (like `A2uiSchemaManager`). Ensure you can load a JSON file via a provider and print its schema. ### Step 2: Prompt Generation + Implement `generateSystemPrompt`. Verify that it outputs valid Markdown with embedded JSON schemas and examples. ### Step 3: Parsing & Validation + Implement `parseResponse` and validation. Hook up a standard JSON Schema validator for your language. Use the centralized YAML conformance suite in `agent_sdks/conformance/` to verify that your implementation handles streaming and validation edge cases identically to the reference implementation. ### Step 4: Transport (A2A) + Create the helper utilities to wrap JSON in transport Parts (if needed for your ecosystem). ### Step 5: Sample Applications + Create a simple sample (like a command-line agent or local server) to verify that the SDK works end-to-end. Refer to the reference Python samples (e.g., `samples/agent/adk/contact_lookup`) for inspiration. > [!IMPORTANT] diff --git a/agent_sdks/conformance/README.md b/agent_sdks/conformance/README.md index a74cbd65e..8e79f0dbc 100644 --- a/agent_sdks/conformance/README.md +++ b/agent_sdks/conformance/README.md @@ -3,23 +3,23 @@ To ensure behavioral parity across all SDK implementations (Python, Kotlin, etc.), the project maintains a language-agnostic conformance suite in this directory. ## Suite Structure + All test suites are located in the `suites/` directory: -* `suites/streaming_parser.yaml`: Contains test cases for the `A2uiStreamParser` (streaming), verifying chunk buffering, incremental yielding, and edge cases like cut tokens. -* `suites/parser.yaml`: Contains test cases for non-streaming parsing and payload fixing. -* `suites/validator.yaml`: Contains test cases for the `A2uiValidator`, verifying structural integrity, cycle detection, and reachability. -* `suites/catalog.yaml`: Contains test cases for `A2uiCatalog` (prune, render, load). -* `suites/schema_manager.yaml`: Contains test cases for `A2uiSchemaManager` (select_catalog, load_catalog, generate_prompt). + +- `suites/streaming_parser.yaml`: Contains test cases for the `A2uiStreamParser` (streaming), verifying chunk buffering, incremental yielding, and edge cases like cut tokens. +- `suites/parser.yaml`: Contains test cases for non-streaming parsing and payload fixing. +- `suites/validator.yaml`: Contains test cases for the `A2uiValidator`, verifying structural integrity, cycle detection, and reachability. +- `suites/catalog.yaml`: Contains test cases for `A2uiCatalog` (prune, render, load). +- `suites/schema_manager.yaml`: Contains test cases for `A2uiSchemaManager` (select_catalog, load_catalog, generate_prompt). All static test data and simplified schemas are located in the `test_data/` directory. `conformance_schema.json` at the root is the JSON schema that validates the structure of the YAML test files themselves. - - - - ## Usage in SDKs + Each language SDK must implement a test harness that: + 1. Reads the YAML files. 2. Feeds the inputs to the language's specific implementation of the parser/validator. 3. Asserts that the output matches the expected results defined in the YAML. diff --git a/agent_sdks/conformance/conformance_schema.json b/agent_sdks/conformance/conformance_schema.json index b3b356665..4aec78994 100644 --- a/agent_sdks/conformance/conformance_schema.json +++ b/agent_sdks/conformance/conformance_schema.json @@ -20,10 +20,7 @@ "properties": { "version": { "type": "string", - "enum": [ - "0.8", - "0.9" - ], + "enum": ["0.8", "0.9"], "description": "Protocol version." }, "s2c_schema": { @@ -66,7 +63,7 @@ ] } }, - "required": [ "version" ] + "required": ["version"] }, "action": { "type": "string", @@ -96,31 +93,31 @@ ] } }, - "required": [ "name", "action" ], + "required": ["name", "action"], "allOf": [ { "oneOf": [ - { "$ref": "#/$defs/ProcessChunkTest" }, - { "$ref": "#/$defs/ValidateTest" }, - { "$ref": "#/$defs/PruneTest" }, - { "$ref": "#/$defs/RenderTest" }, - { "$ref": "#/$defs/LoadTest" }, - { "$ref": "#/$defs/RemoveStrictValidationTest" }, - { "$ref": "#/$defs/ParseFullTest" }, - { "$ref": "#/$defs/FixPayloadTest" }, - { "$ref": "#/$defs/HasPartsTest" }, - { "$ref": "#/$defs/SelectCatalogTest" }, - { "$ref": "#/$defs/GeneratePromptTest" }, - { "$ref": "#/$defs/LoadCatalogTest" }, - { "$ref": "#/$defs/ConvertEventTest" }, - { "$ref": "#/$defs/CreateA2uiPartTest" }, - { "$ref": "#/$defs/IsA2uiPartTest" }, - { "$ref": "#/$defs/TryActivateExtensionTest" }, - { "$ref": "#/$defs/HandleRpcTest" }, - { "$ref": "#/$defs/ExecuteToolTest" }, - { "$ref": "#/$defs/GetExtensionTest" }, - { "$ref": "#/$defs/TryActivateTest" }, - { "$ref": "#/$defs/SelectNewestTest" } + {"$ref": "#/$defs/ProcessChunkTest"}, + {"$ref": "#/$defs/ValidateTest"}, + {"$ref": "#/$defs/PruneTest"}, + {"$ref": "#/$defs/RenderTest"}, + {"$ref": "#/$defs/LoadTest"}, + {"$ref": "#/$defs/RemoveStrictValidationTest"}, + {"$ref": "#/$defs/ParseFullTest"}, + {"$ref": "#/$defs/FixPayloadTest"}, + {"$ref": "#/$defs/HasPartsTest"}, + {"$ref": "#/$defs/SelectCatalogTest"}, + {"$ref": "#/$defs/GeneratePromptTest"}, + {"$ref": "#/$defs/LoadCatalogTest"}, + {"$ref": "#/$defs/ConvertEventTest"}, + {"$ref": "#/$defs/CreateA2uiPartTest"}, + {"$ref": "#/$defs/IsA2uiPartTest"}, + {"$ref": "#/$defs/TryActivateExtensionTest"}, + {"$ref": "#/$defs/HandleRpcTest"}, + {"$ref": "#/$defs/ExecuteToolTest"}, + {"$ref": "#/$defs/GetExtensionTest"}, + {"$ref": "#/$defs/TryActivateTest"}, + {"$ref": "#/$defs/SelectNewestTest"} ] } ] @@ -129,7 +126,7 @@ "ProcessChunkTest": { "type": "object", "properties": { - "action": { "const": "process_chunk" }, + "action": {"const": "process_chunk"}, "steps": { "type": "array", "description": "Steps for parser tests.", @@ -163,32 +160,26 @@ "description": "Expected error message (regex) if this step should fail." } }, - "required": [ - "input" - ], + "required": ["input"], "oneOf": [ { - "required": [ - "expect" - ] + "required": ["expect"] }, { - "required": [ - "expect_error" - ] + "required": ["expect_error"] } ] } } }, - "required": [ "steps" ] + "required": ["steps"] }, "ValidateTest": { "type": "object", "properties": { - "action": { "const": "validate" } + "action": {"const": "validate"} }, - "required": [ "action" ], + "required": ["action"], "oneOf": [ { "properties": { @@ -199,10 +190,7 @@ "type": "object", "properties": { "payload": { - "oneOf": [ - { "type": "object" }, - { "type": "array", "items": { "type": "object" } } - ], + "oneOf": [{"type": "object"}, {"type": "array", "items": {"type": "object"}}], "description": "Payload to validate (single message or list of messages)." }, "expect_error": { @@ -210,19 +198,16 @@ "description": "Expected error message (regex) if validation should fail." } }, - "required": [ "payload" ] + "required": ["payload"] } } }, - "required": [ "steps" ] + "required": ["steps"] }, { "properties": { "payload": { - "oneOf": [ - { "type": "object" }, - { "type": "array", "items": { "type": "object" } } - ], + "oneOf": [{"type": "object"}, {"type": "array", "items": {"type": "object"}}], "description": "Payload to validate (single message or list of messages)." }, "expect_error": { @@ -230,235 +215,235 @@ "description": "Expected error message (regex) if validation should fail." } }, - "required": [ "payload" ] + "required": ["payload"] } ] }, "PruneTest": { "type": "object", "properties": { - "action": { "const": "prune" }, + "action": {"const": "prune"}, "args": { "type": "object", "properties": { - "allowed_components": { "type": "array", "items": { "type": "string" } }, - "allowed_messages": { "type": "array", "items": { "type": "string" } } + "allowed_components": {"type": "array", "items": {"type": "string"}}, + "allowed_messages": {"type": "array", "items": {"type": "string"}} } }, - "expect": { "type": "object" } + "expect": {"type": "object"} }, - "required": [ "args", "expect" ] + "required": ["args", "expect"] }, "RenderTest": { "type": "object", "properties": { - "action": { "const": "render" }, - "expect_output": { "type": "string" } + "action": {"const": "render"}, + "expect_output": {"type": "string"} }, - "required": [ "expect_output" ] + "required": ["expect_output"] }, "LoadTest": { "type": "object", "properties": { - "action": { "const": "load" }, + "action": {"const": "load"}, "args": { "type": "object", "properties": { - "path": { "type": ["string", "null"] }, - "validate": { "type": "boolean" } + "path": {"type": ["string", "null"]}, + "validate": {"type": "boolean"} }, - "required": [ "path" ] + "required": ["path"] }, - "expect_output": { "type": "string" }, - "expect_error": { "type": "string" } + "expect_output": {"type": "string"}, + "expect_error": {"type": "string"} }, - "required": [ "args" ] + "required": ["args"] }, "RemoveStrictValidationTest": { "type": "object", "properties": { - "action": { "const": "remove_strict_validation" }, + "action": {"const": "remove_strict_validation"}, "args": { "type": "object", "properties": { - "schema": { "type": "object" } + "schema": {"type": "object"} }, - "required": [ "schema" ] + "required": ["schema"] }, "expect": { "type": "object", "properties": { - "schema": { "type": "object" } + "schema": {"type": "object"} }, - "required": [ "schema" ] + "required": ["schema"] } }, - "required": [ "args", "expect" ] + "required": ["args", "expect"] }, "ParseFullTest": { "type": "object", "properties": { - "action": { "const": "parse_full" }, - "input": { "type": "string" }, - "expect": { "type": "array" }, - "expect_error": { "type": "string" } + "action": {"const": "parse_full"}, + "input": {"type": "string"}, + "expect": {"type": "array"}, + "expect_error": {"type": "string"} }, - "required": [ "input" ] + "required": ["input"] }, "FixPayloadTest": { "type": "object", "properties": { - "action": { "const": "fix_payload" }, - "input": { "type": "string" }, + "action": {"const": "fix_payload"}, + "input": {"type": "string"}, "expect": {} }, - "required": [ "input", "expect" ] + "required": ["input", "expect"] }, "HasPartsTest": { "type": "object", "properties": { - "action": { "const": "has_parts" }, - "input": { "type": "string" }, - "expect": { "type": "boolean" } + "action": {"const": "has_parts"}, + "input": {"type": "string"}, + "expect": {"type": "boolean"} }, - "required": [ "input", "expect" ] + "required": ["input", "expect"] }, "SelectCatalogTest": { "type": "object", "properties": { - "action": { "const": "select_catalog" }, - "args": { "type": "object" }, - "expect_selected": { "type": "string" }, - "expect_catalog_schema": { "type": "object" }, - "expect_error": { "type": "string" } + "action": {"const": "select_catalog"}, + "args": {"type": "object"}, + "expect_selected": {"type": "string"}, + "expect_catalog_schema": {"type": "object"}, + "expect_error": {"type": "string"} }, - "required": [ "args" ] + "required": ["args"] }, "GeneratePromptTest": { "type": "object", "properties": { - "action": { "const": "generate_prompt" }, - "args": { "type": "object" }, + "action": {"const": "generate_prompt"}, + "args": {"type": "object"}, "expect_contains": { "type": "array", - "items": { "type": "string" } + "items": {"type": "string"} } }, - "required": [ "args", "expect_contains" ] + "required": ["args", "expect_contains"] }, "LoadCatalogTest": { "type": "object", "properties": { - "action": { "const": "load_catalog" }, + "action": {"const": "load_catalog"}, "catalog_configs": { "type": "array", "items": { "type": "object", "properties": { - "name": { "type": "string" }, - "path": { "type": "string" } + "name": {"type": "string"}, + "path": {"type": "string"} }, - "required": [ "name", "path" ] + "required": ["name", "path"] } }, "modifiers": { "type": "array", - "items": { "type": "string" } + "items": {"type": "string"} }, "expect": { "type": "object", "properties": { - "catalog_schema": { "type": "object" }, + "catalog_schema": {"type": "object"}, "supported_catalog_ids": { "type": "array", - "items": { "type": "string" } + "items": {"type": "string"} } } } }, - "required": [ "action", "catalog_configs" ] + "required": ["action", "catalog_configs"] }, "ConvertEventTest": { "type": "object", "properties": { - "action": { "const": "convert_event" }, - "args": { "type": "object" }, - "expect": { "type": "object" }, - "expect_empty": { "type": "boolean" } + "action": {"const": "convert_event"}, + "args": {"type": "object"}, + "expect": {"type": "object"}, + "expect_empty": {"type": "boolean"} }, - "required": [ "action", "args" ] + "required": ["action", "args"] }, "CreateA2uiPartTest": { "type": "object", "properties": { - "action": { "const": "create_a2ui_part" }, - "args": { "type": "object" }, - "expect": { "type": "object" } + "action": {"const": "create_a2ui_part"}, + "args": {"type": "object"}, + "expect": {"type": "object"} }, - "required": [ "action", "args", "expect" ] + "required": ["action", "args", "expect"] }, "IsA2uiPartTest": { "type": "object", "properties": { - "action": { "const": "is_a2ui_part" }, - "args": { "type": "object" }, - "expect": { "type": "boolean" } + "action": {"const": "is_a2ui_part"}, + "args": {"type": "object"}, + "expect": {"type": "boolean"} }, - "required": [ "action", "args", "expect" ] + "required": ["action", "args", "expect"] }, "TryActivateExtensionTest": { "type": "object", "properties": { - "action": { "const": "try_activate_extension" }, - "args": { "type": "object" }, - "expect": { "type": "boolean" } + "action": {"const": "try_activate_extension"}, + "args": {"type": "object"}, + "expect": {"type": "boolean"} }, - "required": [ "action", "args", "expect" ] + "required": ["action", "args", "expect"] }, "HandleRpcTest": { "type": "object", "properties": { - "action": { "const": "handle_rpc" }, - "args": { "type": "object" }, - "expect": { "type": "object" } + "action": {"const": "handle_rpc"}, + "args": {"type": "object"}, + "expect": {"type": "object"} }, - "required": [ "action", "args", "expect" ] + "required": ["action", "args", "expect"] }, "ExecuteToolTest": { "type": "object", "properties": { - "action": { "const": "execute_tool" }, - "args": { "type": "object" }, - "expect": { "type": "object" } + "action": {"const": "execute_tool"}, + "args": {"type": "object"}, + "expect": {"type": "object"} }, - "required": [ "action", "args", "expect" ] + "required": ["action", "args", "expect"] }, "GetExtensionTest": { "type": "object", "properties": { - "action": { "const": "get_extension" }, - "args": { "type": "object" }, - "expect": { "type": "object" } + "action": {"const": "get_extension"}, + "args": {"type": "object"}, + "expect": {"type": "object"} }, - "required": [ "action", "args", "expect" ] + "required": ["action", "args", "expect"] }, "TryActivateTest": { "type": "object", "properties": { - "action": { "const": "try_activate" }, - "args": { "type": "object" }, - "expect": { "type": "object" } + "action": {"const": "try_activate"}, + "args": {"type": "object"}, + "expect": {"type": "object"} }, - "required": [ "action", "args", "expect" ] + "required": ["action", "args", "expect"] }, "SelectNewestTest": { "type": "object", "properties": { - "action": { "const": "select_newest" }, - "args": { "type": "object" }, - "expect": { "type": "object" } + "action": {"const": "select_newest"}, + "args": {"type": "object"}, + "expect": {"type": "object"} }, - "required": [ "action", "args", "expect" ] + "required": ["action", "args", "expect"] } } -} \ No newline at end of file +} diff --git a/agent_sdks/conformance/suites/a2a_integration.yaml b/agent_sdks/conformance/suites/a2a_integration.yaml index 6015242d5..77fe9f31b 100644 --- a/agent_sdks/conformance/suites/a2a_integration.yaml +++ b/agent_sdks/conformance/suites/a2a_integration.yaml @@ -50,7 +50,8 @@ parts: [] metadata: a2uiClientCapabilities: - supportedCatalogIds: ["https://a2ui.org/specification/v0_8/standard_catalog_definition.json"] + supportedCatalogIds: + ["https://a2ui.org/specification/v0_8/standard_catalog_definition.json"] runner_output: "Here is your chart:\n\n[\n {\n “beginRendering”: {\n “surfaceId”: “sales-dashboard”\n }\n }\n]\n\nEnjoy!" expect: parts: diff --git a/agent_sdks/conformance/suites/catalog.yaml b/agent_sdks/conformance/suites/catalog.yaml index 1f7953bf9..f57dad4db 100644 --- a/agent_sdks/conformance/suites/catalog.yaml +++ b/agent_sdks/conformance/suites/catalog.yaml @@ -41,9 +41,9 @@ $defs: anyComponent: oneOf: - - $ref: "#/components/Text" - - $ref: "#/components/Button" - - $ref: "#/components/Image" + - $ref: "#/components/Text" + - $ref: "#/components/Button" + - $ref: "#/components/Image" components: Text: {} Button: {} @@ -57,7 +57,7 @@ $defs: anyComponent: oneOf: - - $ref: "#/components/Text" + - $ref: "#/components/Text" components: Text: {} @@ -67,9 +67,9 @@ version: "0.9" s2c_schema: oneOf: - - $ref: "#/$defs/MessageA" - - $ref: "#/$defs/MessageB" - - $ref: "#/$defs/MessageC" + - $ref: "#/$defs/MessageA" + - $ref: "#/$defs/MessageB" + - $ref: "#/$defs/MessageC" $defs: MessageA: {type: object, properties: {a: {type: string}}} MessageB: {type: object, properties: {b: {type: string}}} @@ -81,8 +81,8 @@ expect: s2c_schema: oneOf: - - $ref: "#/$defs/MessageA" - - $ref: "#/$defs/MessageC" + - $ref: "#/$defs/MessageA" + - $ref: "#/$defs/MessageC" $defs: MessageA: {type: object, properties: {a: {type: string}}} MessageC: {type: object, properties: {c: {type: string}}} @@ -93,7 +93,7 @@ version: "0.9" s2c_schema: oneOf: - - $ref: "#/$defs/MessageA" + - $ref: "#/$defs/MessageA" $defs: MessageA: type: object @@ -108,7 +108,7 @@ expect: s2c_schema: oneOf: - - $ref: "#/$defs/MessageA" + - $ref: "#/$defs/MessageA" $defs: MessageA: type: object @@ -147,8 +147,8 @@ TypeForB: {type: number} s2c_schema: oneOf: - - $ref: "#/$defs/MessageA" - - $ref: "#/$defs/MessageB" + - $ref: "#/$defs/MessageA" + - $ref: "#/$defs/MessageB" $defs: MessageA: {$ref: "common_types.json#/$defs/TypeForA"} MessageB: {$ref: "common_types.json#/$defs/TypeForB"} @@ -390,4 +390,3 @@ schema: {type: object, additionalProperties: true} expect: schema: {type: object, additionalProperties: true} - diff --git a/agent_sdks/conformance/suites/parser.yaml b/agent_sdks/conformance/suites/parser.yaml index 4ede7f34e..2a199f17d 100644 --- a/agent_sdks/conformance/suites/parser.yaml +++ b/agent_sdks/conformance/suites/parser.yaml @@ -33,7 +33,7 @@ - name: test_parse_response_only_json_with_tags description: Tests parsing only JSON with tags. action: parse_full - input: "[{\"id\": \"test\"}]" + input: '[{"id": "test"}]' expect: - text: "" a2ui: [{"id": "test"}] diff --git a/agent_sdks/conformance/suites/schema_manager.yaml b/agent_sdks/conformance/suites/schema_manager.yaml index 79a1e81b5..70454f2c8 100644 --- a/agent_sdks/conformance/suites/schema_manager.yaml +++ b/agent_sdks/conformance/suites/schema_manager.yaml @@ -71,7 +71,6 @@ supportedCatalogIds: ["id_not_exists"] expect_error: "No client-supported catalog found" - - name: test_select_catalog_inline description: Inline catalog loading (merges onto base). action: select_catalog @@ -151,7 +150,6 @@ Text: {} Button: {} - # --- Manager Modifier Tests --- - name: test_manager_with_modifiers @@ -275,7 +273,7 @@ - "---BEGIN A2UI JSON SCHEMA---" - "### Server To Client Schema:" - "### Catalog Schema:" - - "\"Button\": {}" + - '"Button": {}' - "---END A2UI JSON SCHEMA---" # --- New Tests requested by User --- @@ -322,5 +320,5 @@ - "---BEGIN A2UI JSON SCHEMA---" - "### Server To Client Schema:" - "### Catalog Schema:" - - "\"Text\": {" + - '"Text": {' - "---END A2UI JSON SCHEMA---" diff --git a/agent_sdks/conformance/suites/streaming_parser.yaml b/agent_sdks/conformance/suites/streaming_parser.yaml index 8fdc87180..c8bf6375d 100644 --- a/agent_sdks/conformance/suites/streaming_parser.yaml +++ b/agent_sdks/conformance/suites/streaming_parser.yaml @@ -193,7 +193,22 @@ - input: "}" expect: [] - input: "}" - expect: [{a2ui: [{beginRendering: {root: "root", surfaceId: "s1", styles: {primaryColor: "#FF0000", font: "Roboto"}}}]}] + expect: + [ + { + a2ui: + [ + { + beginRendering: + { + root: "root", + surfaceId: "s1", + styles: {primaryColor: "#FF0000", font: "Roboto"}, + }, + }, + ], + }, + ] - name: test_multiple_a2ui_blocks description: Tests that multiple A2UI blocks are handled correctly with interleaved text. @@ -956,7 +971,17 @@ - a2ui: - dataModelUpdate: surfaceId: "default" - contents: [{key: "items", valueMap: [{key: "name", valueString: "Item 1"}, {key: "name", valueString: "Item 2"}]}] + contents: + [ + { + key: "items", + valueMap: + [ + {key: "name", valueString: "Item 1"}, + {key: "name", valueString: "Item 2"}, + ], + }, + ] - input: "]}}}]" expect: [] @@ -978,7 +1003,7 @@ expect: [] - name: test_sniff_partial_datamodel_with_cut_key - description: "Verifies that an unclosed string like {\"key or {\"key\": \"val still yields previous valid entries." + description: 'Verifies that an unclosed string like {"key or {"key": "val still yields previous valid entries.' catalog: version: "0.8" s2c_schema: "test_data/simplified_s2c_v08.json" @@ -1325,7 +1350,8 @@ - input: '"' expect: [] - input: "}}," - expect: [{a2ui: [{version: "v0.9", createSurface: {surfaceId: "s1", catalogId: "test_catalog"}}]}] + expect: + [{a2ui: [{version: "v0.9", createSurface: {surfaceId: "s1", catalogId: "test_catalog"}}]}] - input: '{"version": "v0.9", "updateComponents": {"surfaceId": "s1", "components": [' expect: [] - input: '{"id": "root", "component": "Column", "children": [' @@ -1433,7 +1459,8 @@ action: process_chunk steps: - input: '[{"version": "v0.9", "createSurface": {"catalogId": "test_catalog", "surfaceId": "s1"}},' - expect: [{a2ui: [{version: "v0.9", createSurface: {catalogId: "test_catalog", surfaceId: "s1"}}]}] + expect: + [{a2ui: [{version: "v0.9", createSurface: {catalogId: "test_catalog", surfaceId: "s1"}}]}] - input: '{"version": "v0.9", "updateComponents": {"surfaceId": "s1", "components": [{"id": "c1", "component": "Text", "text": "hello"}' expect: [] - input: ', {"id": "root", "component": "Card", "child": "c1"}}]}}' @@ -1471,7 +1498,8 @@ action: process_chunk steps: - input: '[{"version": "v0.9", "createSurface": {"catalogId": "test_catalog", "surfaceId": "s1"}}, ' - expect: [{a2ui: [{version: "v0.9", createSurface: {catalogId: "test_catalog", surfaceId: "s1"}}]}] + expect: + [{a2ui: [{version: "v0.9", createSurface: {catalogId: "test_catalog", surfaceId: "s1"}}]}] - input: '{"version": "v0.9", "updateComponents": {"surfaceId": "s1", "components": [{"id": "root", "component": "Text", "text": "root"}, {"id": "orphan", "component": "Text", "text": "orphan"}]}}]' expect: - a2ui: @@ -1504,7 +1532,8 @@ action: process_chunk steps: - input: '[{"version": "v0.9", "createSurface": {"catalogId": "test_catalog", "surfaceId": "s1"}},' - expect: [{a2ui: [{version: "v0.9", createSurface: {catalogId: "test_catalog", surfaceId: "s1"}}]}] + expect: + [{a2ui: [{version: "v0.9", createSurface: {catalogId: "test_catalog", surfaceId: "s1"}}]}] - input: '{"version": "v0.9", "updateComponents": {"surfaceId": "s1", "components": [{"id": "root", "component": "Card", "child": "child"}]}},{"version": "v0.9", "updateComponents": {"surfaceId": "s1", "components": [{"id": "child", "component": "Card", "child": "root"}]}}' expect_error: "Circular reference detected" @@ -1529,7 +1558,8 @@ action: process_chunk steps: - input: '[{"version": "v0.9", "createSurface": {"catalogId": "test_catalog", "surfaceId": "s1"}},' - expect: [{a2ui: [{version: "v0.9", createSurface: {catalogId: "test_catalog", surfaceId: "s1"}}]}] + expect: + [{a2ui: [{version: "v0.9", createSurface: {catalogId: "test_catalog", surfaceId: "s1"}}]}] - input: '{"version": "v0.9", "updateComponents": {"surfaceId": "s1", "components": [{"id": "root", "component": "Card", "child": "root"}]}}' expect_error: "Self-reference detected" @@ -1555,7 +1585,8 @@ - input: "Here is your UI: " expect: [{text: "Here is your UI: "}] - input: '[{"version": "v0.9", "createSurface": {"catalogId": "test_catalog", "surfaceId": "s1"}}]' - expect: [{a2ui: [{version: "v0.9", createSurface: {catalogId: "test_catalog", surfaceId: "s1"}}]}] + expect: + [{a2ui: [{version: "v0.9", createSurface: {catalogId: "test_catalog", surfaceId: "s1"}}]}] - input: " That's all!" expect: [{text: " That's all!"}] @@ -1623,7 +1654,23 @@ - input: "}" expect: [] - input: "}" - expect: [{a2ui: [{version: "v0.9", createSurface: {catalogId: "test_catalog", surfaceId: "s1", theme: {primaryColor: "#FF0000", font: "Roboto"}}}]}] + expect: + [ + { + a2ui: + [ + { + version: "v0.9", + createSurface: + { + catalogId: "test_catalog", + surfaceId: "s1", + theme: {primaryColor: "#FF0000", font: "Roboto"}, + }, + }, + ], + }, + ] - name: test_multiple_a2ui_blocks_v09 description: Tests that multiple A2UI blocks are handled correctly with interleaved text for v0.9. @@ -1883,7 +1930,7 @@ discriminator: {propertyName: "component"} action: process_chunk steps: - - input: '[' + - input: "[" expect: [] - input: '{"version": "v0.9", "updateDataModel": {"surfaceId": "s1", "value": {"name": "Alice"}}}, ' expect: @@ -1938,7 +1985,7 @@ discriminator: {propertyName: "component"} action: process_chunk steps: - - input: '[' + - input: "[" expect: [] - input: '{"version": "v0.9", "createSurface": {"catalogId": "test_catalog", "surfaceId": "s1"}}, ' expect: @@ -1991,7 +2038,7 @@ discriminator: {propertyName: "component"} action: process_chunk steps: - - input: '[' + - input: "[" expect: [] - input: '{"version": "v0.9", "createSurface": {"catalogId": "test_catalog", "surfaceId": "s1"}}, ' expect: @@ -2042,7 +2089,7 @@ discriminator: {propertyName: "component"} action: process_chunk steps: - - input: '[' + - input: "[" expect: [] - input: '{"version": "v0.9", "createSurface": {"catalogId": "test_catalog", "surfaceId": "contact' expect: [] @@ -2091,7 +2138,7 @@ discriminator: {propertyName: "component"} action: process_chunk steps: - - input: '[' + - input: "[" expect: [] - input: '{"version": "v0.9", "createSurface": {"catalogId": "test_catalog", "surfaceId": "s1"}}, ' expect: @@ -2143,7 +2190,7 @@ discriminator: {propertyName: "component"} action: process_chunk steps: - - input: '[' + - input: "[" expect: [] - input: '{"version": "v0.9", "updateComponents": {"surfaceId": "s1", "components": [{"id": "root", "component": "Text", "text": "hi"}]}}, ' expect: [] @@ -2162,7 +2209,6 @@ component: "Text" text: "hi" - - name: test_data_model_after_components_v09 description: Tests that data model after components works. catalog: @@ -2190,7 +2236,7 @@ discriminator: {propertyName: "component"} action: process_chunk steps: - - input: '[' + - input: "[" expect: [] - input: '{"version": "v0.9", "createSurface": {"catalogId": "test_catalog", "surfaceId": "s1"}}, ' expect: @@ -2244,7 +2290,7 @@ discriminator: {propertyName: "component"} action: process_chunk steps: - - input: '[' + - input: "[" expect: [] - input: '{"version": "v0.9", "createSurface": {"catalogId": "test_catalog", "surfaceId": "s1"}}, ' expect: @@ -2295,7 +2341,7 @@ discriminator: {propertyName: "component"} action: process_chunk steps: - - input: '[' + - input: "[" expect: [] - input: '{"version": "v0.9", "createSurface": {"catalogId": "test_catalog", "surfaceId": "contact' expect: [] @@ -2344,7 +2390,7 @@ discriminator: {propertyName: "component"} action: process_chunk steps: - - input: '[' + - input: "[" expect: [] - input: '{"version": "v0.9", "createSurface": {"catalogId": "test_catalog", "surfaceId": "s1"}}, ' expect: @@ -2396,7 +2442,7 @@ discriminator: {propertyName: "component"} action: process_chunk steps: - - input: '[' + - input: "[" expect: [] - input: '{"version": "v0.9", "updateComponents": {"surfaceId": "s1", "components": [{"id": "root", "component": "Text", "text": "hi"}]}}, ' expect: [] @@ -2426,7 +2472,7 @@ components: {} action: process_chunk steps: - - input: '[' + - input: "[" expect: [] - input: '{"version": "v0.9", "deleteSurface": {"surfaceId": "s1"}}, ' expect: [] @@ -2465,7 +2511,7 @@ discriminator: {propertyName: "component"} action: process_chunk steps: - - input: '[' + - input: "[" expect: [] - input: '{"version": "v0.9", "createSurface": {"catalogId": "test_catalog", "surfaceId": "s1"}}, ' expect: @@ -2498,7 +2544,7 @@ components: {} action: process_chunk steps: - - input: '[' + - input: "[" expect: [] - input: '{"unknownMessage": "invalid"}]' expect_error: "Validation failed" @@ -2525,7 +2571,7 @@ discriminator: {propertyName: "component"} action: process_chunk steps: - - input: '[' + - input: "[" expect: [] - input: '{"version": "v0.9", "createSurface": {"catalogId": "test_catalog", "surfaceId": "s1"}}, ' expect: @@ -2559,9 +2605,9 @@ discriminator: {propertyName: "component"} action: process_chunk steps: - - input: '' + - input: "" expect: [] - - input: '[' + - input: "[" expect: [] - input: '{"version": "v0.9", "createSurface": {"catalogId": "test_catalog", "surfaceId": "s1"}}' expect: @@ -2584,7 +2630,7 @@ - id: "root" component: "Text" text: "hello" - - input: ' world' + - input: " world" expect: - a2ui: - version: "v0.9" @@ -2629,7 +2675,7 @@ discriminator: {propertyName: "component"} action: process_chunk steps: - - input: '[' + - input: "[" expect: [] - input: '{"version": "v0.9", "createSurface": {"catalogId": "test_catalog", "surfaceId": "s1"}}, ' expect: @@ -2707,7 +2753,7 @@ discriminator: {propertyName: "component"} action: process_chunk steps: - - input: '[' + - input: "[" expect: [] - input: '{"version": "v0.9", "createSurface": {"catalogId": "test_catalog", "surfaceId": "default"}}, ' expect: @@ -2823,7 +2869,7 @@ components: {} action: process_chunk steps: - - input: '[' + - input: "[" expect: [] - input: '{"version": "v0.9", "updateDataModel": {"surfaceId": "default", "value": {"title": "Top Restaurants", "items": {' expect: @@ -3086,7 +3132,7 @@ discriminator: {propertyName: "component"} action: process_chunk steps: - - input: '[' + - input: "[" expect: [] - input: '{"version": "v0.9", "createSurface": {"surfaceId": "surface1", "catalogId": "test_catalog"}},' expect: @@ -3201,7 +3247,8 @@ action: process_chunk steps: - input: '[{"version": "v0.9", "createSurface": {"surfaceId": "s1", "catalogId": "test_catalog"}}]' - expect: [{a2ui: [{version: "v0.9", createSurface: {surfaceId: "s1", catalogId: "test_catalog"}}]}] + expect: + [{a2ui: [{version: "v0.9", createSurface: {surfaceId: "s1", catalogId: "test_catalog"}}]}] - input: '[{"version": "v0.9", "updateComponents": {"surfaceId": "s1", "components": [{"id": "root", "component": "Text", "text": {"path": "some/relative/path"}}]}}]' expect: - a2ui: @@ -3212,5 +3259,3 @@ - id: "root" component: "Text" text: {path: "some/relative/path"} - - diff --git a/agent_sdks/conformance/suites/validator.yaml b/agent_sdks/conformance/suites/validator.yaml index fc0cf4ad0..6ebae0bef 100644 --- a/agent_sdks/conformance/suites/validator.yaml +++ b/agent_sdks/conformance/suites/validator.yaml @@ -27,20 +27,20 @@ type: string action: validate payload: - - beginRendering: - root: c1 - surfaceId: s1 - - surfaceUpdate: - surfaceId: s1 - components: - - id: c1 - component: - Card: - child: c2 - - id: c2 - component: - Card: - child: c1 + - beginRendering: + root: c1 + surfaceId: s1 + - surfaceUpdate: + surfaceId: s1 + components: + - id: c1 + component: + Card: + child: c2 + - id: c2 + component: + Card: + child: c1 expect_error: Circular reference detected - name: test_validator_0_9 @@ -52,36 +52,36 @@ catalog_schema: "test_data/simplified_catalog_v09.json" action: validate steps: - - payload: - - version: v0.9 - createSurface: - surfaceId: test-id - catalogId: standard - theme: - primaryColor: blue - iconUrl: http://img - - payload: - - createSurface: - surfaceId: '123' - catalogId: standard - expect_error: '''version'' is a required property' - - payload: - - version: v0.8 - createSurface: - surfaceId: '123' - catalogId: standard - expect_error: '''v0.9'' was expected' - - payload: - - version: v0.9 - createSurface: - surfaceId: 123 - catalogId: standard - expect_error: 123 is not of type 'string' - - payload: - - version: v0.9 - createSurface: - surfaceId: '123' - expect_error: '''catalogId'' is a required property' + - payload: + - version: v0.9 + createSurface: + surfaceId: test-id + catalogId: standard + theme: + primaryColor: blue + iconUrl: http://img + - payload: + - createSurface: + surfaceId: "123" + catalogId: standard + expect_error: "'version' is a required property" + - payload: + - version: v0.8 + createSurface: + surfaceId: "123" + catalogId: standard + expect_error: "'v0.9' was expected" + - payload: + - version: v0.9 + createSurface: + surfaceId: 123 + catalogId: standard + expect_error: 123 is not of type 'string' + - payload: + - version: v0.9 + createSurface: + surfaceId: "123" + expect_error: "'catalogId' is a required property" - name: test_validator_0_8 description: Tests validation of v0.8 messages. @@ -93,23 +93,23 @@ components: {} action: validate steps: - - payload: - - beginRendering: - surfaceId: test-id - root: root - styles: - primaryColor: '#ff0000' - - payload: - - beginRendering: - surfaceId: 123 - root: root - expect_error: 123 is not of type 'string' - - payload: - - beginRendering: - surfaceId: id - root: root - styles: not-an-object - expect_error: '''not-an-object'' is not of type ''object''' + - payload: + - beginRendering: + surfaceId: test-id + root: root + styles: + primaryColor: "#ff0000" + - payload: + - beginRendering: + surfaceId: 123 + root: root + expect_error: 123 is not of type 'string' + - payload: + - beginRendering: + surfaceId: id + root: root + styles: not-an-object + expect_error: "'not-an-object' is not of type 'object'" - name: test_custom_catalog_0_8 description: Tests validation with a custom catalog in v0.8. @@ -130,17 +130,17 @@ items: type: string required: - - explicitList + - explicitList required: - - children + - children Chart: type: object properties: type: type: string enum: - - doughnut - - pie + - doughnut + - pie title: type: object properties: @@ -156,8 +156,8 @@ path: type: string required: - - type - - chartData + - type + - chartData GoogleMap: type: object properties: @@ -176,31 +176,31 @@ path: type: string required: - - center - - zoom + - center + - zoom action: validate payload: - - surfaceUpdate: - surfaceId: id1 - components: - - id: root - component: - Canvas: - children: - explicitList: - - c1 - - c2 - - id: c1 - component: - Canvas: - children: - explicitList: [] - - id: c2 - component: - Chart: - type: pie - chartData: - path: /data + - surfaceUpdate: + surfaceId: id1 + components: + - id: root + component: + Canvas: + children: + explicitList: + - c1 + - c2 + - id: c1 + component: + Canvas: + children: + explicitList: [] + - id: c2 + component: + Chart: + type: pie + chartData: + path: /data - name: test_custom_catalog_0_9 description: Tests validation with a custom catalog in v0.9. @@ -214,57 +214,57 @@ Canvas: type: object allOf: - - $ref: '#/$defs/ComponentCommon' - - $ref: '#/$defs/CatalogComponentCommon' - - type: object - properties: - component: - const: Canvas - children: - $ref: '#/$defs/ChildList' - required: - - component - - children + - $ref: "#/$defs/ComponentCommon" + - $ref: "#/$defs/CatalogComponentCommon" + - type: object + properties: + component: + const: Canvas + children: + $ref: "#/$defs/ChildList" + required: + - component + - children Chart: type: object allOf: - - $ref: '#/$defs/ComponentCommon' - - $ref: '#/$defs/CatalogComponentCommon' - - type: object - properties: - component: - const: Chart - chartType: - enum: - - doughnut - - pie - title: - $ref: '#/$defs/DynamicString' - chartData: - $ref: '#/$defs/DynamicValue' - required: - - component - - chartType - - chartData + - $ref: "#/$defs/ComponentCommon" + - $ref: "#/$defs/CatalogComponentCommon" + - type: object + properties: + component: + const: Chart + chartType: + enum: + - doughnut + - pie + title: + $ref: "#/$defs/DynamicString" + chartData: + $ref: "#/$defs/DynamicValue" + required: + - component + - chartType + - chartData GoogleMap: type: object allOf: - - $ref: '#/$defs/ComponentCommon' - - $ref: '#/$defs/CatalogComponentCommon' - - type: object - properties: - component: - const: GoogleMap - center: - $ref: '#/$defs/DynamicValue' - zoom: - $ref: '#/$defs/DynamicNumber' - pins: - $ref: '#/$defs/DynamicValue' - required: - - component - - center - - zoom + - $ref: "#/$defs/ComponentCommon" + - $ref: "#/$defs/CatalogComponentCommon" + - type: object + properties: + component: + const: GoogleMap + center: + $ref: "#/$defs/DynamicValue" + zoom: + $ref: "#/$defs/DynamicNumber" + pins: + $ref: "#/$defs/DynamicValue" + required: + - component + - center + - zoom $defs: ComponentId: type: string @@ -272,36 +272,36 @@ type: object properties: id: - $ref: '#/$defs/ComponentId' + $ref: "#/$defs/ComponentId" required: - - id + - id ChildList: oneOf: - - type: array - items: - $ref: '#/$defs/ComponentId' - - type: object - properties: - componentId: - $ref: '#/$defs/ComponentId' - path: - type: string - required: - - componentId - - path - additionalProperties: false + - type: array + items: + $ref: "#/$defs/ComponentId" + - type: object + properties: + componentId: + $ref: "#/$defs/ComponentId" + path: + type: string + required: + - componentId + - path + additionalProperties: false DynamicString: anyOf: - - type: string - - type: object + - type: string + - type: object DynamicValue: anyOf: - - type: object - - type: array + - type: object + - type: array DynamicNumber: anyOf: - - type: number - - type: object + - type: number + - type: object CatalogComponentCommon: type: object properties: @@ -309,30 +309,30 @@ type: number anyComponent: oneOf: - - $ref: '#/components/Canvas' - - $ref: '#/components/Chart' - - $ref: '#/components/GoogleMap' + - $ref: "#/components/Canvas" + - $ref: "#/components/Chart" + - $ref: "#/components/GoogleMap" discriminator: propertyName: component action: validate payload: - - version: v0.9 - updateComponents: - surfaceId: s1 - components: - - id: root - component: Canvas - children: - - c1 - - c2 - - id: c1 - component: Canvas - children: [] - - id: c2 - component: Chart - chartType: doughnut - chartData: - path: /data + - version: v0.9 + updateComponents: + surfaceId: s1 + components: + - id: root + component: Canvas + children: + - c1 + - c2 + - id: c1 + component: Canvas + children: [] + - id: c2 + component: Chart + chartType: doughnut + chartData: + path: /data - name: test_validate_duplicate_ids_v08 description: Tests that duplicate IDs are detected in v0.8. @@ -349,25 +349,25 @@ type: string action: validate payload: - - beginRendering: - root: root - surfaceId: test-surface - - surfaceUpdate: - surfaceId: test-surface - components: - - id: root - component: - Text: - text: Root - - id: c1 - component: - Text: - text: Hello - - id: c1 - component: - Text: - text: World - expect_error: 'Duplicate component ID: c1' + - beginRendering: + root: root + surfaceId: test-surface + - surfaceUpdate: + surfaceId: test-surface + components: + - id: root + component: + Text: + text: Root + - id: c1 + component: + Text: + text: Hello + - id: c1 + component: + Text: + text: World + expect_error: "Duplicate component ID: c1" - name: test_validate_duplicate_ids_v09 description: Tests that duplicate IDs are detected in v0.9. @@ -386,34 +386,34 @@ text: type: string required: - - component - - text + - component + - text $defs: anyComponent: oneOf: - - $ref: '#/components/Text' + - $ref: "#/components/Text" discriminator: propertyName: component action: validate payload: - - version: v0.9 - createSurface: - surfaceId: test-surface - catalogId: std - - version: v0.9 - updateComponents: - surfaceId: test-surface - components: - - id: root - component: Text - text: Root - - id: c1 - component: Text - text: Hello - - id: c1 - component: Text - text: World - expect_error: 'Duplicate component ID: c1' + - version: v0.9 + createSurface: + surfaceId: test-surface + catalogId: std + - version: v0.9 + updateComponents: + surfaceId: test-surface + components: + - id: root + component: Text + text: Root + - id: c1 + component: Text + text: Hello + - id: c1 + component: Text + text: World + expect_error: "Duplicate component ID: c1" - name: test_validate_missing_root_v08 description: Tests that missing root component is detected in v0.8. @@ -430,16 +430,16 @@ type: string action: validate payload: - - beginRendering: - root: root - surfaceId: test - - surfaceUpdate: - surfaceId: test - components: - - id: c1 - component: - Text: - text: hi + - beginRendering: + root: root + surfaceId: test + - surfaceUpdate: + surfaceId: test + components: + - id: c1 + component: + Text: + text: hi expect_error: Missing root component - name: test_validate_missing_root_v09 @@ -459,27 +459,27 @@ text: type: string required: - - component - - text + - component + - text $defs: anyComponent: oneOf: - - $ref: '#/components/Text' + - $ref: "#/components/Text" discriminator: propertyName: component action: validate payload: - - version: v0.9 - createSurface: - surfaceId: test - catalogId: std - - version: v0.9 - updateComponents: - surfaceId: test - components: - - id: c1 - component: Text - text: hi + - version: v0.9 + createSurface: + surfaceId: test + catalogId: std + - version: v0.9 + updateComponents: + surfaceId: test + components: + - id: c1 + component: Text + text: hi expect_error: Missing root component - name: test_validate_dangling_references_v08 @@ -504,31 +504,31 @@ type: string action: validate steps: - - payload: - - beginRendering: - root: root - surfaceId: test-surface - - surfaceUpdate: - surfaceId: test-surface - components: - - id: root - component: - Column: - children: - - missing - expect_error: references non-existent component - - payload: - - beginRendering: - root: root - surfaceId: test-surface - - surfaceUpdate: - surfaceId: test-surface - components: - - id: root - component: - Card: - child: missing - expect_error: references non-existent component + - payload: + - beginRendering: + root: root + surfaceId: test-surface + - surfaceUpdate: + surfaceId: test-surface + components: + - id: root + component: + Column: + children: + - missing + expect_error: references non-existent component + - payload: + - beginRendering: + root: root + surfaceId: test-surface + - surfaceUpdate: + surfaceId: test-surface + components: + - id: root + component: + Card: + child: missing + expect_error: references non-existent component - name: test_validate_dangling_references_v09 description: Tests that dangling references are detected in v0.9. @@ -549,8 +549,8 @@ items: type: string required: - - component - - children + - component + - children Card: type: object properties: @@ -559,44 +559,44 @@ child: type: string required: - - component - - child + - component + - child $defs: anyComponent: oneOf: - - $ref: '#/components/Column' - - $ref: '#/components/Card' + - $ref: "#/components/Column" + - $ref: "#/components/Card" discriminator: propertyName: component action: validate steps: - - payload: - - version: v0.9 - createSurface: - surfaceId: test-surface - catalogId: std - - version: v0.9 - updateComponents: - surfaceId: test-surface - components: - - id: root - component: Column - children: - - missing - expect_error: references non-existent component - - payload: - - version: v0.9 - createSurface: - surfaceId: test-surface - catalogId: std - - version: v0.9 - updateComponents: - surfaceId: test-surface - components: - - id: root - component: Card - child: missing - expect_error: references non-existent component + - payload: + - version: v0.9 + createSurface: + surfaceId: test-surface + catalogId: std + - version: v0.9 + updateComponents: + surfaceId: test-surface + components: + - id: root + component: Column + children: + - missing + expect_error: references non-existent component + - payload: + - version: v0.9 + createSurface: + surfaceId: test-surface + catalogId: std + - version: v0.9 + updateComponents: + surfaceId: test-surface + components: + - id: root + component: Card + child: missing + expect_error: references non-existent component - name: test_validate_self_reference_v08 description: Tests that self-references are detected in v0.8. @@ -613,16 +613,16 @@ type: string action: validate payload: - - beginRendering: - root: root - surfaceId: test-surface - - surfaceUpdate: - surfaceId: test-surface - components: - - id: root - component: - Card: - child: root + - beginRendering: + root: root + surfaceId: test-surface + - surfaceUpdate: + surfaceId: test-surface + components: + - id: root + component: + Card: + child: root expect_error: Self-reference detected - name: test_validate_self_reference_v09 @@ -642,27 +642,27 @@ child: type: string required: - - component - - child + - component + - child $defs: anyComponent: oneOf: - - $ref: '#/components/Card' + - $ref: "#/components/Card" discriminator: propertyName: component action: validate payload: - - version: v0.9 - createSurface: - surfaceId: test-surface - catalogId: std - - version: v0.9 - updateComponents: - surfaceId: test-surface - components: - - id: root - component: Card - child: root + - version: v0.9 + createSurface: + surfaceId: test-surface + catalogId: std + - version: v0.9 + updateComponents: + surfaceId: test-surface + components: + - id: root + component: Card + child: root expect_error: Self-reference detected - name: test_validate_function_call_recursion_v08 @@ -682,36 +682,36 @@ type: object action: validate payload: - - beginRendering: - root: root - surfaceId: test-surface - - surfaceUpdate: - surfaceId: test-surface - components: - - id: root - component: - Button: - label: btn - action: - functionCall: - call: f0 - args: + - beginRendering: + root: root + surfaceId: test-surface + - surfaceUpdate: + surfaceId: test-surface + components: + - id: root + component: + Button: + label: btn + action: functionCall: - call: f1 + call: f0 args: functionCall: - call: f2 + call: f1 args: functionCall: - call: f3 + call: f2 args: functionCall: - call: f4 + call: f3 args: functionCall: - call: f5 - args: {} - expect_error: 'Recursion limit exceeded: functionCall depth > 5' + call: f4 + args: + functionCall: + call: f5 + args: {} + expect_error: "Recursion limit exceeded: functionCall depth > 5" - name: test_validate_function_call_recursion_v09 description: Tests that function call recursion limit is exceeded in v0.9. @@ -732,48 +732,48 @@ action: type: object required: - - component - - text - - action + - component + - text + - action $defs: anyComponent: oneOf: - - $ref: '#/components/Button' + - $ref: "#/components/Button" discriminator: propertyName: component action: validate payload: - - version: v0.9 - createSurface: - surfaceId: test-surface - catalogId: std - - version: v0.9 - updateComponents: - surfaceId: test-surface - components: - - id: root - component: Button - text: btn - action: - functionCall: - call: f0 - args: + - version: v0.9 + createSurface: + surfaceId: test-surface + catalogId: std + - version: v0.9 + updateComponents: + surfaceId: test-surface + components: + - id: root + component: Button + text: btn + action: functionCall: - call: f1 + call: f0 args: functionCall: - call: f2 + call: f1 args: functionCall: - call: f3 + call: f2 args: functionCall: - call: f4 + call: f3 args: functionCall: - call: f5 - args: {} - expect_error: 'Recursion limit exceeded: functionCall depth > 5' + call: f4 + args: + functionCall: + call: f5 + args: {} + expect_error: "Recursion limit exceeded: functionCall depth > 5" - name: test_validate_orphaned_component_v08 description: Tests that orphaned components are detected in v0.8. @@ -790,20 +790,20 @@ type: string action: validate payload: - - beginRendering: - root: root - surfaceId: test-surface - - surfaceUpdate: - surfaceId: test-surface - components: - - id: root - component: - Text: - text: Root - - id: orphan - component: - Text: - text: Orphan + - beginRendering: + root: root + surfaceId: test-surface + - surfaceUpdate: + surfaceId: test-surface + components: + - id: root + component: + Text: + text: Root + - id: orphan + component: + Text: + text: Orphan expect_error: Component 'orphan' is not reachable from 'root' - name: test_validate_orphaned_component_v09 @@ -823,30 +823,30 @@ text: type: string required: - - component - - text + - component + - text $defs: anyComponent: oneOf: - - $ref: '#/components/Text' + - $ref: "#/components/Text" discriminator: propertyName: component action: validate payload: - - version: v0.9 - createSurface: - surfaceId: test-surface - catalogId: std - - version: v0.9 - updateComponents: - surfaceId: test-surface - components: - - id: root - component: Text - text: Root - - id: orphan - component: Text - text: Orphan + - version: v0.9 + createSurface: + surfaceId: test-surface + catalogId: std + - version: v0.9 + updateComponents: + surfaceId: test-surface + components: + - id: root + component: Text + text: Root + - id: orphan + component: Text + text: Orphan expect_error: Component 'orphan' is not reachable from 'root' - name: test_validate_template_reachability_v08 @@ -871,39 +871,39 @@ type: string action: validate steps: - - payload: - - beginRendering: - root: root - surfaceId: test-surface - - surfaceUpdate: - surfaceId: test-surface - components: - - id: root - component: - Column: - children: - - template-id - - id: template-id - component: - Text: - text: Reachable - - payload: - - beginRendering: - root: root - surfaceId: test-surface - - surfaceUpdate: - surfaceId: test-surface - components: - - id: root - component: - Column: - children: - - missing-id - - id: template-id - component: - Text: - text: Reachable - expect_error: references non-existent component 'missing-id' + - payload: + - beginRendering: + root: root + surfaceId: test-surface + - surfaceUpdate: + surfaceId: test-surface + components: + - id: root + component: + Column: + children: + - template-id + - id: template-id + component: + Text: + text: Reachable + - payload: + - beginRendering: + root: root + surfaceId: test-surface + - surfaceUpdate: + surfaceId: test-surface + components: + - id: root + component: + Column: + children: + - missing-id + - id: template-id + component: + Text: + text: Reachable + expect_error: references non-existent component 'missing-id' - name: test_validate_template_reachability_v09 description: Tests template reachability in v0.9. @@ -927,11 +927,11 @@ path: type: string required: - - componentId - - path + - componentId + - path required: - - component - - children + - component + - children Text: type: object properties: @@ -940,52 +940,52 @@ text: type: string required: - - component - - text + - component + - text $defs: anyComponent: oneOf: - - $ref: '#/components/List' - - $ref: '#/components/Text' + - $ref: "#/components/List" + - $ref: "#/components/Text" discriminator: propertyName: component action: validate steps: - - payload: - - version: v0.9 - createSurface: - surfaceId: test-surface - catalogId: std - - version: v0.9 - updateComponents: - surfaceId: test-surface - components: - - id: root - component: List - children: - componentId: template-id - path: /items - - id: template-id - component: Text - text: Reachable - - payload: - - version: v0.9 - createSurface: - surfaceId: test-surface - catalogId: std - - version: v0.9 - updateComponents: - surfaceId: test-surface - components: - - id: root - component: List - children: - componentId: missing-id - path: /items - - id: template-id - component: Text - text: Reachable - expect_error: references non-existent component 'missing-id' + - payload: + - version: v0.9 + createSurface: + surfaceId: test-surface + catalogId: std + - version: v0.9 + updateComponents: + surfaceId: test-surface + components: + - id: root + component: List + children: + componentId: template-id + path: /items + - id: template-id + component: Text + text: Reachable + - payload: + - version: v0.9 + createSurface: + surfaceId: test-surface + catalogId: std + - version: v0.9 + updateComponents: + surfaceId: test-surface + components: + - id: root + component: List + children: + componentId: missing-id + path: /items + - id: template-id + component: Text + text: Reachable + expect_error: references non-existent component 'missing-id' - name: test_validate_v08_custom_root_reachability description: Tests custom root reachability in v0.8. @@ -1007,37 +1007,37 @@ type: string action: validate steps: - - payload: - - beginRendering: - root: custom-root - surfaceId: test-surface - - surfaceUpdate: - surfaceId: test-surface - components: - - id: custom-root - component: - Text: - text: I am the root - - id: orphan - component: - Text: - text: I am an orphan - expect_error: Component 'orphan' is not reachable from 'custom-root' - - payload: - - beginRendering: - root: custom-root - surfaceId: test-surface - - surfaceUpdate: - surfaceId: test-surface - components: - - id: custom-root - component: - Card: - child: orphan - - id: orphan - component: - Text: - text: I am no longer an orphan + - payload: + - beginRendering: + root: custom-root + surfaceId: test-surface + - surfaceUpdate: + surfaceId: test-surface + components: + - id: custom-root + component: + Text: + text: I am the root + - id: orphan + component: + Text: + text: I am an orphan + expect_error: Component 'orphan' is not reachable from 'custom-root' + - payload: + - beginRendering: + root: custom-root + surfaceId: test-surface + - surfaceUpdate: + surfaceId: test-surface + components: + - id: custom-root + component: + Card: + child: orphan + - id: orphan + component: + Text: + text: I am no longer an orphan - name: test_validate_invalid_paths_v08 description: Tests invalid paths in v0.8. @@ -1054,12 +1054,12 @@ type: object action: validate payload: - - dataModelUpdate: - surfaceId: surface1 - path: /invalid/escape/~2 - contents: - - key: dummy - valueString: data + - dataModelUpdate: + surfaceId: surface1 + path: /invalid/escape/~2 + contents: + - key: dummy + valueString: data expect_error: (Invalid path syntax|is not valid under any of the given schemas) - name: test_validate_invalid_paths_v09 @@ -1079,24 +1079,24 @@ text: type: object required: - - component - - text + - component + - text $defs: anyComponent: oneOf: - - $ref: '#/components/Text' + - $ref: "#/components/Text" discriminator: propertyName: component action: validate payload: - - version: v0.9 - updateComponents: - surfaceId: test-surface - components: - - id: root - component: Text - text: - path: /invalid/escape/~2 + - version: v0.9 + updateComponents: + surfaceId: test-surface + components: + - id: root + component: Text + text: + path: /invalid/escape/~2 expect_error: (Invalid path syntax|is not valid under any of the given schemas) - name: test_validate_component_nesting_depth_limit_exceeded_v08 @@ -1119,829 +1119,829 @@ type: string action: validate payload: - - beginRendering: - surfaceId: test-surface - root: root - - surfaceUpdate: - surfaceId: test-surface + - beginRendering: + surfaceId: test-surface + root: root + - surfaceUpdate: + surfaceId: test-surface + components: + - id: root + component: + Card: + child: c0 + - id: c0 + component: + Card: + child: c1 + - id: c1 + component: + Card: + child: c2 + - id: c2 + component: + Card: + child: c3 + - id: c3 + component: + Card: + child: c4 + - id: c4 + component: + Card: + child: c5 + - id: c5 + component: + Card: + child: c6 + - id: c6 + component: + Card: + child: c7 + - id: c7 + component: + Card: + child: c8 + - id: c8 + component: + Card: + child: c9 + - id: c9 + component: + Card: + child: c10 + - id: c10 + component: + Card: + child: c11 + - id: c11 + component: + Card: + child: c12 + - id: c12 + component: + Card: + child: c13 + - id: c13 + component: + Card: + child: c14 + - id: c14 + component: + Card: + child: c15 + - id: c15 + component: + Card: + child: c16 + - id: c16 + component: + Card: + child: c17 + - id: c17 + component: + Card: + child: c18 + - id: c18 + component: + Card: + child: c19 + - id: c19 + component: + Card: + child: c20 + - id: c20 + component: + Card: + child: c21 + - id: c21 + component: + Card: + child: c22 + - id: c22 + component: + Card: + child: c23 + - id: c23 + component: + Card: + child: c24 + - id: c24 + component: + Card: + child: c25 + - id: c25 + component: + Card: + child: c26 + - id: c26 + component: + Card: + child: c27 + - id: c27 + component: + Card: + child: c28 + - id: c28 + component: + Card: + child: c29 + - id: c29 + component: + Card: + child: c30 + - id: c30 + component: + Card: + child: c31 + - id: c31 + component: + Card: + child: c32 + - id: c32 + component: + Card: + child: c33 + - id: c33 + component: + Card: + child: c34 + - id: c34 + component: + Card: + child: c35 + - id: c35 + component: + Card: + child: c36 + - id: c36 + component: + Card: + child: c37 + - id: c37 + component: + Card: + child: c38 + - id: c38 + component: + Card: + child: c39 + - id: c39 + component: + Card: + child: c40 + - id: c40 + component: + Card: + child: c41 + - id: c41 + component: + Card: + child: c42 + - id: c42 + component: + Card: + child: c43 + - id: c43 + component: + Card: + child: c44 + - id: c44 + component: + Card: + child: c45 + - id: c45 + component: + Card: + child: c46 + - id: c46 + component: + Card: + child: c47 + - id: c47 + component: + Card: + child: c48 + - id: c48 + component: + Card: + child: c49 + - id: c49 + component: + Card: + child: c50 + - id: c50 + component: + Card: + child: c51 + - id: c51 + component: + Card: + child: c52 + - id: c52 + component: + Card: + child: c53 + - id: c53 + component: + Card: + child: c54 + - id: c54 + component: + Card: + child: c55 + - id: c55 + component: + Text: + text: End + expect_error: "Global recursion limit exceeded: logical depth" + +- name: test_validate_recursion_limit_exceeded_v09 + description: Tests that global recursion limit is exceeded in v0.9. + catalog: + version: "0.9" + common_types_schema: "test_data/simplified_common_types_v09.json" + s2c_schema: "test_data/simplified_s2c_v09.json" + catalog_schema: + catalogId: standard components: - - id: root - component: - Card: + Card: + type: object + properties: + component: + const: Card + child: + type: string + required: + - component + - child + Text: + type: object + properties: + component: + const: Text + text: + type: string + required: + - component + - text + $defs: + anyComponent: + oneOf: + - $ref: "#/components/Card" + - $ref: "#/components/Text" + discriminator: + propertyName: component + action: validate + payload: + - version: v0.9 + createSurface: + surfaceId: test-surface + catalogId: std + - version: v0.9 + updateComponents: + surfaceId: test-surface + components: + - id: root + component: Card child: c0 - - id: c0 - component: - Card: + - id: c0 + component: Card child: c1 - - id: c1 - component: - Card: + - id: c1 + component: Card child: c2 - - id: c2 - component: - Card: + - id: c2 + component: Card child: c3 - - id: c3 - component: - Card: + - id: c3 + component: Card child: c4 - - id: c4 - component: - Card: + - id: c4 + component: Card child: c5 - - id: c5 - component: - Card: + - id: c5 + component: Card child: c6 - - id: c6 - component: - Card: + - id: c6 + component: Card child: c7 - - id: c7 - component: - Card: + - id: c7 + component: Card child: c8 - - id: c8 - component: - Card: + - id: c8 + component: Card child: c9 - - id: c9 - component: - Card: + - id: c9 + component: Card child: c10 - - id: c10 - component: - Card: + - id: c10 + component: Card child: c11 - - id: c11 - component: - Card: + - id: c11 + component: Card child: c12 - - id: c12 - component: - Card: + - id: c12 + component: Card child: c13 - - id: c13 - component: - Card: + - id: c13 + component: Card child: c14 - - id: c14 - component: - Card: + - id: c14 + component: Card child: c15 - - id: c15 - component: - Card: + - id: c15 + component: Card child: c16 - - id: c16 - component: - Card: + - id: c16 + component: Card child: c17 - - id: c17 - component: - Card: + - id: c17 + component: Card child: c18 - - id: c18 - component: - Card: + - id: c18 + component: Card child: c19 - - id: c19 - component: - Card: + - id: c19 + component: Card child: c20 - - id: c20 - component: - Card: + - id: c20 + component: Card child: c21 - - id: c21 - component: - Card: + - id: c21 + component: Card child: c22 - - id: c22 - component: - Card: + - id: c22 + component: Card child: c23 - - id: c23 - component: - Card: + - id: c23 + component: Card child: c24 - - id: c24 - component: - Card: + - id: c24 + component: Card child: c25 - - id: c25 - component: - Card: + - id: c25 + component: Card child: c26 - - id: c26 - component: - Card: + - id: c26 + component: Card child: c27 - - id: c27 - component: - Card: + - id: c27 + component: Card child: c28 - - id: c28 - component: - Card: + - id: c28 + component: Card child: c29 - - id: c29 - component: - Card: + - id: c29 + component: Card child: c30 - - id: c30 - component: - Card: + - id: c30 + component: Card child: c31 - - id: c31 - component: - Card: + - id: c31 + component: Card child: c32 - - id: c32 - component: - Card: + - id: c32 + component: Card child: c33 - - id: c33 - component: - Card: + - id: c33 + component: Card child: c34 - - id: c34 - component: - Card: + - id: c34 + component: Card child: c35 - - id: c35 - component: - Card: + - id: c35 + component: Card child: c36 - - id: c36 - component: - Card: + - id: c36 + component: Card child: c37 - - id: c37 - component: - Card: + - id: c37 + component: Card child: c38 - - id: c38 - component: - Card: + - id: c38 + component: Card child: c39 - - id: c39 - component: - Card: + - id: c39 + component: Card child: c40 - - id: c40 - component: - Card: + - id: c40 + component: Card child: c41 - - id: c41 - component: - Card: + - id: c41 + component: Card child: c42 - - id: c42 - component: - Card: + - id: c42 + component: Card child: c43 - - id: c43 - component: - Card: + - id: c43 + component: Card child: c44 - - id: c44 - component: - Card: + - id: c44 + component: Card child: c45 - - id: c45 - component: - Card: + - id: c45 + component: Card child: c46 - - id: c46 - component: - Card: + - id: c46 + component: Card child: c47 - - id: c47 - component: - Card: + - id: c47 + component: Card child: c48 - - id: c48 - component: - Card: + - id: c48 + component: Card child: c49 - - id: c49 - component: - Card: + - id: c49 + component: Card child: c50 - - id: c50 - component: - Card: + - id: c50 + component: Card child: c51 - - id: c51 - component: - Card: + - id: c51 + component: Card child: c52 - - id: c52 - component: - Card: + - id: c52 + component: Card child: c53 - - id: c53 - component: - Card: + - id: c53 + component: Card child: c54 - - id: c54 - component: - Card: + - id: c54 + component: Card child: c55 - - id: c55 - component: - Text: + - id: c55 + component: Text text: End - expect_error: 'Global recursion limit exceeded: logical depth' + expect_error: "Global recursion limit exceeded: logical depth" -- name: test_validate_recursion_limit_exceeded_v09 - description: Tests that global recursion limit is exceeded in v0.9. +- name: test_validate_recursion_limit_valid_v08 + description: Tests that global recursion limit is valid in v0.8. catalog: - version: "0.9" - common_types_schema: "test_data/simplified_common_types_v09.json" - s2c_schema: "test_data/simplified_s2c_v09.json" + version: "0.8" + s2c_schema: "test_data/simplified_s2c_v08.json" catalog_schema: catalogId: standard components: Card: type: object properties: - component: - const: Card child: type: string - required: - - component - - child Text: type: object properties: - component: - const: Text text: type: string - required: - - component - - text - $defs: - anyComponent: - oneOf: - - $ref: '#/components/Card' - - $ref: '#/components/Text' - discriminator: - propertyName: component action: validate payload: - - version: v0.9 - createSurface: - surfaceId: test-surface - catalogId: std - - version: v0.9 - updateComponents: - surfaceId: test-surface - components: - - id: root - component: Card - child: c0 - - id: c0 - component: Card - child: c1 - - id: c1 - component: Card - child: c2 - - id: c2 - component: Card - child: c3 - - id: c3 - component: Card - child: c4 - - id: c4 - component: Card - child: c5 - - id: c5 - component: Card - child: c6 - - id: c6 - component: Card - child: c7 - - id: c7 - component: Card - child: c8 - - id: c8 - component: Card - child: c9 - - id: c9 - component: Card - child: c10 - - id: c10 - component: Card - child: c11 - - id: c11 - component: Card - child: c12 - - id: c12 - component: Card - child: c13 - - id: c13 - component: Card - child: c14 - - id: c14 - component: Card - child: c15 - - id: c15 - component: Card - child: c16 - - id: c16 - component: Card - child: c17 - - id: c17 - component: Card - child: c18 - - id: c18 - component: Card - child: c19 - - id: c19 - component: Card - child: c20 - - id: c20 - component: Card - child: c21 - - id: c21 - component: Card - child: c22 - - id: c22 - component: Card - child: c23 - - id: c23 - component: Card - child: c24 - - id: c24 - component: Card - child: c25 - - id: c25 - component: Card - child: c26 - - id: c26 - component: Card - child: c27 - - id: c27 - component: Card - child: c28 - - id: c28 - component: Card - child: c29 - - id: c29 - component: Card - child: c30 - - id: c30 - component: Card - child: c31 - - id: c31 - component: Card - child: c32 - - id: c32 - component: Card - child: c33 - - id: c33 - component: Card - child: c34 - - id: c34 - component: Card - child: c35 - - id: c35 - component: Card - child: c36 - - id: c36 - component: Card - child: c37 - - id: c37 - component: Card - child: c38 - - id: c38 - component: Card - child: c39 - - id: c39 - component: Card - child: c40 - - id: c40 - component: Card - child: c41 - - id: c41 - component: Card - child: c42 - - id: c42 - component: Card - child: c43 - - id: c43 - component: Card - child: c44 - - id: c44 - component: Card - child: c45 - - id: c45 - component: Card - child: c46 - - id: c46 - component: Card - child: c47 - - id: c47 - component: Card - child: c48 - - id: c48 - component: Card - child: c49 - - id: c49 - component: Card - child: c50 - - id: c50 - component: Card - child: c51 - - id: c51 - component: Card - child: c52 - - id: c52 - component: Card - child: c53 - - id: c53 - component: Card - child: c54 - - id: c54 - component: Card - child: c55 - - id: c55 - component: Text - text: End - expect_error: 'Global recursion limit exceeded: logical depth' + - beginRendering: + surfaceId: test-surface + root: root + - surfaceUpdate: + surfaceId: test-surface + components: + - id: root + component: + Card: + child: c0 + - id: c0 + component: + Card: + child: c1 + - id: c1 + component: + Card: + child: c2 + - id: c2 + component: + Card: + child: c3 + - id: c3 + component: + Card: + child: c4 + - id: c4 + component: + Card: + child: c5 + - id: c5 + component: + Card: + child: c6 + - id: c6 + component: + Card: + child: c7 + - id: c7 + component: + Card: + child: c8 + - id: c8 + component: + Card: + child: c9 + - id: c9 + component: + Card: + child: c10 + - id: c10 + component: + Card: + child: c11 + - id: c11 + component: + Card: + child: c12 + - id: c12 + component: + Card: + child: c13 + - id: c13 + component: + Card: + child: c14 + - id: c14 + component: + Card: + child: c15 + - id: c15 + component: + Card: + child: c16 + - id: c16 + component: + Card: + child: c17 + - id: c17 + component: + Card: + child: c18 + - id: c18 + component: + Card: + child: c19 + - id: c19 + component: + Card: + child: c20 + - id: c20 + component: + Card: + child: c21 + - id: c21 + component: + Card: + child: c22 + - id: c22 + component: + Card: + child: c23 + - id: c23 + component: + Card: + child: c24 + - id: c24 + component: + Card: + child: c25 + - id: c25 + component: + Card: + child: c26 + - id: c26 + component: + Card: + child: c27 + - id: c27 + component: + Card: + child: c28 + - id: c28 + component: + Card: + child: c29 + - id: c29 + component: + Card: + child: c30 + - id: c30 + component: + Card: + child: c31 + - id: c31 + component: + Card: + child: c32 + - id: c32 + component: + Card: + child: c33 + - id: c33 + component: + Card: + child: c34 + - id: c34 + component: + Card: + child: c35 + - id: c35 + component: + Card: + child: c36 + - id: c36 + component: + Card: + child: c37 + - id: c37 + component: + Card: + child: c38 + - id: c38 + component: + Card: + child: c39 + - id: c39 + component: + Card: + child: c40 + - id: c40 + component: + Text: + text: End -- name: test_validate_recursion_limit_valid_v08 - description: Tests that global recursion limit is valid in v0.8. +- name: test_validate_recursion_limit_valid_v09 + description: Tests that global recursion limit is valid in v0.9. catalog: - version: "0.8" - s2c_schema: "test_data/simplified_s2c_v08.json" + version: "0.9" + common_types_schema: "test_data/simplified_common_types_v09.json" + s2c_schema: "test_data/simplified_s2c_v09.json" catalog_schema: catalogId: standard components: Card: type: object properties: + component: + const: Card child: type: string + required: + - component + - child Text: type: object properties: + component: + const: Text text: type: string + required: + - component + - text + $defs: + anyComponent: + oneOf: + - $ref: "#/components/Card" + - $ref: "#/components/Text" + discriminator: + propertyName: component action: validate payload: - - beginRendering: - surfaceId: test-surface - root: root - - surfaceUpdate: - surfaceId: test-surface - components: - - id: root - component: - Card: + - version: v0.9 + createSurface: + surfaceId: test-surface + catalogId: std + - version: v0.9 + updateComponents: + surfaceId: test-surface + components: + - id: root + component: Card child: c0 - - id: c0 - component: - Card: + - id: c0 + component: Card child: c1 - - id: c1 - component: - Card: + - id: c1 + component: Card child: c2 - - id: c2 - component: - Card: + - id: c2 + component: Card child: c3 - - id: c3 - component: - Card: + - id: c3 + component: Card child: c4 - - id: c4 - component: - Card: + - id: c4 + component: Card child: c5 - - id: c5 - component: - Card: + - id: c5 + component: Card child: c6 - - id: c6 - component: - Card: + - id: c6 + component: Card child: c7 - - id: c7 - component: - Card: + - id: c7 + component: Card child: c8 - - id: c8 - component: - Card: + - id: c8 + component: Card child: c9 - - id: c9 - component: - Card: + - id: c9 + component: Card child: c10 - - id: c10 - component: - Card: + - id: c10 + component: Card child: c11 - - id: c11 - component: - Card: + - id: c11 + component: Card child: c12 - - id: c12 - component: - Card: + - id: c12 + component: Card child: c13 - - id: c13 - component: - Card: + - id: c13 + component: Card child: c14 - - id: c14 - component: - Card: + - id: c14 + component: Card child: c15 - - id: c15 - component: - Card: + - id: c15 + component: Card child: c16 - - id: c16 - component: - Card: + - id: c16 + component: Card child: c17 - - id: c17 - component: - Card: + - id: c17 + component: Card child: c18 - - id: c18 - component: - Card: + - id: c18 + component: Card child: c19 - - id: c19 - component: - Card: + - id: c19 + component: Card child: c20 - - id: c20 - component: - Card: + - id: c20 + component: Card child: c21 - - id: c21 - component: - Card: + - id: c21 + component: Card child: c22 - - id: c22 - component: - Card: + - id: c22 + component: Card child: c23 - - id: c23 - component: - Card: + - id: c23 + component: Card child: c24 - - id: c24 - component: - Card: + - id: c24 + component: Card child: c25 - - id: c25 - component: - Card: + - id: c25 + component: Card child: c26 - - id: c26 - component: - Card: + - id: c26 + component: Card child: c27 - - id: c27 - component: - Card: + - id: c27 + component: Card child: c28 - - id: c28 - component: - Card: + - id: c28 + component: Card child: c29 - - id: c29 - component: - Card: + - id: c29 + component: Card child: c30 - - id: c30 - component: - Card: + - id: c30 + component: Card child: c31 - - id: c31 - component: - Card: + - id: c31 + component: Card child: c32 - - id: c32 - component: - Card: + - id: c32 + component: Card child: c33 - - id: c33 - component: - Card: + - id: c33 + component: Card child: c34 - - id: c34 - component: - Card: + - id: c34 + component: Card child: c35 - - id: c35 - component: - Card: + - id: c35 + component: Card child: c36 - - id: c36 - component: - Card: + - id: c36 + component: Card child: c37 - - id: c37 - component: - Card: + - id: c37 + component: Card child: c38 - - id: c38 - component: - Card: + - id: c38 + component: Card child: c39 - - id: c39 - component: - Card: + - id: c39 + component: Card child: c40 - - id: c40 - component: - Text: + - id: c40 + component: Text text: End -- name: test_validate_recursion_limit_valid_v09 - description: Tests that global recursion limit is valid in v0.9. - catalog: - version: "0.9" - common_types_schema: "test_data/simplified_common_types_v09.json" - s2c_schema: "test_data/simplified_s2c_v09.json" - catalog_schema: - catalogId: standard - components: - Card: - type: object - properties: - component: - const: Card - child: - type: string - required: - - component - - child - Text: - type: object - properties: - component: - const: Text - text: - type: string - required: - - component - - text - $defs: - anyComponent: - oneOf: - - $ref: '#/components/Card' - - $ref: '#/components/Text' - discriminator: - propertyName: component - action: validate - payload: - - version: v0.9 - createSurface: - surfaceId: test-surface - catalogId: std - - version: v0.9 - updateComponents: - surfaceId: test-surface - components: - - id: root - component: Card - child: c0 - - id: c0 - component: Card - child: c1 - - id: c1 - component: Card - child: c2 - - id: c2 - component: Card - child: c3 - - id: c3 - component: Card - child: c4 - - id: c4 - component: Card - child: c5 - - id: c5 - component: Card - child: c6 - - id: c6 - component: Card - child: c7 - - id: c7 - component: Card - child: c8 - - id: c8 - component: Card - child: c9 - - id: c9 - component: Card - child: c10 - - id: c10 - component: Card - child: c11 - - id: c11 - component: Card - child: c12 - - id: c12 - component: Card - child: c13 - - id: c13 - component: Card - child: c14 - - id: c14 - component: Card - child: c15 - - id: c15 - component: Card - child: c16 - - id: c16 - component: Card - child: c17 - - id: c17 - component: Card - child: c18 - - id: c18 - component: Card - child: c19 - - id: c19 - component: Card - child: c20 - - id: c20 - component: Card - child: c21 - - id: c21 - component: Card - child: c22 - - id: c22 - component: Card - child: c23 - - id: c23 - component: Card - child: c24 - - id: c24 - component: Card - child: c25 - - id: c25 - component: Card - child: c26 - - id: c26 - component: Card - child: c27 - - id: c27 - component: Card - child: c28 - - id: c28 - component: Card - child: c29 - - id: c29 - component: Card - child: c30 - - id: c30 - component: Card - child: c31 - - id: c31 - component: Card - child: c32 - - id: c32 - component: Card - child: c33 - - id: c33 - component: Card - child: c34 - - id: c34 - component: Card - child: c35 - - id: c35 - component: Card - child: c36 - - id: c36 - component: Card - child: c37 - - id: c37 - component: Card - child: c38 - - id: c38 - component: Card - child: c39 - - id: c39 - component: Card - child: c40 - - id: c40 - component: Text - text: End - - name: test_validate_global_recursion_limit_exceeded_v09 description: Tests that global recursion limit is exceeded for data model in v0.9. catalog: @@ -1951,121 +1951,121 @@ catalog_schema: "test_data/simplified_catalog_v09.json" action: validate payload: - - version: v0.9 - updateDataModel: - surfaceId: test-surface - value: - level: 0 - next: - level: 1 + - version: v0.9 + updateDataModel: + surfaceId: test-surface + value: + level: 0 next: - level: 2 + level: 1 next: - level: 3 + level: 2 next: - level: 4 + level: 3 next: - level: 5 + level: 4 next: - level: 6 + level: 5 next: - level: 7 + level: 6 next: - level: 8 + level: 7 next: - level: 9 + level: 8 next: - level: 10 + level: 9 next: - level: 11 + level: 10 next: - level: 12 + level: 11 next: - level: 13 + level: 12 next: - level: 14 + level: 13 next: - level: 15 + level: 14 next: - level: 16 + level: 15 next: - level: 17 + level: 16 next: - level: 18 + level: 17 next: - level: 19 + level: 18 next: - level: 20 + level: 19 next: - level: 21 + level: 20 next: - level: 22 + level: 21 next: - level: 23 + level: 22 next: - level: 24 + level: 23 next: - level: 25 + level: 24 next: - level: 26 + level: 25 next: - level: 27 + level: 26 next: - level: 28 + level: 27 next: - level: 29 + level: 28 next: - level: 30 + level: 29 next: - level: 31 + level: 30 next: - level: 32 + level: 31 next: - level: 33 + level: 32 next: - level: 34 + level: 33 next: - level: 35 + level: 34 next: - level: 36 + level: 35 next: - level: 37 + level: 36 next: - level: 38 + level: 37 next: - level: 39 + level: 38 next: - level: 40 + level: 39 next: - level: 41 + level: 40 next: - level: 42 + level: 41 next: - level: 43 + level: 42 next: - level: 44 + level: 43 next: - level: 45 + level: 44 next: - level: 46 + level: 45 next: - level: 47 + level: 46 next: - level: 48 + level: 47 next: - level: 49 + level: 48 next: - level: 50 + level: 49 next: - level: 51 + level: 50 next: - level: 52 + level: 51 next: - level: 53 + level: 52 next: - level: 54 + level: 53 next: - level: 55 + level: 54 + next: + level: 55 expect_error: Global recursion limit exceeded - name: test_validate_circular_reference_v09 @@ -2090,18 +2090,18 @@ discriminator: {propertyName: "component"} action: validate payload: - - version: v0.9 - createSurface: {surfaceId: "s1", catalogId: "test_catalog"} - - version: v0.9 - updateComponents: - surfaceId: "s1" - components: - - id: "root" - component: "Card" - child: "c1" - - id: "c1" - component: "Card" - child: "root" + - version: v0.9 + createSurface: {surfaceId: "s1", catalogId: "test_catalog"} + - version: v0.9 + updateComponents: + surfaceId: "s1" + components: + - id: "root" + component: "Card" + child: "c1" + - id: "c1" + component: "Card" + child: "root" expect_error: Circular reference detected - name: test_validate_multi_surface_v08 @@ -2124,34 +2124,34 @@ type: string action: validate payload: - - beginRendering: - surfaceId: surface-a - root: root-a - - beginRendering: - surfaceId: surface-b - root: root-b - - surfaceUpdate: - surfaceId: surface-a - components: - - id: root-a - component: - Card: - child: child-a - - id: child-a - component: - Text: - text: Hello A - - surfaceUpdate: - surfaceId: surface-b - components: - - id: root-b - component: - Card: - child: child-b - - id: child-b - component: - Text: - text: Hello B + - beginRendering: + surfaceId: surface-a + root: root-a + - beginRendering: + surfaceId: surface-b + root: root-b + - surfaceUpdate: + surfaceId: surface-a + components: + - id: root-a + component: + Card: + child: child-a + - id: child-a + component: + Text: + text: Hello A + - surfaceUpdate: + surfaceId: surface-b + components: + - id: root-b + component: + Card: + child: child-b + - id: child-b + component: + Text: + text: Hello B - name: test_validate_multi_surface_missing_root_v08 description: Tests that missing root in one surface still fails validation. @@ -2168,26 +2168,26 @@ type: string action: validate payload: - - beginRendering: - surfaceId: surface-a - root: root-a - - beginRendering: - surfaceId: surface-b - root: root-b - - surfaceUpdate: - surfaceId: surface-a - components: - - id: root-a - component: - Text: - text: Hello A - - surfaceUpdate: - surfaceId: surface-b - components: - - id: not-root-b - component: - Text: - text: Hello B + - beginRendering: + surfaceId: surface-a + root: root-a + - beginRendering: + surfaceId: surface-b + root: root-b + - surfaceUpdate: + surfaceId: surface-a + components: + - id: root-a + component: + Text: + text: Hello A + - surfaceUpdate: + surfaceId: surface-b + components: + - id: not-root-b + component: + Text: + text: Hello B expect_error: Missing root component.*root-b - name: test_incremental_update_no_root_v08 @@ -2210,17 +2210,17 @@ type: string action: validate payload: - - surfaceUpdate: - surfaceId: contact-card - components: - - id: main_card - component: - Card: - child: col - - id: col - component: - Text: - text: Updated + - surfaceUpdate: + surfaceId: contact-card + components: + - id: main_card + component: + Card: + child: col + - id: col + component: + Text: + text: Updated - name: test_incremental_update_no_root_v09 description: Incremental update without root component should pass (v0.9). @@ -2239,8 +2239,8 @@ child: type: string required: - - component - - child + - component + - child Text: type: object properties: @@ -2249,27 +2249,27 @@ text: type: string required: - - component - - text + - component + - text $defs: anyComponent: oneOf: - - $ref: '#/components/Card' - - $ref: '#/components/Text' + - $ref: "#/components/Card" + - $ref: "#/components/Text" discriminator: propertyName: component action: validate payload: - - version: v0.9 - updateComponents: - surfaceId: contact-card - components: - - id: card1 - component: Card - child: text1 - - id: text1 - component: Text - text: Updated + - version: v0.9 + updateComponents: + surfaceId: contact-card + components: + - id: card1 + component: Card + child: text1 + - id: text1 + component: Text + text: Updated - name: test_incremental_update_orphans_allowed_v08 description: Incremental update with 'orphaned' components should pass. @@ -2286,17 +2286,17 @@ type: string action: validate payload: - - surfaceUpdate: - surfaceId: contact-card - components: - - id: text1 - component: - Text: - text: Hello - - id: text2 - component: - Text: - text: World + - surfaceUpdate: + surfaceId: contact-card + components: + - id: text1 + component: + Text: + text: Hello + - id: text2 + component: + Text: + text: World - name: test_incremental_update_self_ref_still_fails_v08 description: Self-references should still be caught in incremental updates (v0.8). @@ -2313,13 +2313,13 @@ type: string action: validate payload: - - surfaceUpdate: - surfaceId: s1 - components: - - id: card1 - component: - Card: - child: card1 + - surfaceUpdate: + surfaceId: s1 + components: + - id: card1 + component: + Card: + child: card1 expect_error: Self-reference detected - name: test_incremental_update_self_ref_still_fails_v09 @@ -2339,23 +2339,23 @@ child: type: string required: - - component - - child + - component + - child $defs: anyComponent: oneOf: - - $ref: '#/components/Card' + - $ref: "#/components/Card" discriminator: propertyName: component action: validate payload: - - version: v0.9 - updateComponents: - surfaceId: s1 - components: - - id: card1 - component: Card - child: card1 + - version: v0.9 + updateComponents: + surfaceId: s1 + components: + - id: card1 + component: Card + child: card1 expect_error: Self-reference detected - name: test_incremental_update_cycle_still_fails_v08 @@ -2373,17 +2373,17 @@ type: string action: validate payload: - - surfaceUpdate: - surfaceId: s1 - components: - - id: a - component: - Card: - child: b - - id: b - component: - Card: - child: a + - surfaceUpdate: + surfaceId: s1 + components: + - id: a + component: + Card: + child: b + - id: b + component: + Card: + child: a expect_error: Circular reference detected - name: test_incremental_update_cycle_still_fails_v09 @@ -2403,26 +2403,26 @@ child: type: string required: - - component - - child + - component + - child $defs: anyComponent: oneOf: - - $ref: '#/components/Card' + - $ref: "#/components/Card" discriminator: propertyName: component action: validate payload: - - version: v0.9 - updateComponents: - surfaceId: s1 - components: - - id: a - component: Card - child: b - - id: b - component: Card - child: a + - version: v0.9 + updateComponents: + surfaceId: s1 + components: + - id: a + component: Card + child: b + - id: b + component: Card + child: a expect_error: Circular reference detected - name: test_incremental_update_duplicates_still_fail_v08 @@ -2440,17 +2440,17 @@ type: string action: validate payload: - - surfaceUpdate: - surfaceId: s1 - components: - - id: text1 - component: - Text: - text: A - - id: text1 - component: - Text: - text: B + - surfaceUpdate: + surfaceId: s1 + components: + - id: text1 + component: + Text: + text: A + - id: text1 + component: + Text: + text: B expect_error: Duplicate component ID - name: test_incremental_update_duplicates_still_fail_v09 @@ -2470,26 +2470,26 @@ text: type: string required: - - component - - text + - component + - text $defs: anyComponent: oneOf: - - $ref: '#/components/Text' + - $ref: "#/components/Text" discriminator: propertyName: component action: validate payload: - - version: v0.9 - updateComponents: - surfaceId: s1 - components: - - id: text1 - component: Text - text: A - - id: text1 - component: Text - text: B + - version: v0.9 + updateComponents: + surfaceId: s1 + components: + - id: text1 + component: Text + text: A + - id: text1 + component: Text + text: B expect_error: Duplicate component ID - name: test_validate_global_recursion_limit_exceeded_v08 @@ -2505,126 +2505,126 @@ additionalProperties: true action: validate payload: - - beginRendering: - surfaceId: test-surface - root: root - - surfaceUpdate: - surfaceId: test-surface - components: - - id: root - component: - DeepComp: - level: 0 - next: - level: 1 - next: - level: 2 + - beginRendering: + surfaceId: test-surface + root: root + - surfaceUpdate: + surfaceId: test-surface + components: + - id: root + component: + DeepComp: + level: 0 next: - level: 3 + level: 1 next: - level: 4 + level: 2 next: - level: 5 + level: 3 next: - level: 6 + level: 4 next: - level: 7 + level: 5 next: - level: 8 + level: 6 next: - level: 9 + level: 7 next: - level: 10 + level: 8 next: - level: 11 + level: 9 next: - level: 12 + level: 10 next: - level: 13 + level: 11 next: - level: 14 + level: 12 next: - level: 15 + level: 13 next: - level: 16 + level: 14 next: - level: 17 + level: 15 next: - level: 18 + level: 16 next: - level: 19 + level: 17 next: - level: 20 + level: 18 next: - level: 21 + level: 19 next: - level: 22 + level: 20 next: - level: 23 + level: 21 next: - level: 24 + level: 22 next: - level: 25 + level: 23 next: - level: 26 + level: 24 next: - level: 27 + level: 25 next: - level: 28 + level: 26 next: - level: 29 + level: 27 next: - level: 30 + level: 28 next: - level: 31 + level: 29 next: - level: 32 + level: 30 next: - level: 33 + level: 31 next: - level: 34 + level: 32 next: - level: 35 + level: 33 next: - level: 36 + level: 34 next: - level: 37 + level: 35 next: - level: 38 + level: 36 next: - level: 39 + level: 37 next: - level: 40 + level: 38 next: - level: 41 + level: 39 next: - level: 42 + level: 40 next: - level: 43 + level: 41 next: - level: 44 + level: 42 next: - level: 45 + level: 43 next: - level: 46 + level: 44 next: - level: 47 + level: 45 next: - level: 48 + level: 46 next: - level: 49 + level: 47 next: - level: 50 + level: 48 next: - level: 51 + level: 49 next: - level: 52 + level: 50 next: - level: 53 + level: 51 next: - level: 54 + level: 52 next: - level: 55 + level: 53 + next: + level: 54 + next: + level: 55 expect_error: Global recursion limit exceeded - name: test_validate_invalid_child_type @@ -2642,13 +2642,13 @@ type: string action: validate payload: - - surfaceUpdate: - surfaceId: s1 - components: - - id: c1 - component: - Container: - child: 123 + - surfaceUpdate: + surfaceId: s1 + components: + - id: c1 + component: + Container: + child: 123 expect_error: "123 is not of type 'string'" - name: test_custom_catalog_validation_success_v09 @@ -2673,13 +2673,13 @@ discriminator: {propertyName: "component"} action: validate payload: - - version: "v0.9" - updateComponents: - surfaceId: "s1" - components: - - id: "root" - component: "Text" - text: "hello" + - version: "v0.9" + updateComponents: + surfaceId: "s1" + components: + - id: "root" + component: "Text" + text: "hello" - name: test_custom_catalog_validation_success_v08 description: Tests validation success with a custom catalog in v0.8. @@ -2695,13 +2695,13 @@ text: {type: string} action: validate payload: - - surfaceUpdate: - surfaceId: "s1" - components: - - id: "root" - component: - Text: - text: "hello" + - surfaceUpdate: + surfaceId: "s1" + components: + - id: "root" + component: + Text: + text: "hello" - name: test_custom_catalog_validation_failure_v08 description: Tests validation failure with a custom catalog in v0.8. @@ -2717,13 +2717,13 @@ text: {type: string} action: validate payload: - - surfaceUpdate: - surfaceId: "s1" - components: - - id: "root" - component: - Text: - text: 123 + - surfaceUpdate: + surfaceId: "s1" + components: + - id: "root" + component: + Text: + text: 123 expect_error: "123 is not of type 'string'" - name: test_custom_catalog_validation_failure_v09 @@ -2748,11 +2748,11 @@ discriminator: {propertyName: "component"} action: validate payload: - - version: "v0.9" - updateComponents: - surfaceId: "s1" - components: - - id: "root" - component: "Text" - text: 123 + - version: "v0.9" + updateComponents: + surfaceId: "s1" + components: + - id: "root" + component: "Text" + text: 123 expect_error: "123 is not of type 'string'" diff --git a/agent_sdks/python/README.md b/agent_sdks/python/README.md index 6eb2ff538..e00d62c9e 100644 --- a/agent_sdks/python/README.md +++ b/agent_sdks/python/README.md @@ -9,29 +9,29 @@ The following directories contain the base protocol logic, parsing, and schema o ### Schema Management (`src/a2ui/schema`) -* **`manager.py`**: The `A2uiSchemaManager` handles loading specification +- **`manager.py`**: The `A2uiSchemaManager` handles loading specification schemas, managing catalogs, and generating system prompts for LLMs. -* **`validator.py`**: Implements `A2uiValidator` for validating A2UI messages +- **`validator.py`**: Implements `A2uiValidator` for validating A2UI messages against JSON schemas and protocol rules. -* **`catalog.py`**: Defines `A2uiCatalog` and `CatalogConfig` for handling +- **`catalog.py`**: Defines `A2uiCatalog` and `CatalogConfig` for handling component libraries. ### Parser (`src/a2ui/parser`) -* **`parser.py`**: Implementation of `parse_response` for synchronous parsing. -* **`streaming.py`**: Incremental streaming parsers with automatic JSON healing and validation. -* **`payload_fixer.py`**: Utilities to automatically correct common LLM output +- **`parser.py`**: Implementation of `parse_response` for synchronous parsing. +- **`streaming.py`**: Incremental streaming parsers with automatic JSON healing and validation. +- **`payload_fixer.py`**: Utilities to automatically correct common LLM output issues in A2UI payloads. ## Basic Catalog (`src/a2ui/basic_catalog`) -* **`provider.py`**: Implementation of `BasicCatalog` for handling the basic +- **`provider.py`**: Implementation of `BasicCatalog` for handling the basic A2UI components. ## A2A (`src/a2ui/a2a`) -* **`extension.py`**: Utilities for managing the A2UI extension URI and activation logic. -* **`parts.py`**: Utilities for creating A2A Parts with A2UI data and helpers for response parsing. +- **`extension.py`**: Utilities for managing the A2UI extension URI and activation logic. +- **`parts.py`**: Utilities for creating A2A Parts with A2UI data and helpers for response parsing. ## ADK Extensions (`src/a2ui/adk`) @@ -39,7 +39,7 @@ Support for the [Agent Development Kit (ADK)](https://github.com/google/adk-python) and A2A protocol. -* **`send_a2ui_to_client_toolset.py`**: Implementation of +- **`send_a2ui_to_client_toolset.py`**: Implementation of `SendA2uiToClientToolset` to enable agents to send UI to clients via tool calls. diff --git a/agent_sdks/python/agent_development.md b/agent_sdks/python/agent_development.md index 04bb588b5..63eafae78 100644 --- a/agent_sdks/python/agent_development.md +++ b/agent_sdks/python/agent_development.md @@ -8,11 +8,11 @@ message validation for A2A (Agent-to-Agent/Agent-to-Client) communication. The `agent_sdk` revolves around three main classes: -* **`CatalogConfig`**: Defines the metadata for a component catalog (name, +- **`CatalogConfig`**: Defines the metadata for a component catalog (name, schema path, examples path). -* **`A2uiCatalog`**: Represents a processed catalog, providing methods for +- **`A2uiCatalog`**: Represents a processed catalog, providing methods for validation and LLM instruction rendering. -* **`A2uiSchemaManager`**: The central coordinator that loads catalogs, manages +- **`A2uiSchemaManager`**: The central coordinator that loads catalogs, manages versioning, and generates system prompts. ## Prerequisites @@ -188,10 +188,11 @@ yield { ##### Option B: Incremental Streaming Parsing (Advanced) -Use this approach for sub-second UI updates. The `A2uiStreamParser` **automatically parses, validates, and fixes (heals)** the JSON payload chunks *incrementally* as they arrive from the LLM stream. It yields valid UI messages *before* the entire JSON block is complete by automatically closing open quotes and braces. +Use this approach for sub-second UI updates. The `A2uiStreamParser` **automatically parses, validates, and fixes (heals)** the JSON payload chunks _incrementally_ as they arrive from the LLM stream. It yields valid UI messages _before_ the entire JSON block is complete by automatically closing open quotes and braces. > [!IMPORTANT] > **Prerequisite**: To use incremental streaming, your agent executor must support streaming mode. In ADK, enable this using `RunConfig`: +> > ```python > run_config=run_config.RunConfig( > streaming_mode=run_config.StreamingMode.SSE @@ -208,7 +209,7 @@ parser = A2uiStreamParser(catalog=selected_catalog) for chunk in llm_response_stream: # Process text chunks as they arrive response_parts = parser.process_chunk(chunk.text) - + for part in response_parts: if part.a2ui_json: # Yield partial UI updates immediately @@ -330,7 +331,6 @@ When the LLM calls the UI tool, the toolset uses the dynamic catalog to: 3. **Validate Payloads**: Validate the LLM's generated JSON against the specific `A2uiCatalog` object's validator. - ### 3. Multiple Version Support To support multiple protocol versions (e.g., v0.8 and v0.9), pre-configure `A2uiSchemaManager` and `LlmAgent` instances for each version during your agent's initialization. At runtime, use `try_activate_a2ui_extension` to negotiate the version and select the pre-configured schema manager or runner. @@ -397,6 +397,3 @@ agent_card = AgentCard( ) ) ``` - - - diff --git a/docs/composer.md b/docs/composer.md index 44f9be31c..f1e3521f3 100644 --- a/docs/composer.md +++ b/docs/composer.md @@ -15,4 +15,4 @@ The Widget Builder enables you to: - View real-time previews. - Copy the generated JSON for use in agents. -Built by the [CopilotKit](https://www.copilotkit.ai/) team. \ No newline at end of file +Built by the [CopilotKit](https://www.copilotkit.ai/) team. diff --git a/docs/concepts/actions.md b/docs/concepts/actions.md index ac262d9e0..b60c0d832 100644 --- a/docs/concepts/actions.md +++ b/docs/concepts/actions.md @@ -21,13 +21,14 @@ Functions execute immediate behavior on the renderer without a network round-tri "action": { "functionCall": { "call": "openUrl", - "args": { "url": "https://a2ui.org/help" } + "args": {"url": "https://a2ui.org/help"} } } } ``` Common uses for Functions include: + - **Navigation**: Opening a URL or switching tabs. - **Validation**: Checking inputs before submission (see Checks below). @@ -46,8 +47,8 @@ Components like `Button` expose an `action` property. Here is how an Event is wi "event": { "name": "submit_reservation", "context": { - "time": { "path": "/reservationTime" }, - "size": { "path": "/partySize" } + "time": {"path": "/reservationTime"}, + "size": {"path": "/partySize"} } } } @@ -55,11 +56,10 @@ Components like `Button` expose an `action` property. Here is how an Event is wi ``` - **`name`**: A stable identifier for the agent to switch on. -- **`context`**: A map of key-value pairs. Values can be literal or use a `path` to pull from the current state of the data model. +- **`context`**: A map of key-value pairs. Values can be literal or use a `path` to pull from the current state of the data model. NOTE: **Context vs. Data Model**: While the Data Model represents the entire state tree of a surface, the `context` in an action is effectively a hand-picked **"view"** or subset of that state. This simplifies the Agent's job by providing exactly the values needed for a specific event, without requiring the Agent to navigate a potentially large and complex data model. - ### Basic Catalog Function Validation (Checks) The basic catalog defines a limited set of checks that can be performed on the renderer. Interactive components can define a list of `checks` (using the [`Checkable`](../../specification/v0_9/json/common_types.json#L258-L270) schema in `common_types.json`). For a `Button`, if any check fails, the button is **automatically disabled** on the renderer. @@ -77,12 +77,12 @@ This allows the UI to enforce requirements (like a non-empty field) before the u { "condition": { "call": "required", - "args": { "value": { "path": "/partySize" } } + "args": {"value": {"path": "/partySize"}} }, "message": "Party size is required" } ], - "action": { "event": { "name": "submit_booking" } } + "action": {"event": {"name": "submit_booking"}} } ``` @@ -203,7 +203,7 @@ In an A2A (Agent-to-Agent) binding, the data model is placed in an `a2uiClientDa ```json { - "parts": [{ "text": "Submit the reservation" }], + "parts": [{"text": "Submit the reservation"}], "metadata": { "a2uiClientDataModel": { "version": "v0.9", @@ -322,10 +322,10 @@ When the renderer sends an `action` back to the orchestrator, the orchestrator l async def handle_incoming_action(payload, session): action = payload.get("action") surface_id = action.get("surfaceId") - + # Lookup the owning agent target_agent = session.state.get(f"owner_of_{surface_id}") - + if target_agent: # Programmatically route the request to the sub-agent return transfer_to(target_agent) @@ -344,14 +344,14 @@ This is best implemented in an outbound metadata interceptor: async def intercept(self, request_payload, target_agent, session): message = request_payload["params"]["message"] data_model = message.get("metadata", {}).get("a2uiClientDataModel") - + if data_model: # Filter surfaces to only those owned by the target_agent filtered_surfaces = { surface_id: state for surface_id, state in data_model["surfaces"].items() if session.state.get(f"owner_of_{surface_id}") == target_agent.name } - + # Replace with the stripped data model message["metadata"]["a2uiClientDataModel"]["surfaces"] = filtered_surfaces @@ -371,6 +371,7 @@ CAUTION: **Security Risk: State Scraping**: If an Orchestrator fails to strip th This example shows a button that explicitly gathers the data it needs to send. **Component Definition:** + ```json { "id": "submit-button", @@ -380,8 +381,8 @@ This example shows a button that explicitly gathers the data it needs to send. "event": { "name": "submit_booking", "context": { - "partySize": { "path": "/partySize" }, - "reservationTime": { "path": "/reservationTime" } + "partySize": {"path": "/partySize"}, + "reservationTime": {"path": "/reservationTime"} } } } @@ -414,7 +415,7 @@ The renderer sends an A2A message containing the user's text and the data model ```json { - "parts": [{ "text": "Okay, submit the form" }], + "parts": [{"text": "Okay, submit the form"}], "metadata": { "a2uiClientDataModel": { "version": "v0.9", diff --git a/docs/concepts/catalogs.md b/docs/concepts/catalogs.md index fa943bec1..0bc11a883 100644 --- a/docs/concepts/catalogs.md +++ b/docs/concepts/catalogs.md @@ -43,9 +43,7 @@ A catalog schema is a [JSON Schema file](../../specification/v0_9/json/client_ca } } }, - "required": [ - "catalogId" - ], + "required": ["catalogId"], "additionalProperties": false } } @@ -61,7 +59,7 @@ Whether you are building a simple prototype or a complex production application, To help developers get started quickly, the A2UI team maintains the [Basic Catalog](../../specification/v0_9/json/basic_catalog.json). -This is a pre-defined catalog file that contains a standard set of general-purpose components (Buttons, Inputs, Cards) and functions. It is not a special "type" of catalog; it is simply a version of a catalog that we have already written and have open source renderers for. +This is a pre-defined catalog file that contains a standard set of general-purpose components (Buttons, Inputs, Cards) and functions. It is not a special "type" of catalog; it is simply a version of a catalog that we have already written and have open source renderers for. The basic catalog allows you to bootstrap an application or validate A2UI concepts without needing to write your own schema from scratch. It is intentionally sparse to remain easily implementable by different renderers. @@ -111,9 +109,7 @@ Here is a simple catalog defining a single component. "default": "#f0f0f0" } }, - "required": [ - "message" - ] + "required": ["message"] } } } @@ -149,9 +145,9 @@ When the agent uses that catalog, it generates a payload strictly conforming to ### Freestanding Catalogs -A2UI Catalogs must be standalone (no references to external files) to simplify LLM inference and dependency management. +A2UI Catalogs must be standalone (no references to external files) to simplify LLM inference and dependency management. -While the final catalog must be freestanding, you may still author your catalogs modularly using JSON Schema `$ref` pointing to external documents during local development. Run `tools/build_catalog/assemble_catalog.py` before distributing your catalog to bundle all external file references into a single, independent JSON Schema file: +While the final catalog must be freestanding, you may still author your catalogs modularly using JSON Schema `$ref` pointing to external documents during local development. Run `tools/build_catalog/assemble_catalog.py` before distributing your catalog to bundle all external file references into a single, independent JSON Schema file: ```bash uv run tools/build_catalog/assemble_catalog.py [INPUTS ...] --output-name [--catalog-id ] [--version ] [--extend-basic-catalog] [--out-dir ] [--verbose] @@ -159,18 +155,18 @@ uv run tools/build_catalog/assemble_catalog.py [INPUTS ...] --output-name `. -* `--version`: The A2UI specification version to use for official catalog +- `--catalog-id`: Custom `catalogId` for the output. Defaults to `urn:a2ui:catalog:`. +- `--version`: The A2UI specification version to use for official catalog fallbacks. Choices are `0.9` or `0.10`. Defaults to `0.9`. -* `--extend-basic-catalog`: If passed, automatically includes the entirety of +- `--extend-basic-catalog`: If passed, automatically includes the entirety of `basic_catalog.json` in the root output regardless of whether the input catalogs explicitly reference it. -* `--out-dir`, `-o`: The directory where the assembled catalog will be saved. Defaults to `dist`. -* `--verbose`, `-v`: If passed, enables verbose debug logging to help diagnose issues. +- `--out-dir`, `-o`: The directory where the assembled catalog will be saved. Defaults to `dist`. +- `--verbose`, `-v`: If passed, enables verbose debug logging to help diagnose issues. ### Composition & Imports @@ -185,7 +181,7 @@ This catalog imports all elements from the Basic Catalog and adds a new `Suggest "$id": "https://github.com/.../hello_world_with_all_basic/v1/catalog.json", "components": { "allOf": [ - { "$ref": "basic_catalog_definition.json#/components" }, + {"$ref": "basic_catalog_definition.json#/components"}, { "SuggestionChips": { "type": "object", @@ -196,7 +192,7 @@ This catalog imports all elements from the Basic Catalog and adds a new `Suggest "description": "The suggested prompts." } }, - "required": [ "suggestions" ] + "required": ["suggestions"] } } ] @@ -215,15 +211,15 @@ This catalog imports only `Text` from the Basic Catalog to build a simple Popup "$id": "https://github.com/.../hello_world_with_some_basic/v1/catalog.json", "components": { "allOf": [ - { "$ref": "basic_catalog.json#/components/Text" }, + {"$ref": "basic_catalog.json#/components/Text"}, { - "Popup": { + "Popup": { "type": "object", "description": "A modal overlay that displays an icon and text.", "properties": { - "text": { "$ref": "common_types.json#/$defs/ComponentId" } + "text": {"$ref": "common_types.json#/$defs/ComponentId"} }, - "required": [ "text" ] + "required": ["text"] } } ] @@ -240,29 +236,32 @@ Client renderers implement the catalog by mapping the schema definition to actua Example typescript renderer for the hello world catalog ```typescript -import { Catalog, DEFAULT_CATALOG } from '@a2ui/angular'; -import { inputBinding } from '@angular/core'; - -export const RIZZ_CHARTS_CATALOG = { - ...DEFAULT_CATALOG, // Include the basic catalog - HelloWorldBanner: { - type: () => import('./hello_world_banner').then((r) => r.HelloWorldBanner), - bindings: ({ properties }) => [ - inputBinding('message', () => ('message' in properties && properties['message']) || undefined) - ], - }, +import {Catalog, DEFAULT_CATALOG} from '@a2ui/angular'; +import {inputBinding} from '@angular/core'; + +export const RIZZ_CHARTS_CATALOG = { + ...DEFAULT_CATALOG, // Include the basic catalog + HelloWorldBanner: { + type: () => import('./hello_world_banner').then(r => r.HelloWorldBanner), + bindings: ({properties}) => [ + inputBinding( + 'message', + () => ('message' in properties && properties['message']) || undefined, + ), + ], + }, } as Catalog; ``` and the hello_world_banner implementation ```typescript -import { DynamicComponent } from '@a2ui/angular'; -import { Component, Input } from '@angular/core'; +import {DynamicComponent} from '@a2ui/angular'; +import {Component, Input} from '@angular/core'; @Component({ selector: 'hello-world-banner', - imports: [], + imports: [], template: `

Hello World Banner

@@ -281,7 +280,7 @@ You can see a working example of a client renderer in the [Rizzcharts demo](../. Because clients and agents can support multiple catalogs, they must agree on which catalog to use through a catalog negotiation handshake. -### Step 1: Agent advertises its support catalogs (optional) +### Step 1: Agent advertises its support catalogs (optional) The agent may optionally advertise which catalogs it is capable of speaking (e.g., in the A2A Agent Card). This is informational; it helps the client know if the agent supports their specific features, but the client doesn’t have to use it. @@ -355,9 +354,9 @@ A2UI component catalogs require versioning because catalog definitions are often The `catalogId` is a unique text identifier used for negotiation between the client and the agent. -* **Format:** While the `catalogId` is technically a string, the A2UI convention is to use a **URI** (e.g., `https://example.com/catalogs/mysurface/v1/catalog.json`). -* **Purpose:** We use URIs to make the ID globally unique and easy for human developers to inspect in a browser. -* **No Runtime Fetching:** This URI does not imply that the agent or client downloads the catalog at runtime. **The catalog definition must be known to the agent and client beforehand (at compile/deploy time)**. The URI serves only as a stable identifier. +- **Format:** While the `catalogId` is technically a string, the A2UI convention is to use a **URI** (e.g., `https://example.com/catalogs/mysurface/v1/catalog.json`). +- **Purpose:** We use URIs to make the ID globally unique and easy for human developers to inspect in a browser. +- **No Runtime Fetching:** This URI does not imply that the agent or client downloads the catalog at runtime. **The catalog definition must be known to the agent and client beforehand (at compile/deploy time)**. The URI serves only as a stable identifier. ### Versioning Guidelines @@ -365,20 +364,20 @@ To support continuous evolution without breaking older clients or agents, A2UI c While standard JSON parsers ignore unknown fields, dropping a component in a Server-Driven UI can drop its entire view tree. To balance safety and flexibility, updates are split into **Breaking** and **Non-Breaking** categories, relying on **Graceful Degradation** to absorb version lags. -* **Breaking Changes (Major Version Bump Required)** - Any change that alters structure in a way that cannot be safely ignored by older clients incrementing the **Major** version in the `catalogId` URI (e.g., `v1` to `v2`). - * **Adding a container component:** e.g., adding a `Grid` or `Accordion` component. If an older client ignores a container, it will drop all of its children, breaking the UI tree. - * **Removing a container component:** e.g., removing a `Grid` or `Accordion` component. If an older agent uses the container it would be ignored by the client, and the client would drop all of its children, breaking the UI tree. - * **Changing field types:** e.g., changing a property from a `string` to an `object`. This will fail JSON Schema validation on older clients. - * **Adding a required property:** without a default value, as older agents won't know to send it. - -* **Non-Breaking Changes (Allowable under Major Version)** - Changes that can be safely ignored or degrade gracefully without breaking the layout or data model can stay at the current version. - * **Adding a leaf component (non-container):** e.g., adding `Badge` or `Tooltip`. If ignored, the layout remains intact. - * **Adding an optional property:** e.g., adding `subtitle` to a Card. - * **Removing a property:** Safe for the client to ignore if the agent stops sending it. - * **Adding new functions or styles:** These can generally be ignored without changing the semantic meaning of the component. - * **Metadata Changes:** Updating `description` fields or fixing typos in docs requires no version bump and has no impact on runtime. +- **Breaking Changes (Major Version Bump Required)** + Any change that alters structure in a way that cannot be safely ignored by older clients incrementing the **Major** version in the `catalogId` URI (e.g., `v1` to `v2`). + - **Adding a container component:** e.g., adding a `Grid` or `Accordion` component. If an older client ignores a container, it will drop all of its children, breaking the UI tree. + - **Removing a container component:** e.g., removing a `Grid` or `Accordion` component. If an older agent uses the container it would be ignored by the client, and the client would drop all of its children, breaking the UI tree. + - **Changing field types:** e.g., changing a property from a `string` to an `object`. This will fail JSON Schema validation on older clients. + - **Adding a required property:** without a default value, as older agents won't know to send it. + +- **Non-Breaking Changes (Allowable under Major Version)** + Changes that can be safely ignored or degrade gracefully without breaking the layout or data model can stay at the current version. + - **Adding a leaf component (non-container):** e.g., adding `Badge` or `Tooltip`. If ignored, the layout remains intact. + - **Adding an optional property:** e.g., adding `subtitle` to a Card. + - **Removing a property:** Safe for the client to ignore if the agent stops sending it. + - **Adding new functions or styles:** These can generally be ignored without changing the semantic meaning of the component. + - **Metadata Changes:** Updating `description` fields or fixing typos in docs requires no version bump and has no impact on runtime. ### Graceful Degradation @@ -388,15 +387,15 @@ While standard JSON parsers ignore unknown fields, dropping a component in a Ser Here is how catalog version mismatches are handled in practice: -* **An old iOS client is using an older catalog than the agent** - * The agent sends a new component `Badge` that the old iOS client doesn't know about. The client renders a generic textbox placeholder or safe text description for it, keeping the rest of the interface functional. - * The agent sends a new property `badge` on a `Button` that an old client doesn't know about. The client safely ignores it and renders the standard button. - * The agent no longer sends the `Facepile` component that was removed in a later catalog version. This causes no issues for the client. +- **An old iOS client is using an older catalog than the agent** + - The agent sends a new component `Badge` that the old iOS client doesn't know about. The client renders a generic textbox placeholder or safe text description for it, keeping the rest of the interface functional. + - The agent sends a new property `badge` on a `Button` that an old client doesn't know about. The client safely ignores it and renders the standard button. + - The agent no longer sends the `Facepile` component that was removed in a later catalog version. This causes no issues for the client. -* **A web client rolls out a new catalog version ahead of the agent** - * The web client supports the new `Badge` component, but the agent doesn't know about it yet. - * The web client removed the `badge` property on `Button`, so it ignores it if the agent sends it. - * The web client added new styles for `Button` that the agent doesn't know about. Again this causes no issues as the agent doesn't use them. +- **A web client rolls out a new catalog version ahead of the agent** + - The web client supports the new `Badge` component, but the agent doesn't know about it yet. + - The web client removed the `badge` property on `Button`, so it ignores it if the agent sends it. + - The web client added new styles for `Button` that the agent doesn't know about. Again this causes no issues as the agent doesn't use them. ### Versioning with CatalogId @@ -413,22 +412,22 @@ We recommend including the version in the catalogId. This allows using A2UI cata To upgrade a catalog without breaking active agents, use A2UI Catalog Negotiation: -1. **Client Update:** The client updates its list of supportedCatalogIds to include *both* the old and new versions (e.g., [".../v2/...", ".../v1/..."]). -2. **Agent Update:** Agents are rebuilt with the v2 schema. When they see the client supports v2, they prefer it. +1. **Client Update:** The client updates its list of supportedCatalogIds to include _both_ the old and new versions (e.g., [".../v2/...", ".../v1/..."]). +2. **Agent Update:** Agents are rebuilt with the v2 schema. When they see the client supports v2, they prefer it. 3. **Legacy Support:** Older agents that have not yet been rebuilt will continue to match against v1 in the client's list, ensuring they remain functional. -## A2UI Schema Validation & Fallback +## A2UI Schema Validation & Fallback To ensure a stable user experience, A2UI employs a two-phase validation strategy. This "defense in depth" approach catches errors as early as possible while ensuring clients remain robust when facing unexpected payloads. ### Two-Phase Validation -1. **Agent-Side (Pre-Send):** Before transmitting any UI payload, the agent runtime validates the generated JSON against the catalog definition. - * Purpose: To catch hallucinated properties or malformed structures at the source. - * Outcome: If validation fails, the agent can attempt to fix or regenerate the A2UI JSON, or it can do graceful degradation such as falling back to text in a conversational app. -2. **Client-Side:** Upon receiving the payload, the client library validates the JSON against its local definition of the catalog. - * Purpose: Security and stability. This ensures that the code executing on the user's device strictly conforms to the expected contract, protecting against version mismatches or compromised agent outputs. - * Outcome: Failures here are reported back to the agent using the “error” client message +1. **Agent-Side (Pre-Send):** Before transmitting any UI payload, the agent runtime validates the generated JSON against the catalog definition. + - Purpose: To catch hallucinated properties or malformed structures at the source. + - Outcome: If validation fails, the agent can attempt to fix or regenerate the A2UI JSON, or it can do graceful degradation such as falling back to text in a conversational app. +2. **Client-Side:** Upon receiving the payload, the client library validates the JSON against its local definition of the catalog. + - Purpose: Security and stability. This ensures that the code executing on the user's device strictly conforms to the expected contract, protecting against version mismatches or compromised agent outputs. + - Outcome: Failures here are reported back to the agent using the “error” client message ### Graceful Degradation @@ -436,8 +435,8 @@ Even if a payload passes schema validation, the renderer may encounter runtime i Clients should not crash when encountering these errors. Instead, they should employ Graceful Degradation: -* **Unknown Components:** If a component is recognized in the schema but not implemented in the renderer, render a "safe" fallback (e.g., a generic card with the component's debug name) or skip rendering that specific node entirely. -* **Text Fallback:** If the entire surface fails to render, display the raw text description (if available) or a generic error message: *"This interface could not be displayed."* +- **Unknown Components:** If a component is recognized in the schema but not implemented in the renderer, render a "safe" fallback (e.g., a generic card with the component's debug name) or skip rendering that specific node entirely. +- **Text Fallback:** If the entire surface fails to render, display the raw text description (if available) or a generic error message: _"This interface could not be displayed."_ ### Client-to-Server Error Reporting diff --git a/docs/concepts/components.md b/docs/concepts/components.md index 50c207a5c..8cb28a44e 100644 --- a/docs/concepts/components.md +++ b/docs/concepts/components.md @@ -180,6 +180,7 @@ For the complete component gallery with examples, see [Component Reference](../r ## Static vs. Dynamic Children **Static (`explicitList`)** - Fixed list of child IDs: + ```json { "children": { @@ -189,6 +190,7 @@ For the complete component gallery with examples, see [Component Reference](../r ``` **Dynamic (`template`)** - Generate children from data array: + ```json { "children": { diff --git a/docs/concepts/data-binding.md b/docs/concepts/data-binding.md index a23d97746..bc8fd353c 100644 --- a/docs/concepts/data-binding.md +++ b/docs/concepts/data-binding.md @@ -10,6 +10,7 @@ A2UI separates: 2. **Application State** (Data Model): What data it displays This enables: + - Reactive updates. - Data-driven UIs. - Reusable templates. @@ -105,7 +106,7 @@ Components bound to data paths automatically update when the data changes: "id": "status", "component": { "Text": { - "text": { "path": "/order/status" } + "text": {"path": "/order/status"} } } } @@ -137,11 +138,12 @@ Use templates to render arrays: ``` **Data:** + ```json { "products": [ - { "name": "Widget", "price": 9.99 }, - { "name": "Gadget", "price": 19.99 } + {"name": "Widget", "price": 9.99}, + {"name": "Gadget", "price": 19.99} ] } ``` @@ -157,7 +159,7 @@ Inside a template, paths are scoped to the array item: "id": "product-name", "component": { "Text": { - "text": { "path": "/name" } + "text": {"path": "/name"} } } } @@ -172,32 +174,32 @@ Adding/removing items automatically updates the rendered components. Interactive components update the data model bidirectionally: -| Component | Example | User Action | Data Update | -|-----------|---------|-------------|-------------| -| **TextField** | `{"text": {"path": "/form/name"}}` | Types "Alice" | `/form/name` = "Alice" | -| **CheckBox** | `{"value": {"path": "/form/agreed"}}` | Checks box | `/form/agreed` = true | +| Component | Example | User Action | Data Update | +| ------------------ | ------------------------------------------- | ---------------- | ------------------------ | +| **TextField** | `{"text": {"path": "/form/name"}}` | Types "Alice" | `/form/name` = "Alice" | +| **CheckBox** | `{"value": {"path": "/form/agreed"}}` | Checks box | `/form/agreed` = true | | **MultipleChoice** | `{"selections": {"path": "/form/country"}}` | Selects "Canada" | `/form/country` = ["ca"] | ## Best Practices - **Use granular updates**: Update only changed paths. + ```json { "dataModelUpdate": { "path": "/user", - "contents": [ - { "key": "name", "valueString": "Alice" } - ] + "contents": [{"key": "name", "valueString": "Alice"}] } } ``` - **Organize by domain**: Group related data. + ```json {"user": {...}, "cart": {...}, "ui": {...}} ``` - **Pre-compute display values**: Formats data (currency, dates) on the agent before sending. ```json - {"price": "$19.99"} // Not: {"price": 19.99} + {"price": "$19.99"} // Not: {"price": 19.99} ``` diff --git a/docs/concepts/overview.md b/docs/concepts/overview.md index 794f564ce..7fb2d8d8b 100644 --- a/docs/concepts/overview.md +++ b/docs/concepts/overview.md @@ -15,12 +15,15 @@ A2UI is built around three core ideas: ## Key Topics ### [Data Flow](data-flow.md) + How messages travel from agents to rendered UI. Includes a complete lifecycle example of a restaurant booking flow, transport options (SSE, WebSockets, A2A), progressive rendering, and error handling. ### [Component Structure](components.md) + A2UI's **adjacency list model** for representing component hierarchies. Learn why flat lists are better than nested trees, how to use static vs. dynamic children, and best practices for incremental updates. ### [Data Binding](data-binding.md) + How components connect to application state using JSON Pointer paths. Covers reactive components, dynamic lists, input bindings, and the separation of structure from state that makes A2UI powerful. ## Message Types diff --git a/docs/concepts/transports.md b/docs/concepts/transports.md index 2e9bfaa1c..7017b2b89 100644 --- a/docs/concepts/transports.md +++ b/docs/concepts/transports.md @@ -16,18 +16,18 @@ A2UI defines a sequence of JSON messages. The transport layer is responsible for ## Available Transports -| Transport | Status | Use Case | -|-----------|--------|----------| -| **A2A Protocol** | ✅ Stable | Multi-agent systems, enterprise meshes | -| **AG UI** | ✅ Stable | Full-stack React applications | -| **REST API** | 📋 Planned | Simple HTTP endpoints | -| **WebSockets** | 💡 Proposed | Real-time bidirectional | -| **SSE (Server-Sent Events)** | 💡 Proposed | Web streaming | +| Transport | Status | Use Case | +| ---------------------------- | ----------- | -------------------------------------- | +| **A2A Protocol** | ✅ Stable | Multi-agent systems, enterprise meshes | +| **AG UI** | ✅ Stable | Full-stack React applications | +| **REST API** | 📋 Planned | Simple HTTP endpoints | +| **WebSockets** | 💡 Proposed | Real-time bidirectional | +| **SSE (Server-Sent Events)** | 💡 Proposed | Web streaming | ## A2A Protocol The [Agent2Agent (A2A) protocol](https://a2a-protocol.org) provides secure, -standardized agent communication. An A2A extension provides easy integration with A2UI. +standardized agent communication. An A2A extension provides easy integration with A2UI. **Benefits:** @@ -72,4 +72,4 @@ You can use any transport that sends JSON: ## Next Steps - **[A2A Protocol Docs](https://a2a-protocol.org)**: Learn about A2A -- **[A2A Extension Spec](../specification/v0.8-a2a-extension.md)**: A2UI + A2A details \ No newline at end of file +- **[A2A Extension Spec](../specification/v0.8-a2a-extension.md)**: A2UI + A2A details diff --git a/docs/ecosystem/a2ui-in-the-world.md b/docs/ecosystem/a2ui-in-the-world.md index 236cb977b..a8601ba2b 100644 --- a/docs/ecosystem/a2ui-in-the-world.md +++ b/docs/ecosystem/a2ui-in-the-world.md @@ -83,6 +83,7 @@ ADK integrated the A2UI v0.8 standard catalog to automatically render spec-compl - **Agent SDK**: The [A2UI Python agent SDK](https://github.com/google/A2UI/tree/main/agent_sdks/python) provides an ADK extension for generating A2UI from agents. **Try it:** + - [ADK Documentation](https://google.github.io/adk-docs/) - [ADK Web](https://github.com/google/adk-web) (developer UI with A2UI support) - [Agent Development Guide](../guides/agent-development.md) (building A2UI agents with ADK) @@ -165,19 +166,19 @@ The A2UI community is building exciting projects: ### Open Source Examples - **Restaurant Finder** ([samples/agent/adk/restaurant_finder](https://github.com/google/A2UI/tree/main/samples/agent/adk/restaurant_finder)) - - Table reservation with dynamic forms - - Gemini-powered agent - - Full source code available + - Table reservation with dynamic forms + - Gemini-powered agent + - Full source code available - **Contact Lookup** ([samples/agent/adk/contact_lookup](https://github.com/google/A2UI/tree/main/samples/agent/adk/contact_lookup)) - - Search interface with results list - - A2A agent example - - Demonstrates data binding + - Search interface with results list + - A2A agent example + - Demonstrates data binding - **Component Gallery** ([samples/client/angular - gallery mode](https://github.com/google/A2UI/tree/main/samples/client/angular)) - - Interactive showcase of all components - - Live examples with code - - Great for learning + - Interactive showcase of all components + - Live examples with code + - Great for learning ### Third-Party Integrations diff --git a/docs/ecosystem/renderers.md b/docs/ecosystem/renderers.md index b228f8bdc..f6a3f2bd2 100644 --- a/docs/ecosystem/renderers.md +++ b/docs/ecosystem/renderers.md @@ -10,15 +10,14 @@ Community and third-party A2UI renderer implementations. ## Community Renderers -| Renderer | Platform | v0.8 | v0.9 | Activity | Links | -|----------|----------|------|------|----------|-------| -| **easyops-cn/a2ui-sdk** (`@a2ui-sdk/react`) | React (Web) | ✅ | ❌ | ![Stars](https://img.shields.io/github/stars/easyops-cn/a2ui-sdk?style=flat-square&label=⭐) ![Last commit](https://img.shields.io/github/last-commit/easyops-cn/a2ui-sdk?style=flat-square&label=updated) | [GitHub](https://github.com/easyops-cn/a2ui-sdk) · [npm](https://www.npmjs.com/package/@a2ui-sdk/react) · [Docs](https://a2ui-sdk.js.org/) | -| **lmee/A2UI-Android** | Android (Compose) | ✅ | ❌ | ![Stars](https://img.shields.io/github/stars/lmee/A2UI-Android?style=flat-square&label=⭐) ![Last commit](https://img.shields.io/github/last-commit/lmee/A2UI-Android?style=flat-square&label=updated) | [GitHub](https://github.com/lmee/A2UI-Android) | -| **sivamrudram-eng/a2ui-react-native** | React Native | ✅ | ❌ | ![Stars](https://img.shields.io/github/stars/sivamrudram-eng/a2ui-react-native?style=flat-square&label=⭐) ![Last commit](https://img.shields.io/github/last-commit/sivamrudram-eng/a2ui-react-native?style=flat-square&label=updated) | [GitHub](https://github.com/sivamrudram-eng/a2ui-react-native) | -| **zhama/a2ui** | React (Web) | ✅ | ❌ | — | [npm](https://www.npmjs.com/package/@zhama/a2ui) | -| **jem-computer/A2UI-react** | React (Web) | ✅ | ❌ | ![Stars](https://img.shields.io/github/stars/jem-computer/A2UI-react?style=flat-square&label=⭐) ![Last commit](https://img.shields.io/github/last-commit/jem-computer/A2UI-react?style=flat-square&label=updated) | [GitHub](https://github.com/jem-computer/A2UI-react) | -| **BBC6BAE9/a2ui-swift** | Apple (iOS, iPadOS, macOS, tvOS, watchOS, visionOS) | ✅ | ✅ | ![Stars](https://img.shields.io/github/stars/BBC6BAE9/a2ui-swift?style=flat-square&label=⭐) ![Last commit](https://img.shields.io/github/last-commit/BBC6BAE9/a2ui-swift?style=flat-square&label=updated) | [GitHub](https://github.com/BBC6BAE9/a2ui-swift) | - +| Renderer | Platform | v0.8 | v0.9 | Activity | Links | +| ------------------------------------------- | --------------------------------------------------- | ---- | ---- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | +| **easyops-cn/a2ui-sdk** (`@a2ui-sdk/react`) | React (Web) | ✅ | ❌ | ![Stars](https://img.shields.io/github/stars/easyops-cn/a2ui-sdk?style=flat-square&label=⭐) ![Last commit](https://img.shields.io/github/last-commit/easyops-cn/a2ui-sdk?style=flat-square&label=updated) | [GitHub](https://github.com/easyops-cn/a2ui-sdk) · [npm](https://www.npmjs.com/package/@a2ui-sdk/react) · [Docs](https://a2ui-sdk.js.org/) | +| **lmee/A2UI-Android** | Android (Compose) | ✅ | ❌ | ![Stars](https://img.shields.io/github/stars/lmee/A2UI-Android?style=flat-square&label=⭐) ![Last commit](https://img.shields.io/github/last-commit/lmee/A2UI-Android?style=flat-square&label=updated) | [GitHub](https://github.com/lmee/A2UI-Android) | +| **sivamrudram-eng/a2ui-react-native** | React Native | ✅ | ❌ | ![Stars](https://img.shields.io/github/stars/sivamrudram-eng/a2ui-react-native?style=flat-square&label=⭐) ![Last commit](https://img.shields.io/github/last-commit/sivamrudram-eng/a2ui-react-native?style=flat-square&label=updated) | [GitHub](https://github.com/sivamrudram-eng/a2ui-react-native) | +| **zhama/a2ui** | React (Web) | ✅ | ❌ | — | [npm](https://www.npmjs.com/package/@zhama/a2ui) | +| **jem-computer/A2UI-react** | React (Web) | ✅ | ❌ | ![Stars](https://img.shields.io/github/stars/jem-computer/A2UI-react?style=flat-square&label=⭐) ![Last commit](https://img.shields.io/github/last-commit/jem-computer/A2UI-react?style=flat-square&label=updated) | [GitHub](https://github.com/jem-computer/A2UI-react) | +| **BBC6BAE9/a2ui-swift** | Apple (iOS, iPadOS, macOS, tvOS, watchOS, visionOS) | ✅ | ✅ | ![Stars](https://img.shields.io/github/stars/BBC6BAE9/a2ui-swift?style=flat-square&label=⭐) ![Last commit](https://img.shields.io/github/last-commit/BBC6BAE9/a2ui-swift?style=flat-square&label=updated) | [GitHub](https://github.com/BBC6BAE9/a2ui-swift) | ### Notable Mentions @@ -32,8 +31,8 @@ These projects are early-stage or experimental: These projects are not directly A2UI renderers but are closely related and do support A2UI: -| Project | Platform | Description | Links | -|---------|----------|-------------|-------| +| Project | Platform | Description | Links | +| ---------------------------------------------- | --------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | | **vercel-labs/json-render** (`@json-render/*`) | React, Vue, Svelte, Solid, React Native | Generative UI framework by Vercel — uses its own JSON schema (not A2UI protocol) with Zod-based component catalogs. Supports streaming, 36 pre-built shadcn/ui components, and cross-platform rendering. | [GitHub](https://github.com/vercel-labs/json-render) · [npm](https://www.npmjs.com/package/@json-render/core) · [Docs](https://json-render.dev/) | ### Highlights diff --git a/docs/glossary.md b/docs/glossary.md index f77820a84..8dba207f2 100644 --- a/docs/glossary.md +++ b/docs/glossary.md @@ -7,11 +7,12 @@ Terms, required by A2UI protocol. ### A2UI agent and A2UI renderer The A2UI protocol enables conversation between **agent** and **renderer**: + 1. **Renderer** provides **UI capabilities** in the form of A2UI catalog and **instructions** on how to use it. 2. **Agent** iterates on the loop: - - Provides **UI** and **functions** to call, taking into account the received catalog - - Receives **user input**, communicated by renderer - - Updates **data** to show in UI + - Provides **UI** and **functions** to call, taking into account the received catalog + - Receives **user input**, communicated by renderer + - Updates **data** to show in UI ![agent and renderer](assets/agent-and-renderer.png) @@ -19,7 +20,7 @@ While the protocol is designed for **AI-empowered agents**, it can work with det In case the agent is stateless or does not guarantee to preserve the catalog, the renderer should provide the catalog with every message. -And, sometimes, an agent is using a predefined catalog, thus forcing the renderers to either support this catalog or use an adapter. +And, sometimes, an agent is using a predefined catalog, thus forcing the renderers to either support this catalog or use an adapter. ### GenUI Component @@ -28,16 +29,16 @@ UI component, allowed for use by agent. Examples: date picker, carousel, button, ### Catalog 1. Itemized renderer capabilities: - - List of components that the agent can use to generate UI - - List of functions that can be invoked by renderer - - Styles and themes + - List of components that the agent can use to generate UI + - List of functions that can be invoked by renderer + - Styles and themes 2. Explanation on how the renderer capabilities should be used. It is observed that depending on use case, catalog components may be more or less specific to domain: - **Less specific**: - Basic UI primitives like buttons, labels, rows, columns, option-selectors and so on. + Basic UI primitives like buttons, labels, rows, columns, option-selectors and so on. - **More specific**: @@ -89,34 +90,31 @@ There are options for A2UI agent: Functionality of A2UI renderer consists of layers that can be developed separately and reused: - **Core Library**: - + Set of primitives, needed to describe catalog and to interact with the agent. For example, see the [JavaScript web core library](../renderers/web_core/README.md). - **Catalog Schema**: - + Definition of catalog in the form of JSON. For example, see the [basic catalog schema](../specification/v0_10/json/basic_catalog.json). - **Framework adapter**: - + Code that implements the execution of the agent’s instructions in a concrete framework. For example: - - JavaScript core and catalogs may be adapted to Angular, Electron, React and Lit frameworks. - Dart core and catalogs may be adapted to Flutter and Jaspr frameworks. See the [Angular adapter](../renderers/angular/README.md). - **Catalog Implementation**: - + Implementation of the catalog schema for a framework. For example: - - See the [Angular implementation of the basic catalog](../renderers/angular/src/v0_9/catalog/basic) - ```mermaid flowchart TD; @@ -173,6 +171,7 @@ See the [example in common types](../specification/v0_9/json/common_types.json#L ### Action A container for an interaction triggered by the user in the UI. Actions come in two types: + - **Event**: Dispatched to the agent for processing (e.g., clicking "Submit"). - **Function**: Executed locally on the renderer (e.g., opening a URL). diff --git a/docs/guides/a2ui-in-mcp-apps.md b/docs/guides/a2ui-in-mcp-apps.md index 982a3989a..8d940dbac 100644 --- a/docs/guides/a2ui-in-mcp-apps.md +++ b/docs/guides/a2ui-in-mcp-apps.md @@ -1,7 +1,6 @@ # A2UI Dynamic Rendering within MCP Applications -This guide shows you how to serve rich, interactive A2UI interfaces within [MCP Apps](https://modelcontextprotocol.io/extensions/apps/overview) using Tools and Embedded Resources. By the end, you'll have a working MCP server that returns an MCP App which can render A2UI components and handle A2UI interactions. By supporting native A2UI within MCP Apps, your MCP server can securely collaborate with remote agents while maintaining consistency over UI styling. - +This guide shows you how to serve rich, interactive A2UI interfaces within [MCP Apps](https://modelcontextprotocol.io/extensions/apps/overview) using Tools and Embedded Resources. By the end, you'll have a working MCP server that returns an MCP App which can render A2UI components and handle A2UI interactions. By supporting native A2UI within MCP Apps, your MCP server can securely collaborate with remote agents while maintaining consistency over UI styling. ## Prerequisites @@ -21,20 +20,20 @@ The system consists of three main actors interacting through a chain of communic 2. **MCP Application (Sandboxed)**: The untrusted third-party web application (e.g., a Lit or Angular micro-app) running inside a double-iframe sandbox. This app contains the A2UI surface. 3. **MCP Server**: The backend server providing the application resources and handling tool calls. - ## Deep Dive: The Communication Flow A key aspect of this pattern is that the **MCP App** renders the A2UI payloads directly, rather than relying on the Client Host Application to do so. ### Loading A2UI Components in MCP Apps + Here is the sequence of events for dynamically loading A2UI components into MCP Apps: 1. **Trigger**: The MCP App decides it needs to fetch or update UI content (e.g., on initialization or via a user-initiated Action). 2. **Request**: The MCP App sends a JSON-RPC request to the Host via `window.parent.postMessage`. - * *Example Method*: `ui/fetch_counter_a2ui` + - _Example Method_: `ui/fetch_counter_a2ui` 3. **Relay**: The Sandbox Proxy relays this message to the Client Host. 4. **MCP Call**: The Client Host translates this custom message into a standard MCP `tools/call` request to the MCP Server. - * *Example Tool*: `fetch_counter_a2ui` + - _Example Tool_: `fetch_counter_a2ui` 5. **Response**: The MCP Server executes the tool and returns a result containing an `application/json+a2ui` resource. 6. **Response forwarding**: The Host receives the tool result and forwards it back down through the Sandbox Proxy to the MCP App. 7. **Rendering**: The MCP App extracts the A2UI JSON payload from the resource and feeds it into its local A2UI `MessageProcessor`, which updates the A2UI surface dynamically. @@ -48,7 +47,7 @@ Interactivity within the rendered A2UI surface is handled by reversing the flow: 3. The MCP App captures this event via the A2UI `MessageProcessor.events` subscription. 4. The MCP App packages the action and sends it as a JSON-RPC message to the Host (e.g., `ui/increase_counter`). 5. The Host calls the corresponding tool on the MCP Server. -6. The Server returns a new A2UI payload (representing the updated state), which is piped back to the MCP App to update the rendering. +6. The Server returns a new A2UI payload (representing the updated state), which is piped back to the MCP App to update the rendering. ### Sequence Diagram @@ -106,7 +105,7 @@ MCP Apps are typically delivered as a single HTML resource from the MCP Server. 3. This produces a self-contained HTML file that can be safely loaded via `srcdoc` in the restricted iframe. !!! tip "Using Vite to inline " - If your project uses Vite (common for React, Vue, or Lit), you can achieve the same single-file output automatically using plugins like `vite-plugin-singlefile`. This eliminates the need for a custom post-build script by handling the inlining during the build process itself. +If your project uses Vite (common for React, Vue, or Lit), you can achieve the same single-file output automatically using plugins like `vite-plugin-singlefile`. This eliminates the need for a custom post-build script by handling the inlining during the build process itself. **How to use it:** 1. **Install the plugin**: @@ -117,7 +116,7 @@ MCP Apps are typically delivered as a single HTML resource from the MCP Server. ```typescript import { defineConfig } from 'vite' import { viteSingleFile } from 'vite-plugin-singlefile' - + export default defineConfig({ plugins: [viteSingleFile()], }) @@ -135,47 +134,48 @@ Your inlined app is now running in the sandbox. To leverage A2UI: **Example: Fetching and Rendering A2UI** - ```typescript // 1. Request A2UI data from Host -const result = await callHostMethod("ui/fetch_counter_a2ui"); +const result = await callHostMethod('ui/fetch_counter_a2ui'); // 2. Find and parse the A2UI resource -const a2uiResource = result.find(c => - c.type === 'resource' && c.resource?.mimeType === 'application/json+a2ui' +const a2uiResource = result.find( + c => c.type === 'resource' && c.resource?.mimeType === 'application/json+a2ui', ); if (a2uiResource?.resource?.text) { - const messages = JSON.parse(a2uiResource.resource.text); - this.processor.processMessages(messages); + const messages = JSON.parse(a2uiResource.resource.text); + this.processor.processMessages(messages); } - // Utility for JSON-RPC communication function callHostMethod(method: string, params: any = {}): Promise { - return new Promise((resolve, reject) => { - const requestId = `${method}-${Date.now()}`; - - const handler = (event: MessageEvent) => { - if (event.data.id !== requestId) return; - window.removeEventListener('message', handler); - - if (event.data.error) { - reject(event.data.error); - } else { - resolve(event.data.result); - } - }; - - window.addEventListener('message', handler); - - window.parent.postMessage({ - jsonrpc: "2.0", - id: requestId, - method, - params - }, "*"); // Note: Replace "*" with explicit target origin in production - }); + return new Promise((resolve, reject) => { + const requestId = `${method}-${Date.now()}`; + + const handler = (event: MessageEvent) => { + if (event.data.id !== requestId) return; + window.removeEventListener('message', handler); + + if (event.data.error) { + reject(event.data.error); + } else { + resolve(event.data.result); + } + }; + + window.addEventListener('message', handler); + + window.parent.postMessage( + { + jsonrpc: '2.0', + id: requestId, + method, + params, + }, + '*', + ); // Note: Replace "*" with explicit target origin in production + }); } ``` @@ -187,23 +187,23 @@ To handle interactivity within the rendered A2UI surface, your MCP App must capt ```typescript // Subscribing to A2UI events in the MCP App ([main.ts](https://github.com/google/A2UI/blob/main/samples/mcp/a2ui-in-mcpapps/server/apps/src/src/main.ts)) -this.processor.events.subscribe(async (event) => { +this.processor.events.subscribe(async event => { if (!event.message.userAction) return; - + const method = `ui/${event.message.userAction.name}`; const params = event.message.userAction.context; try { - // Translate A2UI UserAction to JSON-RPC, send to Host, and await response - const result = await callHostMethod(method, params); - - // Parse the updated A2UI payload and update the rendering - const messages = extractA2UIMessages(result); - if (messages) { - this.processor.processMessages(messages); - } + // Translate A2UI UserAction to JSON-RPC, send to Host, and await response + const result = await callHostMethod(method, params); + + // Parse the updated A2UI payload and update the rendering + const messages = extractA2UIMessages(result); + if (messages) { + this.processor.processMessages(messages); + } } catch (error) { - console.error(`Error handling user action[${method}]:`, error); + console.error(`Error handling user action[${method}]:`, error); } }); ``` @@ -212,5 +212,5 @@ This pattern enables the MCP App to serve as a dynamic interface for the MCP Ser ## Security Considerations -- **Explicit Target Origin**: Always use specific target origins (e.g., `'https://trusted-host.com'`) instead of `*` when calling `postMessage` if the host origin is known. This prevents malicious iframes from intercepting your RPC requests. -- **Null Origin Handling**: Remember that inside a strict sandbox (`sandbox="allow-scripts"` without `allow-same-origin`), `window.location.origin` will evaluate to `"null"`. You must validate incoming messages carefully by comparing `event.source` against the expected window object (e.g., `window.parent`). \ No newline at end of file +- **Explicit Target Origin**: Always use specific target origins (e.g., `'https://trusted-host.com'`) instead of `*` when calling `postMessage` if the host origin is known. This prevents malicious iframes from intercepting your RPC requests. +- **Null Origin Handling**: Remember that inside a strict sandbox (`sandbox="allow-scripts"` without `allow-same-origin`), `window.location.origin` will evaluate to `"null"`. You must validate incoming messages carefully by comparing `event.source` against the expected window object (e.g., `window.parent`). diff --git a/docs/guides/a2ui-with-any-agent-framework.md b/docs/guides/a2ui-with-any-agent-framework.md index 7ead3e064..a684f072e 100644 --- a/docs/guides/a2ui-with-any-agent-framework.md +++ b/docs/guides/a2ui-with-any-agent-framework.md @@ -34,11 +34,11 @@ Turn on A2UI in `CopilotRuntime` and inject the `render_a2ui` tool so your agent can produce A2UI surfaces: ```ts title="app/api/copilotkit/route.ts" -import { CopilotRuntime } from "@copilotkit/runtime"; +import {CopilotRuntime} from '@copilotkit/runtime'; const runtime = new CopilotRuntime({ - agents: { default: myAgent }, - a2ui: { injectA2UITool: true }, + agents: {default: myAgent}, + a2ui: {injectA2UITool: true}, }); ``` @@ -49,15 +49,17 @@ Scope to specific agents with `a2ui: { injectA2UITool: true, agents: ["my-agent" The A2UI renderer activates automatically. Optionally pass a theme: {% raw %} + ```tsx -import { CopilotKitProvider } from "@copilotkit/react-core/v2"; -import "@copilotkit/react-core/v2/styles.css"; -import { myCustomTheme } from "@copilotkit/a2ui-renderer"; +import {CopilotKitProvider} from '@copilotkit/react-core/v2'; +import '@copilotkit/react-core/v2/styles.css'; +import {myCustomTheme} from '@copilotkit/a2ui-renderer'; - + {children} - +; ``` + {% endraw %} ### Custom components (BYOC) @@ -82,22 +84,22 @@ gets injected into the agent's prompt so the LLM knows when to reach for each component; the schema validates the props the agent sends. ```ts title="lib/a2ui/definitions.ts" -import { z } from "zod"; +import {z} from 'zod'; export const myDefinitions = { StatusBadge: { - description: "A colored status badge.", + description: 'A colored status badge.', props: z.object({ text: z.string(), - variant: z.enum(["success", "warning", "error"]).optional(), + variant: z.enum(['success', 'warning', 'error']).optional(), }), }, Metric: { - description: "A key metric with label and value.", + description: 'A key metric with label and value.', props: z.object({ label: z.string(), value: z.string(), - trend: z.enum(["up", "down"]).optional(), + trend: z.enum(['up', 'down']).optional(), }), }, }; @@ -112,42 +114,52 @@ the definitions type, so the props your renderer receives are type-checked against the Zod schema — a typo in `props.text` is a compile error. {% raw %} + ```tsx title="lib/a2ui/renderers.tsx" -"use client"; +'use client'; -import { createCatalog, type CatalogRenderers } from "@copilotkit/a2ui-renderer"; -import { myDefinitions, type MyDefinitions } from "./definitions"; +import {createCatalog, type CatalogRenderers} from '@copilotkit/a2ui-renderer'; +import {myDefinitions, type MyDefinitions} from './definitions'; const myRenderers: CatalogRenderers = { - StatusBadge: ({ props }) => { + StatusBadge: ({props}) => { const colors = { - success: { bg: "#dcfce7", text: "#166534" }, - warning: { bg: "#fef3c7", text: "#92400e" }, - error: { bg: "#fee2e2", text: "#991b1b" }, + success: {bg: '#dcfce7', text: '#166534'}, + warning: {bg: '#fef3c7', text: '#92400e'}, + error: {bg: '#fee2e2', text: '#991b1b'}, }; - const c = colors[props.variant ?? "success"]; + const c = colors[props.variant ?? 'success']; return ( - + {props.text} ); }, - Metric: ({ props }) => ( + Metric: ({props}) => (
-
{props.label}
-
- {props.value} {props.trend === "up" ? "↑" : props.trend === "down" ? "↓" : ""} +
{props.label}
+
+ {props.value} {props.trend === 'up' ? '↑' : props.trend === 'down' ? '↓' : ''}
), }; export const myCatalog = createCatalog(myDefinitions, myRenderers, { - catalogId: "my-app-catalog", + catalogId: 'my-app-catalog', includeBasicCatalog: true, // merges with built-in components }); ``` + {% endraw %} `catalogId` is the stable handle the agent uses to target this catalog; @@ -157,21 +169,23 @@ alongside your own (omit it to render _only_ your components). #### 3. Pass the catalog to CopilotKit {% raw %} + ```tsx title="app/layout.tsx" -"use client"; +'use client'; -import { CopilotKitProvider } from "@copilotkit/react-core/v2"; -import "@copilotkit/react-core/v2/styles.css"; -import { myCatalog } from "@/lib/a2ui/renderers"; +import {CopilotKitProvider} from '@copilotkit/react-core/v2'; +import '@copilotkit/react-core/v2/styles.css'; +import {myCatalog} from '@/lib/a2ui/renderers'; -export default function Layout({ children }: { children: React.ReactNode }) { +export default function Layout({children}: {children: React.ReactNode}) { return ( - + {children} ); } ``` + {% endraw %} Agents will now see your custom components alongside the built-ins and diff --git a/docs/guides/a2ui_over_mcp.md b/docs/guides/a2ui_over_mcp.md index 4f6235b2e..eb26be451 100644 --- a/docs/guides/a2ui_over_mcp.md +++ b/docs/guides/a2ui_over_mcp.md @@ -44,6 +44,7 @@ In the Inspector: > NOTE: Note > > The sample uses a local path reference to the A2UI Agent SDK. For your own projects, install from PyPI: +> > ```bash > pip install a2ui-agent-sdk > ``` @@ -90,9 +91,7 @@ MCP is a stateful session protocol, so the most efficient approach is to declare "a2ui": { "clientCapabilities": { "v0.9": { - "supportedCatalogIds": [ - "https://a2ui.org/specification/v0_9/basic_catalog.json" - ] + "supportedCatalogIds": ["https://a2ui.org/specification/v0_9/basic_catalog.json"] } } } @@ -114,14 +113,12 @@ If your server must remain stateless, the client can pass A2UI capabilities in t "id": "id-123", "params": { "name": "generate_report", - "arguments": { "date": "2026-03-01" }, + "arguments": {"date": "2026-03-01"}, "_meta": { "a2ui": { "clientCapabilities": { "v0.9": { - "supportedCatalogIds": [ - "https://a2ui.org/specification/v0_9/basic_catalog.json" - ], + "supportedCatalogIds": ["https://a2ui.org/specification/v0_9/basic_catalog.json"], "inlineCatalogs": [] } } @@ -312,10 +309,10 @@ a2ui_resource = types.EmbeddedResource( ) ``` -| Audience | Behavior | -|----------|----------| -| *(empty)* | Visible to both user and LLM | -| `["user"]` | Rendered for the user; hidden from LLM context | +| Audience | Behavior | +| --------------- | ------------------------------------------------------ | +| _(empty)_ | Visible to both user and LLM | +| `["user"]` | Rendered for the user; hidden from LLM context | | `["assistant"]` | Available to LLM for follow-up reasoning; not rendered | ## Using the A2UI Agent SDK diff --git a/docs/guides/agent-development.md b/docs/guides/agent-development.md index 3fc9a91ac..b3c30b6fa 100644 --- a/docs/guides/agent-development.md +++ b/docs/guides/agent-development.md @@ -82,7 +82,7 @@ root_agent = Agent( ) ``` -Don't forget to set the `GOOGLE_API_KEY` environment variable to run this example. +Don't forget to set the `GOOGLE_API_KEY` environment variable to run this example. ```bash echo 'GOOGLE_API_KEY="YOUR_API_KEY"' > .env @@ -94,7 +94,7 @@ You can test out this agent with the ADK web interface: adk web ``` -Select `my_agent` from the list, and ask questions about restaurants in New York. You should see a list of restaurants in the UI as plain text. +Select `my_agent` from the list, and ask questions about restaurants in New York. You should see a list of restaurants in the UI as plain text. ## Generating A2UI Messages @@ -162,8 +162,8 @@ Your agent will no longer strictly output text. Instead, it will output text and The `A2UI_SCHEMA` that we imported is a standard JSON schema that defines valid operations like: -* `render` (displaying a UI) -* `update` (changing data in an existing UI) +- `render` (displaying a UI) +- `update` (changing data in an existing UI) Because the output is structured JSON, you may parse and validate it before sending it to the client. @@ -183,4 +183,4 @@ jsonschema.validate( By validating the output against `A2UI_SCHEMA`, you ensure that your client never receives malformed UI instructions. -TODO: Continue this guide with examples of how to parse, validate, and send the output to the client renderer without the A2A extension. +TODO: Continue this guide with examples of how to parse, validate, and send the output to the client renderer without the A2A extension. diff --git a/docs/guides/authoring-components.md b/docs/guides/authoring-components.md index 366ff5f37..f52cd4db7 100644 --- a/docs/guides/authoring-components.md +++ b/docs/guides/authoring-components.md @@ -111,10 +111,10 @@ Implement your component using your client-side framework. For Angular, your com In the [`rizzcharts`](../../samples/client/angular/projects/rizzcharts/README.md) example, the `Chart` component is defined in [`chart.ts`](../../samples/client/angular/projects/rizzcharts/src/a2ui-catalog/chart.ts). ```typescript -import { DynamicComponent } from '@a2ui/angular'; +import {DynamicComponent} from '@a2ui/angular'; import * as Primitives from '@a2ui/web_core/types/primitives'; import * as Types from '@a2ui/web_core/types/types'; -import { Component, computed, input, Signal, signal } from '@angular/core'; +import {Component, computed, input, Signal, signal} from '@angular/core'; @Component({ selector: 'a2ui-chart', @@ -138,6 +138,7 @@ export class Chart extends DynamicComponent { ``` Keep these key points in mind when implementing components: + - **Extend `DynamicComponent`**: This gives you access to `resolvePrimitive` for data binding resolution. - **Use Angular Inputs**: Map properties from the schema to Angular inputs. @@ -150,14 +151,14 @@ Once the component is implemented, register it in your client catalog. This maps In the [`rizzcharts`](../../samples/agent/adk/rizzcharts/python/README.md) example, this is done in [`catalog.ts`](../../samples/client/angular/projects/rizzcharts/src/a2ui-catalog/catalog.ts). ```typescript -import { Catalog, DEFAULT_CATALOG } from '@a2ui/angular'; -import { inputBinding } from '@angular/core'; +import {Catalog, DEFAULT_CATALOG} from '@a2ui/angular'; +import {inputBinding} from '@angular/core'; export const RIZZ_CHARTS_CATALOG = { ...DEFAULT_CATALOG, Chart: { - type: () => import('./chart').then((r) => r.Chart), - bindings: ({ properties }) => [ + type: () => import('./chart').then(r => r.Chart), + bindings: ({properties}) => [ inputBinding('type', () => ('type' in properties && properties['type']) || undefined), inputBinding('title', () => ('title' in properties && properties['title']) || undefined), inputBinding( @@ -170,6 +171,7 @@ export const RIZZ_CHARTS_CATALOG = { ``` Key points for registration: + - **Lazy Loading**: Use `import()` to lazy-load the component code. - **Input Bindings**: Use `inputBinding` to map properties from the schema to Angular inputs. @@ -240,4 +242,3 @@ from a2ui.adk.send_a2ui_to_client_toolset import ( config = A2aAgentExecutorConfig(event_converter=A2uiEventConverter()) ``` - diff --git a/docs/guides/client-setup.md b/docs/guides/client-setup.md index 4e6df3286..7302ef2e3 100644 --- a/docs/guides/client-setup.md +++ b/docs/guides/client-setup.md @@ -4,19 +4,19 @@ Integrate A2UI into your application using the renderer for your platform. ## Renderers -| Renderer | Platform | v0.8 | v0.9 | Status | -| ------------------------ | ------------------ | ---- | ---- | ----------------- | -| **[React](https://github.com/google/A2UI/tree/main/renderers/react)** | Web | ✅ | ✅ | ✅ Stable | -| **[Lit (Web Components)](https://github.com/google/A2UI/tree/main/renderers/lit)** | Web | ✅ | ✅ | ✅ Stable | -| **[Angular](https://github.com/google/A2UI/tree/main/renderers/angular)** | Web | ✅ | ✅ | ✅ Stable | -| **[Flutter (GenUI SDK)](https://docs.flutter.dev/ai/genui)** | Mobile/Desktop/Web | ✅ | ✅ | ✅ Stable | -| **Jetpack Compose** | Android | — | — | 🚧 Planned Q2 2026 | +| Renderer | Platform | v0.8 | v0.9 | Status | +| ---------------------------------------------------------------------------------- | ------------------ | ---- | ---- | ------------------ | +| **[React](https://github.com/google/A2UI/tree/main/renderers/react)** | Web | ✅ | ✅ | ✅ Stable | +| **[Lit (Web Components)](https://github.com/google/A2UI/tree/main/renderers/lit)** | Web | ✅ | ✅ | ✅ Stable | +| **[Angular](https://github.com/google/A2UI/tree/main/renderers/angular)** | Web | ✅ | ✅ | ✅ Stable | +| **[Flutter (GenUI SDK)](https://docs.flutter.dev/ai/genui)** | Mobile/Desktop/Web | ✅ | ✅ | ✅ Stable | +| **Jetpack Compose** | Android | — | — | 🚧 Planned Q2 2026 | For more see all [A2UI Renderers](../reference/renderers.md) and [Community A2UI Renderers](../ecosystem/renderers.md). ## Component Catalogs -A component catalog is any collection of components. A2UI provides a "Basic Catalog" but we expect you will add your own components, or shared libraries or fully replace the basic components with your own. +A component catalog is any collection of components. A2UI provides a "Basic Catalog" but we expect you will add your own components, or shared libraries or fully replace the basic components with your own. **Your design system is what matters.** You can register any collection of components and functions, and A2UI will work with them. The catalog is just the contract between your agent and your renderer. @@ -32,7 +32,6 @@ The shared `web_core` library provides: - **Message Processor**: Manages A2UI state and processes incoming messages. - ## Web Components (Lit) ```bash @@ -64,12 +63,8 @@ Once installed, you can use the renderer in your app. The Angular renderer provi A2UI uses versioned imports for its protocol-specific implementations. For v0.9, configure your application providers as follows: ```typescript -import { ApplicationConfig } from '@angular/core'; -import { - A2UI_RENDERER_CONFIG, - A2uiRendererService, - minimalCatalog -} from '@a2ui/angular/v0_9'; +import {ApplicationConfig} from '@angular/core'; +import {A2UI_RENDERER_CONFIG, A2uiRendererService, minimalCatalog} from '@a2ui/angular/v0_9'; export const appConfig: ApplicationConfig = { providers: [ @@ -77,13 +72,13 @@ export const appConfig: ApplicationConfig = { provide: A2UI_RENDERER_CONFIG, useValue: { catalogs: [minimalCatalog], - actionHandler: (action) => { + actionHandler: action => { console.log('Action dispatched:', action); - } - } + }, + }, }, - A2uiRendererService - ] + A2uiRendererService, + ], }; ``` diff --git a/docs/guides/defining-your-own-catalog.md b/docs/guides/defining-your-own-catalog.md index d561c3fff..cb70fe2fb 100644 --- a/docs/guides/defining-your-own-catalog.md +++ b/docs/guides/defining-your-own-catalog.md @@ -9,9 +9,10 @@ By defining your own catalog, you restrict the agent to using exactly the compon Every A2UI surface is driven by a **Catalog**. A catalog is simply a JSON Schema file that tells the agent which components, functions, and themes are available for it to use. Defining your own catalog offers the following benefits: -- **Design System Alignment**: Restrict the agent to using exactly the components and visual language that exist in your application. -- **Security and Type Safety**: You register entire catalogs with your client application, ensuring that only trusted components are rendered. -- **No Mappers Needed**: It is recommended to build catalogs that directly reflect your client's design system rather than trying to map a generic catalog (like the Basic Catalog) to it through an adapter. + +- **Design System Alignment**: Restrict the agent to using exactly the components and visual language that exist in your application. +- **Security and Type Safety**: You register entire catalogs with your client application, ensuring that only trusted components are rendered. +- **No Mappers Needed**: It is recommended to build catalogs that directly reflect your client's design system rather than trying to map a generic catalog (like the Basic Catalog) to it through an adapter. The Basic Catalog is just one example and is intentionally sparse to remain easily implementable by different renderers. @@ -31,10 +32,11 @@ It is recommended to create catalogs that directly map to your existing componen To implement your own catalog on the web: - Create a JSON Schema containing your component definitions. + - Create your own `Component` objects and `Catalog` object within your chosen web renderer. - - Provide the schema or reference ID to the agent. + - Provide the schema or reference ID to the agent. - *Detailed guides for each framework coming soon.* + _Detailed guides for each framework coming soon._ === "Flutter" @@ -54,6 +56,6 @@ When defining and registering catalogs: ## Next Steps -- **[Theming & Styling](theming.md)**: Customize the look and feel of components. -- **[Component Reference](../reference/components.md)**: Explore standard types that might be available for reuse. -- **[Agent Development](agent-development.md)**: Build agents that interact with your Catalog. +- **[Theming & Styling](theming.md)**: Customize the look and feel of components. +- **[Component Reference](../reference/components.md)**: Explore standard types that might be available for reuse. +- **[Agent Development](agent-development.md)**: Build agents that interact with your Catalog. diff --git a/docs/guides/mcp-apps-in-a2ui.md b/docs/guides/mcp-apps-in-a2ui.md index b24071562..388fd97a6 100644 --- a/docs/guides/mcp-apps-in-a2ui.md +++ b/docs/guides/mcp-apps-in-a2ui.md @@ -23,11 +23,11 @@ To prevent this, A2UI strictly excludes `allow-same-origin` for the inner iframe ### The Architecture 1. **[Sandbox Proxy (`sandbox.html`)](https://github.com/google/A2UI/blob/main/samples/client/shared/mcp_apps_inner_iframe/sandbox.html)**: An intermediate `iframe` served from the same origin. It isolates raw DOM injection from the main app while maintaining a structured JSON-RPC channel. - - Permissions: **Do not sandbox** in the host template (e.g., [`mcp-app.ts`](https://github.com/google/A2UI/blob/main/samples/client/angular/projects/mcp_calculator/src/a2ui-catalog/mcp-app.ts) or [`mcp-apps-component.ts`](https://github.com/google/A2UI/blob/main/samples/client/lit/custom-components-example/ui/custom-components/mcp-apps-component.ts)). - - Host origin validation: Validates that messages come from the expected host origin. + - Permissions: **Do not sandbox** in the host template (e.g., [`mcp-app.ts`](https://github.com/google/A2UI/blob/main/samples/client/angular/projects/mcp_calculator/src/a2ui-catalog/mcp-app.ts) or [`mcp-apps-component.ts`](https://github.com/google/A2UI/blob/main/samples/client/lit/custom-components-example/ui/custom-components/mcp-apps-component.ts)). + - Host origin validation: Validates that messages come from the expected host origin. 2. **Embedded App (Inner Iframe)**: The innermost `iframe`. Injected dynamically via `srcdoc` with restricted permissions. - - Permissions: `sandbox="allow-scripts allow-forms allow-popups allow-modals"` (**MUST NOT** include `allow-same-origin`). - - Isolation: Removes access to `localStorage`, `sessionStorage`, `IndexedDB`, and cookies due to unique origin. + - Permissions: `sandbox="allow-scripts allow-forms allow-popups allow-modals"` (**MUST NOT** include `allow-same-origin`). + - Isolation: Removes access to `localStorage`, `sessionStorage`, `IndexedDB`, and cookies due to unique origin. ### Architecture Diagram @@ -53,13 +53,13 @@ The MCP Apps component typically resolves to a `custom` node in the A2UI catalog You must register the component in your catalog application. For example, in Angular: ```typescript -import { Catalog } from '@a2ui/angular'; -import { inputBinding } from '@angular/core'; +import {Catalog} from '@a2ui/angular'; +import {inputBinding} from '@angular/core'; export const DEMO_CATALOG = { McpApp: { - type: () => import('./mcp-app').then((r) => r.McpApp), - bindings: ({ properties }) => [ + type: () => import('./mcp-app').then(r => r.McpApp), + bindings: ({properties}) => [ inputBinding( 'content', () => ('content' in properties && properties['content']) || undefined, @@ -102,24 +102,25 @@ If the content is complex or requires encoding, you can pass a URL-encoded strin Communication between the Host and the embedded inner iframe is facilitated via a structured JSON-RPC channel over `postMessage`. -- **Events**: The Host Component listens for a `SANDBOX_PROXY_READY_METHOD` message from the proxy. -- **Bridging**: An `AppBridge` handles message relaying. Developers (specifically the MCP App Developer inside the untrusted iframe) can call tools on the MCP server using `bridge.callTool()`. -- **The Host**: Resolves callbacks (e.g., specific resizing, Tool results). +- **Events**: The Host Component listens for a `SANDBOX_PROXY_READY_METHOD` message from the proxy. +- **Bridging**: An `AppBridge` handles message relaying. Developers (specifically the MCP App Developer inside the untrusted iframe) can call tools on the MCP server using `bridge.callTool()`. +- **The Host**: Resolves callbacks (e.g., specific resizing, Tool results). ### Limitations Because `allow-same-origin` is strictly omitted for the innermost iframe, the following conditions apply: -- The MCP app **cannot** use `localStorage`, `sessionStorage`, `IndexedDB`, or cookies. Each application runs with a unique origin. -- Direct DOM manipulation by the parent is blocked. All interactions must proceed via message passing. + +- The MCP app **cannot** use `localStorage`, `sessionStorage`, `IndexedDB`, or cookies. Each application runs with a unique origin. +- Direct DOM manipulation by the parent is blocked. All interactions must proceed via message passing. ## Prerequisites To run the samples, ensure you have the following installed: -- **Python 3.10+** — Required for the agent and MCP server backends -- **[uv](https://docs.astral.sh/uv/)** — Fast Python package manager (used to run all Python samples) -- **Node.js 18+** and **npm** — Required for building and running the client apps -- **A `GEMINI_API_KEY`** — Required by all ADK-based agents. Get one from [Google AI Studio](https://aistudio.google.com/apikey) +- **Python 3.10+** — Required for the agent and MCP server backends +- **[uv](https://docs.astral.sh/uv/)** — Fast Python package manager (used to run all Python samples) +- **Node.js 18+** and **npm** — Required for building and running the client apps +- **A `GEMINI_API_KEY`** — Required by all ADK-based agents. Get one from [Google AI Studio](https://aistudio.google.com/apikey) > ⚠️ **Environment variable setup**: You can either export `GEMINI_API_KEY` in your shell or create a `.env` file in each agent directory. The agents use `dotenv` to load `.env` files automatically. > @@ -141,13 +142,13 @@ There are two primary samples demonstrating MCP Apps integration. Each sample re This sample verifies the sandbox with a Lit-based client and an ADK-based A2A agent. -- **A2A Agent Server**: - - Path: [`samples/agent/adk/mcp-apps-in-a2ui-sample/`](https://github.com/google/A2UI/tree/main/samples/agent/adk/mcp-apps-in-a2ui-sample/) - - Command: `uv run .` (requires `GEMINI_API_KEY` in `.env`) -- **Lit Client App**: - - Path: [`samples/client/lit/mcp-apps-in-a2ui-sample/`](https://github.com/google/A2UI/tree/main/samples/client/lit/mcp-apps-in-a2ui-sample/) - - Command: `npm install && npm run dev` (requires building the Lit renderer first) - - URL: `http://localhost:5173/` +- **A2A Agent Server**: + - Path: [`samples/agent/adk/mcp-apps-in-a2ui-sample/`](https://github.com/google/A2UI/tree/main/samples/agent/adk/mcp-apps-in-a2ui-sample/) + - Command: `uv run .` (requires `GEMINI_API_KEY` in `.env`) +- **Lit Client App**: + - Path: [`samples/client/lit/mcp-apps-in-a2ui-sample/`](https://github.com/google/A2UI/tree/main/samples/client/lit/mcp-apps-in-a2ui-sample/) + - Command: `npm install && npm run dev` (requires building the Lit renderer first) + - URL: `http://localhost:5173/` **What to expect**: A simple interface loading the MCP App, with a button to trigger an action handled by the agent. @@ -171,7 +172,9 @@ This sample verifies the sandbox with an Angular-based client, an MCP Proxy Agen cd samples/mcp/mcp-apps-calculator/ uv run . ``` + ======= + ```bash cd samples/client/lit/mcp-apps-in-a2ui-sample npm install @@ -209,7 +212,8 @@ This sample verifies the sandbox with an Angular-based client, an MCP Proxy Agen cd samples/mcp/mcp-apps-calculator/ uv run . ``` ->>>>>>> e3c17f1f (docs: add npm install step to MCP guide) + +> > > > > > > e3c17f1f (docs: add npm install step to MCP guide) The MCP server starts on `http://localhost:8000` using SSE transport. @@ -256,8 +260,8 @@ http://localhost:4200/?disable_security_self_test=true **What to expect**: A set of smart chips to load calculator app or pong app will be rendered. Both apps run in their own sandboxed iframes. -| Calculator App | Pong App | -| :---: | :---: | +| Calculator App | Pong App | +| :------------------------------------------------------------------------------------------------------------: | :-----------------------------------------------------------------------: | | ![An animated GIF of the calculator app being used to perform multiplications.](../assets/calculator_demo.gif) | ![An animated GIF of the pong app being played.](../assets/pong_demo.gif) | --- @@ -271,19 +275,20 @@ For testing purposes, you can opt-out of the security self-test by using specifi This query parameter allows you to bypass the security self-test that verifies iframe isolation. This is useful for debugging and testing environments where the double-iframe setup may not pass strict origin checks (e.g., `localhost` development). Example usage: + ``` http://localhost:4200/?disable_security_self_test=true ``` ## Troubleshooting -| Problem | Solution | -|---------|----------| -| `GEMINI_API_KEY environment variable not set` | Export the key or add a `.env` file in the agent directory | -| Python version error on `contact_lookup` agent | Install Python 3.13+ (required by that sample's `pyproject.toml`) | -| `npm run build:renderer` fails | Make sure you ran `npm install` first in `samples/client/lit/` | -| Angular client shows blank page | Ensure you ran `npm run build:sandbox` before `npm start` | -| MCP app iframe doesn't load | Check that both the MCP server (port 8000) and proxy agent (port 10006) are running | -| `ng serve` not found | Run `npm install --include=dev` to install dev dependencies including `@angular/cli` | -| "URL with hostname not allowed" | Angular 21 restricts allowed hosts. Use `localhost` (the default) — do not pass `--host 0.0.0.0` | -| Security self-test fails in dev | Add `?disable_security_self_test=true` to the URL | +| Problem | Solution | +| ---------------------------------------------- | ------------------------------------------------------------------------------------------------ | +| `GEMINI_API_KEY environment variable not set` | Export the key or add a `.env` file in the agent directory | +| Python version error on `contact_lookup` agent | Install Python 3.13+ (required by that sample's `pyproject.toml`) | +| `npm run build:renderer` fails | Make sure you ran `npm install` first in `samples/client/lit/` | +| Angular client shows blank page | Ensure you ran `npm run build:sandbox` before `npm start` | +| MCP app iframe doesn't load | Check that both the MCP server (port 8000) and proxy agent (port 10006) are running | +| `ng serve` not found | Run `npm install --include=dev` to install dev dependencies including `@angular/cli` | +| "URL with hostname not allowed" | Angular 21 restricts allowed hosts. Use `localhost` (the default) — do not pass `--host 0.0.0.0` | +| Security self-test fails in dev | Add `?disable_security_self_test=true` to the URL | diff --git a/docs/guides/renderer-development.md b/docs/guides/renderer-development.md index 6086fdc60..6e703cab0 100644 --- a/docs/guides/renderer-development.md +++ b/docs/guides/renderer-development.md @@ -12,14 +12,14 @@ If you're building a renderer for the web (React, Vue, Svelte, etc.), you don't ### What `web_core` provides -| Module | What it does | -|--------|-------------| -| **`MessageProcessor`** | Processes the A2UI JSONL stream, dispatches messages, manages surface lifecycle | -| **`SurfaceModel` / `SurfaceGroupModel`** | State management for surfaces, components, and data models | -| **`DataModel` / `DataContext`** | Data binding resolution, path-based lookups, template list rendering | -| **`ComponentModel`** | Component tree state, adjacency list → tree resolution | -| **Types & Schemas** | TypeScript types for all A2UI components, primitives, colors, styles, and JSON schema validation | -| **Expression parser** | Client-side function evaluation (v0.9) | +| Module | What it does | +| ---------------------------------------- | ------------------------------------------------------------------------------------------------ | +| **`MessageProcessor`** | Processes the A2UI JSONL stream, dispatches messages, manages surface lifecycle | +| **`SurfaceModel` / `SurfaceGroupModel`** | State management for surfaces, components, and data models | +| **`DataModel` / `DataContext`** | Data binding resolution, path-based lookups, template list rendering | +| **`ComponentModel`** | Component tree state, adjacency list → tree resolution | +| **Types & Schemas** | TypeScript types for all A2UI components, primitives, colors, styles, and JSON schema validation | +| **Expression parser** | Client-side function evaluation (v0.9) | ### How the maintained renderers use it @@ -31,11 +31,11 @@ import type * as Types from '@a2ui/web_core/types/types'; import type * as Primitives from '@a2ui/web_core/types/primitives'; // v0.8: Message processing and state -import { A2uiMessageProcessor } from '@a2ui/web_core/data/model-processor'; +import {A2uiMessageProcessor} from '@a2ui/web_core/data/model-processor'; // v0.9: Message processing, surfaces, catalogs -import { MessageProcessor } from '@a2ui/web_core/v0_9'; -import { SurfaceModel } from '@a2ui/web_core/v0_9'; +import {MessageProcessor} from '@a2ui/web_core/v0_9'; +import {SurfaceModel} from '@a2ui/web_core/v0_9'; // Styles and layout helpers import * as Styles from '@a2ui/web_core/styles/index'; diff --git a/docs/guides/theming.md b/docs/guides/theming.md index dd6b97795..33a5873bc 100644 --- a/docs/guides/theming.md +++ b/docs/guides/theming.md @@ -6,8 +6,8 @@ Customize the look and feel of A2UI components to match your brand. A2UI follows a **renderer-controlled styling** approach by default, but allows for flexibility through catalogs: -- **Agents describe *what* to show** (components and structure) -- **Renderers decide *how* it looks** (colors, fonts, spacing) +- **Agents describe _what_ to show** (components and structure) +- **Renderers decide _how_ it looks** (colors, fonts, spacing) However, the protocol is flexible enough to allow agents to influence styling when needed. @@ -29,7 +29,7 @@ flowchart TD ### Semantic hints -Agents provide semantic hints (not visual styles) to guide rendering. In the *basic catalog*: +Agents provide semantic hints (not visual styles) to guide rendering. In the _basic catalog_: ```json { @@ -56,21 +56,21 @@ The A2UI protocol allows for an arbitrary `theme` property in the `createSurface defined as `z.any().optional()` in the Zod schema, meaning the agent can pass any JSON structure that the client renderer and catalog understand. -* See the schema definition in [server-to-client.ts](../../renderers/web_core/src/v0_9/schema/server-to-client.ts). -* See the `Catalog` class and `themeSchema` in [catalog/types.ts](../../renderers/web_core/src/v0_9/catalog/types.ts). +- See the schema definition in [server-to-client.ts](../../renderers/web_core/src/v0_9/schema/server-to-client.ts). +- See the `Catalog` class and `themeSchema` in [catalog/types.ts](../../renderers/web_core/src/v0_9/catalog/types.ts). -**Note:** The *basic catalog* components are not wired to use the `theme` coming from the agent. +**Note:** The _basic catalog_ components are not wired to use the `theme` coming from the agent. _Want to influence this design? Chime in here: [#1118](https://github.com/google/A2UI/issues/1118)._ ## Catalog theming Theming is a responsibility of the catalog implementation. Each catalog can provide whatever theming solution it wants. -As an example, this is how the default *basic catalog* does it: +As an example, this is how the default _basic catalog_ does it: ### The Web Basic Catalog Theming -On the web, the *basic catalog* provided by the default A2UI renderers is themed by overriding CSS variables. +On the web, the _basic catalog_ provided by the default A2UI renderers is themed by overriding CSS variables. Basic catalog components inject a small stylesheet with default values for these variables. The stylesheet targets `:where(:root)` so their specificity is minimal, and the host app can override them easily. @@ -93,7 +93,7 @@ See the default styles in [default.ts](../../renderers/web_core/src/v0_9/basic_c ### Per-component overrides -Beyond global theming, each component of the *basic catalog* exposes custom variables to further refine its appearance. +Beyond global theming, each component of the _basic catalog_ exposes custom variables to further refine its appearance. For example, the `Card` component exposes a `--a2ui-card-background` variable. Check the documentation of each component to see what variables it exposes. @@ -109,7 +109,7 @@ To always force dark or light mode (or to programmatically control switching), u ### Custom Fonts -Fonts can be loaded as in any other web application. The *basic catalog* components attempt to inherit the font family +Fonts can be loaded as in any other web application. The _basic catalog_ components attempt to inherit the font family of their container, but offer two overridable values: `--a2ui-font-family-title` and `--a2ui-font-family-monospace` to set a different font for headings and monospace text blocks. @@ -117,7 +117,7 @@ set a different font for headings and monospace text blocks. Flutter has built-in theming support. See: -* [Use themes to share colors and font styles](https://docs.flutter.dev/cookbook/design/themes) from the Flutter docs. +- [Use themes to share colors and font styles](https://docs.flutter.dev/cookbook/design/themes) from the Flutter docs. ## Best Practices diff --git a/docs/index.md b/docs/index.md index 79197fbac..4f62102b2 100644 --- a/docs/index.md +++ b/docs/index.md @@ -22,10 +22,10 @@ A2UI enables AI agents to generate rich, interactive user interfaces that render ## Specification Versions -| Version | Status | Description | -|---------|--------|-------------| -| **[v0.8](specification/v0.8-a2ui.md)** | **Stable** | Current production release. Surfaces, components, data binding, adjacency list model. | -| **[v0.9](specification/v0.9-a2ui.md)** | **Draft** | Adds `createSurface`, client-side functions, custom catalogs, and the extension specification. [Evolution guide →](specification/v0.9-evolution-guide.md) | +| Version | Status | Description | +| -------------------------------------- | ---------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **[v0.8](specification/v0.8-a2ui.md)** | **Stable** | Current production release. Surfaces, components, data binding, adjacency list model. | +| **[v0.9](specification/v0.9-a2ui.md)** | **Draft** | Adds `createSurface`, client-side functions, custom catalogs, and the extension specification. [Evolution guide →](specification/v0.9-evolution-guide.md) | A2UI is Apache 2.0 licensed, created by Google with contributions from CopilotKit and the open source community, @@ -36,6 +36,7 @@ A2UI solves the following problem: **how can AI agents safely send rich UIs acro Instead of text-only responses or risky code execution, A2UI lets agents send **declarative component descriptions** that clients render using their own native widgets. It's like having agents speak a universal UI language. This repository contains: + - **[A2UI specifications](specification/v0.8-a2ui.md)** (v0.8 stable, v0.9 draft). - **Implementations for [renderers](reference/renderers.md)** (Angular, Flutter, Lit, Markdown, etc.) on the client side. - **Transports like [A2A](concepts/transports.md)** which communicate A2UI messages between agents and clients. @@ -44,27 +45,27 @@ This repository contains: - :material-shield-check: **Secure by Design** - --- + *** - Declarative data format, not executable code. Agents can only use pre-approved components from your catalog—no UI injection attacks. + Declarative data format, not executable code. Agents can only use pre-approved components from your catalog—no UI injection attacks. - :material-rocket-launch: **LLM-Friendly** - --- + *** - Flat, streaming JSON structure designed for easy generation. LLMs can build UIs incrementally without perfect JSON in one shot. + Flat, streaming JSON structure designed for easy generation. LLMs can build UIs incrementally without perfect JSON in one shot. - :material-devices: **Framework-Agnostic** - --- + *** - One agent response works everywhere. Render the same UI on Angular, Flutter, React, or native mobile with your own styled components. + One agent response works everywhere. Render the same UI on Angular, Flutter, React, or native mobile with your own styled components. - :material-chart-timeline: **Progressive Rendering** - --- + *** - Stream UI updates as they're generated. Users see the interface building in real-time instead of waiting for complete responses. + Stream UI updates as they're generated. Users see the interface building in real-time instead of waiting for complete responses.
@@ -74,59 +75,59 @@ This repository contains: - :material-clock-fast:{ .lg .middle } **[Quickstart Restaurant Finder Demo](quickstart.md)** - --- + *** - Run the full-stack demo locally with a Gemini powered ADK agent and Lit renderer. Learn A2UI end-to-end and customize to your use case. + Run the full-stack demo locally with a Gemini powered ADK agent and Lit renderer. Learn A2UI end-to-end and customize to your use case. - [:octicons-arrow-right-24: Run the demo](quickstart.md) + [:octicons-arrow-right-24: Run the demo](quickstart.md) - :material-react:{ .lg .middle } **[A2UI + AG-UI (React)](guides/a2ui-with-any-agent-framework.md)** - --- + *** - Scaffold a Next.js app wired to any agent framework via AG-UI. This is a React + A2UI app, ready to ship. + Scaffold a Next.js app wired to any agent framework via AG-UI. This is a React + A2UI app, ready to ship. - [:octicons-arrow-right-24: Use with any agent](guides/a2ui-with-any-agent-framework.md) + [:octicons-arrow-right-24: Use with any agent](guides/a2ui-with-any-agent-framework.md) - :material-palette-outline:{ .lg .middle } **[A2UI Composer](https://a2ui-composer.ag-ui.com/)** - --- + *** - Generate A2UI JSON from a visual editor — no install required. Paste the output into any agent prompt. + Generate A2UI JSON from a visual editor — no install required. Paste the output into any agent prompt. - [:octicons-arrow-right-24: Open the composer](https://a2ui-composer.ag-ui.com/) + [:octicons-arrow-right-24: Open the composer](https://a2ui-composer.ag-ui.com/) - :material-play-circle-outline:{ .lg .middle } **[A2UI Theater](https://a2ui-composer.ag-ui.com/theater)** - --- + *** - Step through pre-built A2UI streaming scenarios across Lit, React, and Angular renderers. See the protocol in motion before writing code. + Step through pre-built A2UI streaming scenarios across Lit, React, and Angular renderers. See the protocol in motion before writing code. - [:octicons-arrow-right-24: Open the playground](https://a2ui-composer.ag-ui.com/theater) + [:octicons-arrow-right-24: Open the playground](https://a2ui-composer.ag-ui.com/theater) - :material-book-open-variant:{ .lg .middle } **[Core Concepts](concepts/overview.md)** - --- + *** - Understand surfaces, components, data binding, and the adjacency list model. + Understand surfaces, components, data binding, and the adjacency list model. - [:octicons-arrow-right-24: Learn concepts](concepts/overview.md) + [:octicons-arrow-right-24: Learn concepts](concepts/overview.md) - :material-code-braces:{ .lg .middle } **[Developer Guides](guides/client-setup.md)** - --- + *** - Integrate A2UI renderers into your app or build agents that generate UIs. + Integrate A2UI renderers into your app or build agents that generate UIs. - [:octicons-arrow-right-24: Start building](guides/client-setup.md) + [:octicons-arrow-right-24: Start building](guides/client-setup.md) - :material-file-document:{ .lg .middle } **Protocol Specifications** - --- + *** - Dive into the complete technical specs: [v0.8 (stable)](specification/v0.8-a2ui.md) · [v0.9 (draft)](specification/v0.9-a2ui.md) + Dive into the complete technical specs: [v0.8 (stable)](specification/v0.8-a2ui.md) · [v0.9 (draft)](specification/v0.9-a2ui.md) - [:octicons-arrow-right-24: Read the v0.8 spec](specification/v0.8-a2ui.md) + [:octicons-arrow-right-24: Read the v0.8 spec](specification/v0.8-a2ui.md)
@@ -178,4 +179,3 @@ The typical interaction flow consists of these steps: CopilotKit has a public [A2UI Widget Builder](https://go.copilotkit.ai/A2UI-widget-builder) to try out as well. [![A2UI Composer](assets/A2UI-widget-builder.png)](https://go.copilotkit.ai/A2UI-widget-builder) - diff --git a/docs/introduction/agent-ui-ecosystem.md b/docs/introduction/agent-ui-ecosystem.md index 9f2469cf0..ab7dc9ae4 100644 --- a/docs/introduction/agent-ui-ecosystem.md +++ b/docs/introduction/agent-ui-ecosystem.md @@ -4,16 +4,16 @@ The agentic UI space is evolving rapidly. Here's how A2UI relates to the other m ## At a Glance -| | **A2UI** | **MCP Apps** | **AG UI** | -|---|---|---|---| -| **Approach** | Declarative component blueprints | Pre-built HTML via `ui://` URIs | High-bandwidth protocol connecting backends to frontends | -| **Rendering** | Native components (Angular, Flutter, Lit, etc.) | Sandboxed `iframe` | Developer-defined (any framework) | -| **Styling** | Host app controls — inherits design system | Isolated — remote server controls appearance | Developer controls — part of the host app | -| **Security** | Declarative data, no code execution | Sandboxed iframe isolation | Trusted code within your own app | -| **Multi-agent** | ✅ Across trust boundaries | ✅ Multiple MCP servers | ⚠️ Primarily single-agent | -| **Cross-platform** | ✅ Web, mobile, desktop, native | ⚠️ Web-focused (iframe) | ✅ Protocol is framework-agnostic | -| **LLM generation** | ✅ Designed for streaming output | ❌ Pre-built by server | ✅ Via A2UI integration | -| **Spec** | Open protocol (Apache 2.0) | [MCP extension](https://modelcontextprotocol.io/docs/extensions/apps) (SEP-1865) | Open source (by CopilotKit) | +| | **A2UI** | **MCP Apps** | **AG UI** | +| ------------------ | ----------------------------------------------- | -------------------------------------------------------------------------------- | -------------------------------------------------------- | +| **Approach** | Declarative component blueprints | Pre-built HTML via `ui://` URIs | High-bandwidth protocol connecting backends to frontends | +| **Rendering** | Native components (Angular, Flutter, Lit, etc.) | Sandboxed `iframe` | Developer-defined (any framework) | +| **Styling** | Host app controls — inherits design system | Isolated — remote server controls appearance | Developer controls — part of the host app | +| **Security** | Declarative data, no code execution | Sandboxed iframe isolation | Trusted code within your own app | +| **Multi-agent** | ✅ Across trust boundaries | ✅ Multiple MCP servers | ⚠️ Primarily single-agent | +| **Cross-platform** | ✅ Web, mobile, desktop, native | ⚠️ Web-focused (iframe) | ✅ Protocol is framework-agnostic | +| **LLM generation** | ✅ Designed for streaming output | ❌ Pre-built by server | ✅ Via A2UI integration | +| **Spec** | Open protocol (Apache 2.0) | [MCP extension](https://modelcontextprotocol.io/docs/extensions/apps) (SEP-1865) | Open source (by CopilotKit) | ## A2UI vs MCP Apps diff --git a/docs/introduction/who-is-it-for.md b/docs/introduction/who-is-it-for.md index b94ff40c3..bc34d9be8 100644 --- a/docs/introduction/who-is-it-for.md +++ b/docs/introduction/who-is-it-for.md @@ -34,7 +34,7 @@ Build agents that generate forms, dashboards, and interactive workflows. ### 3. Platform Builders (SDK Creators) -Build agent orchestration platforms, frameworks, or UI integrations. +Build agent orchestration platforms, frameworks, or UI integrations. Do you bring remote agents into your app? @@ -53,6 +53,7 @@ Do you ship your agent into other apps you don't necessarily control? ## When to Use A2UI Use A2UI in the following scenarios: + - **Agent-generated UI**: Core purpose. - **Multi-agent systems**: Standard protocol across trust boundaries. - **Cross-platform apps**: One agent, many renderers (web/mobile/desktop). @@ -60,6 +61,7 @@ Use A2UI in the following scenarios: - **Brand consistency**: Client controls styling. Do not use A2UI for: + - **Static websites**: Use HTML/CSS. - **Simple text-only chat**: Use Markdown. - **Remote widgets not integrated with client**: Use iframes, like [MCP Apps](../introduction/agent-ui-ecosystem.md). diff --git a/docs/quickstart.md b/docs/quickstart.md index a80bab972..8d23bdf76 100644 --- a/docs/quickstart.md +++ b/docs/quickstart.md @@ -69,12 +69,14 @@ The source code for the Restaurant Finder agent is located in [`samples/agent/ad If you prefer to run the agent and client in separate terminals, or need to troubleshoot: **1. Run the Agent:** + ```bash cd samples/agent/adk/restaurant_finder uv run . ``` **2. Run the Client:** + ```bash cd samples/client/lit/shell npm run dev @@ -208,6 +210,7 @@ This runs a client-only demo showcasing every standard component (Card, Button, ### Other Languages and Frameworks While this guide uses the Lit client as an example, A2UI provides samples for other popular frameworks in the `samples/client` directory: + - **Angular**: `samples/client/angular` - **React**: `samples/client/react` diff --git a/docs/reference/agents.md b/docs/reference/agents.md index ac759a9ea..69a22a177 100644 --- a/docs/reference/agents.md +++ b/docs/reference/agents.md @@ -21,30 +21,31 @@ User interactions from the client can be treated as new user input. The A2UI repository includes sample agents you can learn from: -- [Restaurant Finder](https://github.com/google/A2UI/tree/main/samples/agent/adk/restaurant_finder) - - Table reservations with forms. - - Written with the ADK. -- [Contact Lookup](https://github.com/google/A2UI/tree/main/samples/agent/adk/contact_lookup) - - Search with result lists. - - Written with the ADK. -- [Rizzcharts](https://github.com/google/A2UI/tree/main/samples/agent/adk/rizzcharts/python) - - A2UI Custom components demo. - - Written with the ADK. -- [Orchestrator](https://github.com/google/A2UI/tree/main/samples/agent/adk/orchestrator) - - Passes A2UI messages from remote subagents. - - Written with the ADK. +- [Restaurant Finder](https://github.com/google/A2UI/tree/main/samples/agent/adk/restaurant_finder) + - Table reservations with forms. + - Written with the ADK. +- [Contact Lookup](https://github.com/google/A2UI/tree/main/samples/agent/adk/contact_lookup) + - Search with result lists. + - Written with the ADK. +- [Rizzcharts](https://github.com/google/A2UI/tree/main/samples/agent/adk/rizzcharts/python) + - A2UI Custom components demo. + - Written with the ADK. +- [Orchestrator](https://github.com/google/A2UI/tree/main/samples/agent/adk/orchestrator) + - Passes A2UI messages from remote subagents. + - Written with the ADK. ## Agent Types in A2A ### 1. User Facing Agent (standalone) -A user facing agent is one that is directly interacted with by the user. +A user facing agent is one that is directly interacted with by the user. ### 2. User Facing Agent as a host for a Remote Agent This is a pattern where the user facing agent is a host for one or more remote agents. The user facing agent will call the remote agent and the remote agent will generate the A2UI messages. This is a common pattern in [A2A](https://a2a-protocol.org) with the client agent calling the server agent. In this pattern, the user-facing agent can handle messages in two ways: + - The user facing agent may "passthrough" the A2UI message without altering them. - The user facing agent may alter the A2UI message before sending it to the client. diff --git a/docs/reference/components.md b/docs/reference/components.md index 9a050ec83..339a04784 100644 --- a/docs/reference/components.md +++ b/docs/reference/components.md @@ -612,18 +612,18 @@ All components share: The component names and properties are largely the same across versions. The structural differences are: -| Aspect | v0.8 | v0.9 | -|--------|------|------| -| Component wrapper | `"component": { "Text": { ... } }` | `"component": "Text", ...props` | -| String values | `{ "literalString": "Hello" }` | `"Hello"` | -| Children | `{ "explicitList": ["a", "b"] }` | `["a", "b"]` | -| Data binding | `{ "path": "/data" }` | `{ "path": "/data" }` (same) | -| Text/Image styling | `usageHint` | `variant` | -| Button styling | `primary: true` | `variant: "primary"` | -| Action format | `{ "name": "..." }` | `{ "event": { "name": "..." } }` | -| Choice component | `MultipleChoice` | `ChoicePicker` | -| Layout alignment | `distribution`, `alignment` | `justify`, `align` | -| TextField value | `text` | `value` | +| Aspect | v0.8 | v0.9 | +| ------------------ | ---------------------------------- | -------------------------------- | +| Component wrapper | `"component": { "Text": { ... } }` | `"component": "Text", ...props` | +| String values | `{ "literalString": "Hello" }` | `"Hello"` | +| Children | `{ "explicitList": ["a", "b"] }` | `["a", "b"]` | +| Data binding | `{ "path": "/data" }` | `{ "path": "/data" }` (same) | +| Text/Image styling | `usageHint` | `variant` | +| Button styling | `primary: true` | `variant: "primary"` | +| Action format | `{ "name": "..." }` | `{ "event": { "name": "..." } }` | +| Choice component | `MultipleChoice` | `ChoicePicker` | +| Layout alignment | `distribution`, `alignment` | `justify`, `align` | +| TextField value | `text` | `value` | ## Live Examples diff --git a/docs/reference/messages.md b/docs/reference/messages.md index 77f634705..a7ea91b7a 100644 --- a/docs/reference/messages.md +++ b/docs/reference/messages.md @@ -586,11 +586,12 @@ Remove a surface and all its components and data. | Property | Type | Required | Description | | ----------- | ------ | -------- | --------------------------- | -| `surfaceId` | string | ✅ | ID of the surface to delete | +| `surfaceId` | string | ✅ | ID of the surface to delete | ### Usage Notes Keep these usage notes in mind: + - Removes all components associated with the surface. - Clears the data model for the surface. - Client should remove the surface from the UI. diff --git a/docs/reference/renderers.md b/docs/reference/renderers.md index 1de73143d..99f04d399 100644 --- a/docs/reference/renderers.md +++ b/docs/reference/renderers.md @@ -14,20 +14,20 @@ You have a lot of flexibility, to bring custom components to a renderer, or buil ### Web -| Renderer | Platform | v0.8 | v0.9 | Links | -|----------|----------|------|------|-------| -| **React** | Web | ✅ Stable | ❌ | [Code](https://github.com/google/A2UI/tree/main/renderers/react) | -| **Lit (Web Components)** | Web | ✅ Stable | ✅ Stable | [Code](https://github.com/google/A2UI/tree/main/renderers/lit) | -| **Angular** | Web | ✅ Stable | ✅ Stable | [Code](https://github.com/google/A2UI/tree/main/renderers/angular) | -| **Flutter (GenUI SDK)** | Mobile/Desktop/Web | ✅ Stable | ✅ Stable | [Docs](https://docs.flutter.dev/ai/genui) · [Code](https://github.com/flutter/genui) | +| Renderer | Platform | v0.8 | v0.9 | Links | +| ------------------------ | ------------------ | --------- | --------- | ------------------------------------------------------------------------------------ | +| **React** | Web | ✅ Stable | ❌ | [Code](https://github.com/google/A2UI/tree/main/renderers/react) | +| **Lit (Web Components)** | Web | ✅ Stable | ✅ Stable | [Code](https://github.com/google/A2UI/tree/main/renderers/lit) | +| **Angular** | Web | ✅ Stable | ✅ Stable | [Code](https://github.com/google/A2UI/tree/main/renderers/angular) | +| **Flutter (GenUI SDK)** | Mobile/Desktop/Web | ✅ Stable | ✅ Stable | [Docs](https://docs.flutter.dev/ai/genui) · [Code](https://github.com/flutter/genui) | ### Mobile -| Renderer | Platform | v0.8 | v0.9 | Links | -|----------|----------|------|------|-------| -| **Flutter (GenUI SDK)** | Mobile/Desktop/Web | ✅ Stable | ✅ Stable | [Docs](https://docs.flutter.dev/ai/genui) · [Code](https://github.com/flutter/genui) | -| **SwiftUI** | iOS/macOS | — | 🚧 Planned Q2 | — | -| **Jetpack Compose** | Android | — | 🚧 Planned Q2 | — | +| Renderer | Platform | v0.8 | v0.9 | Links | +| ----------------------- | ------------------ | --------- | ------------- | ------------------------------------------------------------------------------------ | +| **Flutter (GenUI SDK)** | Mobile/Desktop/Web | ✅ Stable | ✅ Stable | [Docs](https://docs.flutter.dev/ai/genui) · [Code](https://github.com/flutter/genui) | +| **SwiftUI** | iOS/macOS | — | 🚧 Planned Q2 | — | +| **Jetpack Compose** | Android | — | 🚧 Planned Q2 | — | Check the [Roadmap](../roadmap.md) for more. diff --git a/docs/roadmap.md b/docs/roadmap.md index 930d29dfb..9bcd7a023 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -6,10 +6,10 @@ This roadmap outlines the current state and future plans for the A2UI project. T ### Protocol -| Version | Status | Notes | -|---------|--------|-------| -| **v0.8** | ✅ Stable | Initial public release | -| **v0.9** | 🚧 Draft | Prompt-First specification improvements | +| Version | Status | Notes | +| -------- | --------- | --------------------------------------- | +| **v0.8** | ✅ Stable | Initial public release | +| **v0.9** | 🚧 Draft | Prompt-First specification improvements | Key features: @@ -21,51 +21,51 @@ Key features: ### Renderers -| Client libraries | Status | Platform | Notes | -|-----------------|--------|----------|-------| -| **Web Components (Lit)** | ✅ Stable | Web | Framework-agnostic, works anywhere | -| **Angular** | ✅ Stable | Web | Full Angular integration | -| **Flutter (GenUI SDK)** | ✅ Stable | Multi-platform | Works on mobile, web, desktop | -| **React** | 🚧 In Progress | Web | Coming Q1 2026 | -| **SwiftUI** | 📋 Planned | iOS/macOS | Planned for Q2 2026 | -| **Jetpack Compose** | 📋 Planned | Android | Planned for Q2 2026 | -| **Vue** | 💡 Proposed | Web | Community interest | -| [**Svelte/Kit**](https://svelte.dev/docs/kit/introduction) | 💡 Proposed | Web | [Community interest](https://news.ycombinator.com/item?id=46287728) | -| **ShadCN (React)** | 💡 Proposed | Web | Community interest | +| Client libraries | Status | Platform | Notes | +| ---------------------------------------------------------- | -------------- | -------------- | ------------------------------------------------------------------- | +| **Web Components (Lit)** | ✅ Stable | Web | Framework-agnostic, works anywhere | +| **Angular** | ✅ Stable | Web | Full Angular integration | +| **Flutter (GenUI SDK)** | ✅ Stable | Multi-platform | Works on mobile, web, desktop | +| **React** | 🚧 In Progress | Web | Coming Q1 2026 | +| **SwiftUI** | 📋 Planned | iOS/macOS | Planned for Q2 2026 | +| **Jetpack Compose** | 📋 Planned | Android | Planned for Q2 2026 | +| **Vue** | 💡 Proposed | Web | Community interest | +| [**Svelte/Kit**](https://svelte.dev/docs/kit/introduction) | 💡 Proposed | Web | [Community interest](https://news.ycombinator.com/item?id=46287728) | +| **ShadCN (React)** | 💡 Proposed | Web | Community interest | ### Transports -| Transport | Status | Notes | -|-------------|--------|-------| -| **A2A Protocol** | ✅ Complete | Native A2A transport | -| **AG UI** | ✅ Complete | Day-zero compatibility | -| **REST API** | 📋 Planned | Bidirectional communication | -| **WebSockets** | 💡 Proposed | Bidirectional communication | -| **SSE (Server-Sent Events)** | 💡 Proposed | Web streaming | -| **MCP (Model Context Protocol)** | 💡 Proposed | Community interest | +| Transport | Status | Notes | +| -------------------------------- | ----------- | --------------------------- | +| **A2A Protocol** | ✅ Complete | Native A2A transport | +| **AG UI** | ✅ Complete | Day-zero compatibility | +| **REST API** | 📋 Planned | Bidirectional communication | +| **WebSockets** | 💡 Proposed | Bidirectional communication | +| **SSE (Server-Sent Events)** | 💡 Proposed | Web streaming | +| **MCP (Model Context Protocol)** | 💡 Proposed | Community interest | ### Agent UI toolkits -| Agent UI toolkit | Status | Notes | -|-------------|--------|-------| -| **CopilotKit** | ✅ Complete | Day-zero compatibility thanks to AG UI | -| **Open AI ChatKit** | 💡 Proposed | Community interest | -| **Vecel AI SDK UI** | 💡 Proposed | Community interest | +| Agent UI toolkit | Status | Notes | +| ------------------- | ----------- | -------------------------------------- | +| **CopilotKit** | ✅ Complete | Day-zero compatibility thanks to AG UI | +| **Open AI ChatKit** | 💡 Proposed | Community interest | +| **Vecel AI SDK UI** | 💡 Proposed | Community interest | ### Agent frameworks -| Integration | Status | Notes | -|-------------|--------|-------| -| **Any agent with A2A support** | ✅ Complete | Day-zero compatibility thanks to A2A protocol | -| **ADK** | 📋 Planned | Still designing developer ergonomics, see [samples](../samples/agent/adk) | -| **Genkit** | 💡 Proposed | Community interest | -| **LangGraph** | 💡 Proposed | Community interest | -| **CrewAI** | 💡 Proposed | Community interest | -| **AG2** | ✅ Complete | [A2UIAgent](https://docs.ag2.ai/latest/docs/user-guide/reference-agents/a2uiagent) | -| **Claude Agent SDK** | 💡 Proposed | Community interest | -| **OpenAI Agent SDK** | 💡 Proposed | Community interest | -| **Microsoft Agent Framework** | 💡 Proposed | Community interest | -| **AWS Strands Agent SDK** | 💡 Proposed | Community interest | +| Integration | Status | Notes | +| ------------------------------ | ----------- | ---------------------------------------------------------------------------------- | +| **Any agent with A2A support** | ✅ Complete | Day-zero compatibility thanks to A2A protocol | +| **ADK** | 📋 Planned | Still designing developer ergonomics, see [samples](../samples/agent/adk) | +| **Genkit** | 💡 Proposed | Community interest | +| **LangGraph** | 💡 Proposed | Community interest | +| **CrewAI** | 💡 Proposed | Community interest | +| **AG2** | ✅ Complete | [A2UIAgent](https://docs.ag2.ai/latest/docs/user-guide/reference-agents/a2uiagent) | +| **Claude Agent SDK** | 💡 Proposed | Community interest | +| **OpenAI Agent SDK** | 💡 Proposed | Community interest | +| **Microsoft Agent Framework** | 💡 Proposed | Community interest | +| **AWS Strands Agent SDK** | 💡 Proposed | Community interest | ## Recent Milestones diff --git a/docs/scripts/README.md b/docs/scripts/README.md index b21d2df6a..33d5ea1dc 100644 --- a/docs/scripts/README.md +++ b/docs/scripts/README.md @@ -13,6 +13,7 @@ The script performs a uni-directional transformation: **GitHub Markdown → MkDo ### Alert/Admonition Conversion The script handles the following conversions: + - GitHub uses a blockquote-based syntax for alerts. - MkDocs requires the `!!!` or `???` syntax to render colored callout boxes. @@ -27,6 +28,7 @@ python docs/scripts/convert_docs.py ### Example - **Source (GitHub-flavored Markdown):** + ```markdown > ⚠️ **Attention** > @@ -36,5 +38,5 @@ python docs/scripts/convert_docs.py - **Target (MkDocs Syntax):** ```markdown !!! warning "Attention" - This is an alert. + This is an alert. ``` diff --git a/docs/specification/v0.8-a2a-extension.md b/docs/specification/v0.8-a2a-extension.md index e1fa89d33..14c7bc3cf 100644 --- a/docs/specification/v0.8-a2a-extension.md +++ b/docs/specification/v0.8-a2a-extension.md @@ -1,8 +1,7 @@ # A2UI Extension for A2A Protocol v0.8 — Stable - > NOTE: Living Document -> +> > This specification is automatically included from `specification/v0_8/docs/a2ui_extension_specification.md`. Any updates to the specification will automatically appear here. > NOTE: Version Compatibility diff --git a/docs/specification/v0.8-a2ui.md b/docs/specification/v0.8-a2ui.md index eda04d6b4..c9aff9975 100644 --- a/docs/specification/v0.8-a2ui.md +++ b/docs/specification/v0.8-a2ui.md @@ -1,5 +1,5 @@ # A2UI Protocol v0.8 — Stable -> + > Version 0.8 is the current stable release, recommended for production use. > NOTE: Living Document diff --git a/docs/specification/v0.9-a2ui.md b/docs/specification/v0.9-a2ui.md index 810c35806..be693c5b9 100644 --- a/docs/specification/v0.9-a2ui.md +++ b/docs/specification/v0.9-a2ui.md @@ -1,9 +1,9 @@ # A2UI Protocol v0.9 — Draft -> + > Version 0.9 is currently in draft status. For production use, consider [v0.8 (Stable)](v0.8-a2ui.md). > NOTE: Living Document -> +> > This specification is automatically included from `specification/v0_9/docs/a2ui_protocol.md`. Any updates to the specification will automatically appear here. For more information, see the following related documentation: diff --git a/docs/specification/v0.9-evolution-guide.md b/docs/specification/v0.9-evolution-guide.md index c5f2c8d91..dc3c557cf 100644 --- a/docs/specification/v0.9-evolution-guide.md +++ b/docs/specification/v0.9-evolution-guide.md @@ -1,5 +1,5 @@ # Evolution Guide v0.8 → v0.9 -> + > This guide is automatically included from `specification/v0_9/docs/evolution_guide.md`. Any updates will automatically appear here. For more information, see the following related documentation: diff --git a/docs/stylesheets/custom.css b/docs/stylesheets/custom.css index d01639f28..60747d826 100644 --- a/docs/stylesheets/custom.css +++ b/docs/stylesheets/custom.css @@ -31,12 +31,12 @@ /* --- Dark/Light Mode Utilities --- */ /* Hide elements meant for dark mode when in light (default) mode */ -[data-md-color-scheme="default"] .dark-mode-only { +[data-md-color-scheme='default'] .dark-mode-only { display: none !important; } /* Hide elements meant for light mode when in dark (slate) mode */ -[data-md-color-scheme="slate"] .light-mode-only { +[data-md-color-scheme='slate'] .light-mode-only { display: none !important; } @@ -68,17 +68,17 @@ } /* Dark mode overrides */ -[data-md-color-scheme="slate"] .version-badge.stable { +[data-md-color-scheme='slate'] .version-badge.stable { background-color: #1b5e20; color: #a5d6a7; border-color: #2e7d32; } -[data-md-color-scheme="slate"] .version-badge.draft { +[data-md-color-scheme='slate'] .version-badge.draft { background-color: #e65100; color: #ffe0b2; border-color: #ef6c00; } -[data-md-color-scheme="slate"] .version-badge.info { +[data-md-color-scheme='slate'] .version-badge.info { background-color: #01579b; color: #b3e5fc; border-color: #0277bd; diff --git a/mkdocs.yaml b/mkdocs.yaml index 6574c804a..9d4c5d6fc 100644 --- a/mkdocs.yaml +++ b/mkdocs.yaml @@ -23,12 +23,12 @@ extra: provider: google property: G-YX9TPV8DCC consent: - title: Cookie consent - description: >- - We use cookies to recognize repeated visits and preferences, - as well as to measure the effectiveness of our documentation and - whether users find the information they need. With your consent, - you're helping us to make our documentation better. + title: Cookie consent + description: >- + We use cookies to recognize repeated visits and preferences, + as well as to measure the effectiveness of our documentation and + whether users find the information they need. With your consent, + you're helping us to make our documentation better. # Navigation nav: @@ -55,7 +55,7 @@ nav: - Renderer Development: guides/renderer-development.md - Defining Your Own Catalog: guides/defining-your-own-catalog.md - Authoring Custom Components: guides/authoring-components.md - - Theming & Styling: guides/theming.md + - Theming & Styling: guides/theming.md - A2UI over MCP: guides/a2ui_over_mcp.md - MCP Apps in A2UI: guides/mcp-apps-in-a2ui.md - A2UI in MCP Apps: guides/a2ui-in-mcp-apps.md @@ -92,7 +92,6 @@ copyright: Copyright Google 2025  |   { + actionHandler: action => { console.log('Action received:', action); }, }, @@ -57,8 +57,8 @@ export const appConfig: ApplicationConfig = { The simplest way to render an A2UI surface is using the `SurfaceComponent`. This component handles setting up the root `ComponentHost` for you. ```typescript -import { Component } from '@angular/core'; -import { SurfaceComponent } from '@a2ui/angular/v0_9'; +import {Component} from '@angular/core'; +import {SurfaceComponent} from '@a2ui/angular/v0_9'; @Component({ selector: 'app-root', @@ -74,8 +74,8 @@ export class AppComponent {} For more fine-grained control, use the `ComponentHostComponent` to render specific components within a surface: ```typescript -import { Component } from '@angular/core'; -import { ComponentHostComponent } from '@a2ui/angular/v0_9'; +import {Component} from '@angular/core'; +import {ComponentHostComponent} from '@a2ui/angular/v0_9'; @Component({ selector: 'app-custom-layout', diff --git a/renderers/angular/a2ui_explorer/src/app/action-dispatcher.service.ts b/renderers/angular/a2ui_explorer/src/app/action-dispatcher.service.ts index 20477c558..b6145a45e 100644 --- a/renderers/angular/a2ui_explorer/src/app/action-dispatcher.service.ts +++ b/renderers/angular/a2ui_explorer/src/app/action-dispatcher.service.ts @@ -14,11 +14,11 @@ * limitations under the License. */ -import { Injectable } from '@angular/core'; -import { Subject } from 'rxjs'; -import { A2uiClientAction } from '@a2ui/web_core/v0_9'; +import {Injectable} from '@angular/core'; +import {Subject} from 'rxjs'; +import {A2uiClientAction} from '@a2ui/web_core/v0_9'; -@Injectable({ providedIn: 'root' }) +@Injectable({providedIn: 'root'}) export class ActionDispatcher { private action$ = new Subject(); actions = this.action$.asObservable(); diff --git a/renderers/angular/a2ui_explorer/src/app/agent-stub.service.spec.ts b/renderers/angular/a2ui_explorer/src/app/agent-stub.service.spec.ts index 102e6f30f..30450f262 100644 --- a/renderers/angular/a2ui_explorer/src/app/agent-stub.service.spec.ts +++ b/renderers/angular/a2ui_explorer/src/app/agent-stub.service.spec.ts @@ -14,12 +14,12 @@ * limitations under the License. */ -import { TestBed } from '@angular/core/testing'; -import { AgentStubService } from './agent-stub.service'; -import { A2uiRendererService } from '@a2ui/angular/v0_9'; -import { ActionDispatcher } from './action-dispatcher.service'; -import { Subject } from 'rxjs'; -import { A2uiMessage } from '@a2ui/web_core/v0_9'; +import {TestBed} from '@angular/core/testing'; +import {AgentStubService} from './agent-stub.service'; +import {A2uiRendererService} from '@a2ui/angular/v0_9'; +import {ActionDispatcher} from './action-dispatcher.service'; +import {Subject} from 'rxjs'; +import {A2uiMessage} from '@a2ui/web_core/v0_9'; describe('AgentStubService', () => { let service: AgentStubService; @@ -44,8 +44,8 @@ describe('AgentStubService', () => { TestBed.configureTestingModule({ providers: [ AgentStubService, - { provide: A2uiRendererService, useValue: mockRendererService }, - { provide: ActionDispatcher, useValue: mockActionDispatcher }, + {provide: A2uiRendererService, useValue: mockRendererService}, + {provide: ActionDispatcher, useValue: mockActionDispatcher}, ], }); service = TestBed.inject(AgentStubService); @@ -77,7 +77,7 @@ describe('AgentStubService', () => { mockRendererService.processMessages.calls.reset(); // 2. Second call: Surface now exists - mockSurfaceGroup.getSurface.and.returnValue({ id: surfaceId }); + mockSurfaceGroup.getSurface.and.returnValue({id: surfaceId}); service.initializeDemo(messages); // Should have called processMessages twice: @@ -85,7 +85,7 @@ describe('AgentStubService', () => { const deleteMessages = [ { version: 'v0.9' as const, - deleteSurface: { surfaceId }, + deleteSurface: {surfaceId}, }, ]; expect(mockRendererService.processMessages.calls.allArgs()).toEqual([ diff --git a/renderers/angular/a2ui_explorer/src/app/agent-stub.service.ts b/renderers/angular/a2ui_explorer/src/app/agent-stub.service.ts index 6454172f5..2bc3b4467 100644 --- a/renderers/angular/a2ui_explorer/src/app/agent-stub.service.ts +++ b/renderers/angular/a2ui_explorer/src/app/agent-stub.service.ts @@ -14,11 +14,11 @@ * limitations under the License. */ -import { Injectable } from '@angular/core'; -import { A2uiRendererService } from '@a2ui/angular/v0_9'; +import {Injectable} from '@angular/core'; +import {A2uiRendererService} from '@a2ui/angular/v0_9'; -import { A2uiClientAction, A2uiMessage } from '@a2ui/web_core/v0_9'; -import { ActionDispatcher } from './action-dispatcher.service'; +import {A2uiClientAction, A2uiMessage} from '@a2ui/web_core/v0_9'; +import {ActionDispatcher} from './action-dispatcher.service'; /** * Context for the 'update_property' event. @@ -46,14 +46,14 @@ interface SubmitFormContext { }) export class AgentStubService { /** Log of actions received from the surface. */ - actionsLog: Array<{ timestamp: Date; action: A2uiClientAction }> = []; + actionsLog: Array<{timestamp: Date; action: A2uiClientAction}> = []; constructor( private rendererService: A2uiRendererService, private dispatcher: ActionDispatcher, ) { // Subscribe to actions dispatched by the renderer - this.dispatcher.actions.subscribe((action) => this.handleAction(action)); + this.dispatcher.actions.subscribe(action => this.handleAction(action)); } /** @@ -61,13 +61,13 @@ export class AgentStubService { */ handleAction(action: A2uiClientAction) { console.log('[AgentStub] handleAction action:', action); - this.actionsLog.push({ timestamp: new Date(), action }); + this.actionsLog.push({timestamp: new Date(), action}); // Simulate server processing delay setTimeout(() => { - const { name, context } = action; + const {name, context} = action; if (name === 'update_property' && context) { - const { path, value, surfaceId } = context as unknown as UpdatePropertyContext; + const {path, value, surfaceId} = context as unknown as UpdatePropertyContext; console.log( '[AgentStub] update_property path:', path, @@ -127,7 +127,7 @@ export class AgentStubService { this.rendererService.processMessages([ { version: 'v0.9', - deleteSurface: { surfaceId: createSurface.surfaceId }, + deleteSurface: {surfaceId: createSurface.surfaceId}, }, ]); } diff --git a/renderers/angular/a2ui_explorer/src/app/app.config.ts b/renderers/angular/a2ui_explorer/src/app/app.config.ts index 7b73c5be3..90a01595b 100644 --- a/renderers/angular/a2ui_explorer/src/app/app.config.ts +++ b/renderers/angular/a2ui_explorer/src/app/app.config.ts @@ -14,8 +14,8 @@ * limitations under the License. */ -import { ApplicationConfig, provideBrowserGlobalErrorListeners } from '@angular/core'; -import { provideMarkdownRenderer } from '../../../src/v0_9/core/markdown'; +import {ApplicationConfig, provideBrowserGlobalErrorListeners} from '@angular/core'; +import {provideMarkdownRenderer} from '../../../src/v0_9/core/markdown'; export const appConfig: ApplicationConfig = { providers: [provideBrowserGlobalErrorListeners(), provideMarkdownRenderer()], diff --git a/renderers/angular/a2ui_explorer/src/app/app.spec.ts b/renderers/angular/a2ui_explorer/src/app/app.spec.ts index e905d1ab9..a6b41e1cf 100644 --- a/renderers/angular/a2ui_explorer/src/app/app.spec.ts +++ b/renderers/angular/a2ui_explorer/src/app/app.spec.ts @@ -14,8 +14,8 @@ * limitations under the License. */ -import { TestBed, fakeAsync, tick } from '@angular/core/testing'; -import { App } from './app'; +import {TestBed, fakeAsync, tick} from '@angular/core/testing'; +import {App} from './app'; describe('App', () => { beforeEach(async () => { diff --git a/renderers/angular/a2ui_explorer/src/app/app.ts b/renderers/angular/a2ui_explorer/src/app/app.ts index 23665e10d..a4e2fd0f1 100644 --- a/renderers/angular/a2ui_explorer/src/app/app.ts +++ b/renderers/angular/a2ui_explorer/src/app/app.ts @@ -14,8 +14,8 @@ * limitations under the License. */ -import { Component } from '@angular/core'; -import { DemoComponent } from './demo.component'; +import {Component} from '@angular/core'; +import {DemoComponent} from './demo.component'; /** * Root Component of the A2UI Angular Demo app. diff --git a/renderers/angular/a2ui_explorer/src/app/card.component.ts b/renderers/angular/a2ui_explorer/src/app/card.component.ts index 5b7343228..b5fd10daf 100644 --- a/renderers/angular/a2ui_explorer/src/app/card.component.ts +++ b/renderers/angular/a2ui_explorer/src/app/card.component.ts @@ -14,10 +14,10 @@ * limitations under the License. */ -import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; -import { CommonModule } from '@angular/common'; -import { ComponentHostComponent } from '@a2ui/angular/v0_9'; -import { BoundProperty } from '@a2ui/angular/v0_9'; +import {Component, Input, ChangeDetectionStrategy} from '@angular/core'; +import {CommonModule} from '@angular/common'; +import {ComponentHostComponent} from '@a2ui/angular/v0_9'; +import {BoundProperty} from '@a2ui/angular/v0_9'; /** * A simple card component for the demo. diff --git a/renderers/angular/a2ui_explorer/src/app/custom-slider.component.ts b/renderers/angular/a2ui_explorer/src/app/custom-slider.component.ts index e983bab64..27d29472c 100644 --- a/renderers/angular/a2ui_explorer/src/app/custom-slider.component.ts +++ b/renderers/angular/a2ui_explorer/src/app/custom-slider.component.ts @@ -14,11 +14,11 @@ * limitations under the License. */ -import { Component, ChangeDetectionStrategy } from '@angular/core'; -import { CommonModule } from '@angular/common'; -import { CatalogComponent } from 'src/v0_9/core/catalog_component'; +import {Component, ChangeDetectionStrategy} from '@angular/core'; +import {CommonModule} from '@angular/common'; +import {CatalogComponent} from 'src/v0_9/core/catalog_component'; import z from 'zod'; -import { ComponentApi } from '@a2ui/web_core/v0_9'; +import {ComponentApi} from '@a2ui/web_core/v0_9'; const customSliderApi = { name: 'CustomSlider', @@ -71,4 +71,7 @@ export class CustomSliderComponent extends CatalogComponent { + args => { const value = String(args.value || ''); return value.charAt(0).toUpperCase() + value.slice(1); }, diff --git a/renderers/angular/a2ui_explorer/src/app/demo.component.ts b/renderers/angular/a2ui_explorer/src/app/demo.component.ts index 0f56493e5..ab738bb35 100644 --- a/renderers/angular/a2ui_explorer/src/app/demo.component.ts +++ b/renderers/angular/a2ui_explorer/src/app/demo.component.ts @@ -14,17 +14,17 @@ * limitations under the License. */ -import { ChangeDetectorRef, Component, OnInit, inject, OnDestroy } from '@angular/core'; -import { CommonModule } from '@angular/common'; -import { A2uiRendererService, A2UI_RENDERER_CONFIG } from '@a2ui/angular/v0_9'; -import { AgentStubService } from './agent-stub.service'; -import { SurfaceComponent } from '@a2ui/angular/v0_9'; -import { AngularCatalog } from '@a2ui/angular/v0_9'; -import { DemoCatalog } from './demo-catalog'; -import { A2uiClientAction, CreateSurfaceMessage } from '@a2ui/web_core/v0_9'; -import { EXAMPLES } from './generated/examples-bundle'; -import { Example } from './types'; -import { ActionDispatcher } from './action-dispatcher.service'; +import {ChangeDetectorRef, Component, OnInit, inject, OnDestroy} from '@angular/core'; +import {CommonModule} from '@angular/common'; +import {A2uiRendererService, A2UI_RENDERER_CONFIG} from '@a2ui/angular/v0_9'; +import {AgentStubService} from './agent-stub.service'; +import {SurfaceComponent} from '@a2ui/angular/v0_9'; +import {AngularCatalog} from '@a2ui/angular/v0_9'; +import {DemoCatalog} from './demo-catalog'; +import {A2uiClientAction, CreateSurfaceMessage} from '@a2ui/web_core/v0_9'; +import {EXAMPLES} from './generated/examples-bundle'; +import {Example} from './types'; +import {ActionDispatcher} from './action-dispatcher.service'; /** * Main dashboard component for A2UI v0.9 Angular Renderer. @@ -73,17 +73,28 @@ import { ActionDispatcher } from './action-dispatcher.service';
-
+
- + @@ -108,17 +119,28 @@ import { ActionDispatcher } from './action-dispatcher.service';
-
+
- + @@ -143,24 +165,37 @@ import { ActionDispatcher } from './action-dispatcher.service';
-
+
- +

Events Log

- +
@@ -460,7 +495,7 @@ import { ActionDispatcher } from './action-dispatcher.service'; ], providers: [ A2uiRendererService, - { provide: AngularCatalog, useClass: DemoCatalog }, + {provide: AngularCatalog, useClass: DemoCatalog}, ActionDispatcher, AgentStubService, { @@ -484,7 +519,7 @@ export class DemoComponent implements OnInit, OnDestroy { inspectTab: 'data' | 'events' = 'data'; currentDataModel: Record = {}; - eventsLog: Array<{ timestamp: Date; action: A2uiClientAction }> = []; + eventsLog: Array<{timestamp: Date; action: A2uiClientAction}> = []; currentCreateSurfaceMessageJson: string = ''; messageError: string | null = null; currentDataModelJson: string = ''; @@ -509,8 +544,8 @@ export class DemoComponent implements OnInit, OnDestroy { localStorage.setItem('isEventsLogFolded', String(this.isEventsLogFolded)); } - private actionSub?: { unsubscribe: () => void }; - private dataModelSub?: { unsubscribe: () => void }; + private actionSub?: {unsubscribe: () => void}; + private dataModelSub?: {unsubscribe: () => void}; ngOnInit(): void { if (typeof window !== 'undefined') { @@ -564,8 +599,8 @@ export class DemoComponent implements OnInit, OnDestroy { if (this.actionSub) { this.actionSub.unsubscribe(); } - this.actionSub = this.rendererService.surfaceGroup.onAction.subscribe((action) => { - this.eventsLog.unshift({ timestamp: new Date(), action }); + this.actionSub = this.rendererService.surfaceGroup.onAction.subscribe(action => { + this.eventsLog.unshift({timestamp: new Date(), action}); this.cdr.detectChanges(); }); } @@ -592,7 +627,7 @@ export class DemoComponent implements OnInit, OnDestroy { if (!('createSurface' in parsed) || !this.selectedExample) return; const updatedMessages = this.selectedExample.messages.map(m => - 'createSurface' in m ? parsed : m + 'createSurface' in m ? parsed : m, ); // Re-initialize the demo with the updated messages @@ -611,7 +646,7 @@ export class DemoComponent implements OnInit, OnDestroy { this.surfaceId = newSurfaceId; const surface = this.rendererService.surfaceGroup?.getSurface(this.surfaceId!); if (surface) { - this.dataModelSub = surface.dataModel.subscribe('/', (data) => { + this.dataModelSub = surface.dataModel.subscribe('/', data => { this.currentDataModel = data as Record; this.currentDataModelJson = JSON.stringify(data, null, 2); this.cdr.detectChanges(); @@ -675,12 +710,16 @@ export class DemoComponent implements OnInit, OnDestroy { } private slugify(text: string): string { - return text.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, ''); + return text + .toLowerCase() + .replace(/[^a-z0-9]+/g, '-') + .replace(/^-|-$/g, ''); } private selectExampleFromUrl(): void { const hash = window.location.hash.substring(1) || ''; - const example: Example | undefined = this.examples.find(ex => this.slugify(ex.name) === hash) || this.examples[0]; + const example: Example | undefined = + this.examples.find(ex => this.slugify(ex.name) === hash) || this.examples[0]; if (!example) return; this.selectExample(example); } diff --git a/renderers/angular/a2ui_explorer/src/app/kitchen-sink-surface.ts b/renderers/angular/a2ui_explorer/src/app/kitchen-sink-surface.ts index 1e137d033..673696dd1 100644 --- a/renderers/angular/a2ui_explorer/src/app/kitchen-sink-surface.ts +++ b/renderers/angular/a2ui_explorer/src/app/kitchen-sink-surface.ts @@ -93,13 +93,13 @@ export const KITCHEN_SINK_SURFACE = [ id: 'name-field', component: 'TextField', label: 'Your Name', - value: { path: '/user/name' }, + value: {path: '/user/name'}, }, { id: 'satisfaction-slider', component: 'CustomSlider', label: 'Satisfaction Level', - value: { path: '/user/satisfaction' }, + value: {path: '/user/satisfaction'}, min: 0, max: 10, }, @@ -107,7 +107,7 @@ export const KITCHEN_SINK_SURFACE = [ id: 'email-field', component: 'TextField', label: 'Email Address', - value: { path: '/user/email' }, + value: {path: '/user/email'}, variant: 'shortText', }, { @@ -119,8 +119,8 @@ export const KITCHEN_SINK_SURFACE = [ event: { name: 'submit_form', context: { - name: { path: '/user/name' }, - email: { path: '/user/email' }, + name: {path: '/user/name'}, + email: {path: '/user/email'}, }, }, }, @@ -133,7 +133,7 @@ export const KITCHEN_SINK_SURFACE = [ { id: 'result-msg', component: 'Text', - text: { path: '/form/responseMessage' }, + text: {path: '/form/responseMessage'}, }, { id: 'footer', diff --git a/renderers/angular/a2ui_explorer/src/app/types.ts b/renderers/angular/a2ui_explorer/src/app/types.ts index 208d46691..c24524ccb 100644 --- a/renderers/angular/a2ui_explorer/src/app/types.ts +++ b/renderers/angular/a2ui_explorer/src/app/types.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { A2uiMessage } from '@a2ui/web_core/v0_9'; +import {A2uiMessage} from '@a2ui/web_core/v0_9'; /** * Represents a demo example configuration. diff --git a/renderers/angular/a2ui_explorer/src/main.ts b/renderers/angular/a2ui_explorer/src/main.ts index 94303b012..56f29a67f 100644 --- a/renderers/angular/a2ui_explorer/src/main.ts +++ b/renderers/angular/a2ui_explorer/src/main.ts @@ -14,8 +14,8 @@ * limitations under the License. */ -import { bootstrapApplication } from '@angular/platform-browser'; -import { appConfig } from './app/app.config'; -import { App } from './app/app'; +import {bootstrapApplication} from '@angular/platform-browser'; +import {appConfig} from './app/app.config'; +import {App} from './app/app'; -bootstrapApplication(App, appConfig).catch((err) => console.error(err)); +bootstrapApplication(App, appConfig).catch(err => console.error(err)); diff --git a/renderers/angular/karma.conf.js b/renderers/angular/karma.conf.js index 1402572ad..f1e790433 100644 --- a/renderers/angular/karma.conf.js +++ b/renderers/angular/karma.conf.js @@ -41,7 +41,7 @@ module.exports = function (config) { coverageReporter: { dir: require('path').join(__dirname, './coverage/lib'), subdir: '.', - reporters: [{ type: 'html' }, { type: 'text-summary' }], + reporters: [{type: 'html'}, {type: 'text-summary'}], }, reporters: ['progress', 'kjhtml'], browsers: ['Chrome'], diff --git a/renderers/angular/package.json b/renderers/angular/package.json index e9e9b535a..b3f1f6e03 100644 --- a/renderers/angular/package.json +++ b/renderers/angular/package.json @@ -75,18 +75,6 @@ "vitest": "^4.0.15" }, "sideEffects": false, - "prettier": { - "printWidth": 100, - "singleQuote": true, - "overrides": [ - { - "files": "*.html", - "options": { - "parser": "angular" - } - } - ] - }, "overrides": { "@preact/signals-core": "^1.13.0" } diff --git a/renderers/angular/scripts/generate-examples.mjs b/renderers/angular/scripts/generate-examples.mjs index 95be93e43..9b6955c65 100644 --- a/renderers/angular/scripts/generate-examples.mjs +++ b/renderers/angular/scripts/generate-examples.mjs @@ -16,7 +16,7 @@ import fs from 'fs'; import path from 'path'; -import { parseArgs } from 'node:util'; +import {parseArgs} from 'node:util'; /** * The default output file path where the generated examples bundle will be written. @@ -37,11 +37,11 @@ const DEFAULT_CATALOGS = ['minimal', 'basic']; * The options that this script accepts. */ const options = { - help: { type: 'boolean', short: 'h' }, - 'out-file': { type: 'string', short: 'o', default: DEFAULT_OUT_FILE }, - 'spec-path': { type: 'string', short: 's', default: DEFAULT_SPEC_PATH }, - catalog: { type: 'string', short: 'c', multiple: true, default: DEFAULT_CATALOGS }, - 'override-minimal-catalog-id': { type: 'boolean', default: true }, + help: {type: 'boolean', short: 'h'}, + 'out-file': {type: 'string', short: 'o', default: DEFAULT_OUT_FILE}, + 'spec-path': {type: 'string', short: 's', default: DEFAULT_SPEC_PATH}, + catalog: {type: 'string', short: 'c', multiple: true, default: DEFAULT_CATALOGS}, + 'override-minimal-catalog-id': {type: 'boolean', default: true}, }; /** @@ -62,7 +62,7 @@ Options: * preserving the version in the path. */ function overrideMessagesCatalogId(messages) { - const overrideCatalogId = (catalogId) => { + const overrideCatalogId = catalogId => { return catalogId.replace('catalogs/minimal/minimal_catalog.json', 'basic_catalog.json'); }; for (const msg of messages) { @@ -83,7 +83,7 @@ function overrideMessagesCatalogId(messages) { * Parses arguments, reads catalog examples, and generates the TypeScript bundle. */ async function main() { - const { values } = parseArgs({ options, allowNegative: true }); + const {values} = parseArgs({options, allowNegative: true}); if (values.help) { console.log(HELP_MESSAGE); @@ -96,7 +96,7 @@ async function main() { const overrideCatalogId = values['override-minimal-catalog-id']; if (!fs.existsSync(outDir)) { - fs.mkdirSync(outDir, { recursive: true }); + fs.mkdirSync(outDir, {recursive: true}); } const catalogs = values.catalog; @@ -107,7 +107,7 @@ async function main() { if (fs.existsSync(examplesDir)) { const files = fs .readdirSync(examplesDir) - .filter((f) => f.endsWith('.json')) + .filter(f => f.endsWith('.json')) .sort(); for (const file of files) { const filePath = path.join(examplesDir, file); @@ -120,7 +120,7 @@ async function main() { .replace('.json', '') .replace(/^[0-9]+_/, '') .replace(/[-_]/g, ' ') - .replace(/\b\w/g, (l) => l.toUpperCase()); + .replace(/\b\w/g, l => l.toUpperCase()); // Ensure it's in the Example format if (Array.isArray(data)) { @@ -145,7 +145,7 @@ async function main() { examples.push(example); } catch (e) { - throw new Error(`Error parsing ${filePath}`, { cause: e }); + throw new Error(`Error parsing ${filePath}`, {cause: e}); } } } else { @@ -173,7 +173,7 @@ export const EXAMPLES: Example[] = ${JSON.stringify(examples, null, 2)}; /** * Entry point of the script. */ -main().catch((err) => { +main().catch(err => { console.error(err); process.exit(1); }); diff --git a/renderers/angular/src/v0_8/catalog/index.ts b/renderers/angular/src/v0_8/catalog/index.ts index 4591fbfce..a0185f06b 100644 --- a/renderers/angular/src/v0_8/catalog/index.ts +++ b/renderers/angular/src/v0_8/catalog/index.ts @@ -14,27 +14,27 @@ * limitations under the License. */ -import { Catalog } from '../rendering/catalog'; +import {Catalog} from '../rendering/catalog'; // Components -import { AudioPlayer } from '../components/audio'; -import { Button } from '../components/button'; -import { Card } from '../components/card'; -import { Checkbox } from '../components/checkbox'; -import { Column } from '../components/column'; -import { DateTimeInput } from '../components/datetime-input'; -import { Divider } from '../components/divider'; -import { Icon } from '../components/icon'; -import { Image } from '../components/image'; -import { List } from '../components/list'; -import { Modal } from '../components/modal'; -import { MultipleChoice } from '../components/multiple-choice'; -import { Row } from '../components/row'; -import { Slider } from '../components/slider'; -import { Tabs } from '../components/tabs'; -import { Text } from '../components/text'; -import { TextField } from '../components/text-field'; -import { Video } from '../components/video'; +import {AudioPlayer} from '../components/audio'; +import {Button} from '../components/button'; +import {Card} from '../components/card'; +import {Checkbox} from '../components/checkbox'; +import {Column} from '../components/column'; +import {DateTimeInput} from '../components/datetime-input'; +import {Divider} from '../components/divider'; +import {Icon} from '../components/icon'; +import {Image} from '../components/image'; +import {List} from '../components/list'; +import {Modal} from '../components/modal'; +import {MultipleChoice} from '../components/multiple-choice'; +import {Row} from '../components/row'; +import {Slider} from '../components/slider'; +import {Tabs} from '../components/tabs'; +import {Text} from '../components/text'; +import {TextField} from '../components/text-field'; +import {Video} from '../components/video'; export const DEFAULT_CATALOG: Catalog = { AudioPlayer: () => AudioPlayer, diff --git a/renderers/angular/src/v0_8/components/audio.spec.ts b/renderers/angular/src/v0_8/components/audio.spec.ts index 42af324a9..634827cb1 100644 --- a/renderers/angular/src/v0_8/components/audio.spec.ts +++ b/renderers/angular/src/v0_8/components/audio.spec.ts @@ -14,13 +14,13 @@ * limitations under the License. */ -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { AudioPlayer } from './audio'; -import type { AudioPlayerNode } from '../types'; -import { Theme } from '../rendering/theming'; -import { ChangeDetectionStrategy } from '@angular/core'; -import { MessageProcessor } from '../data/processor'; -import { Catalog } from '../rendering/catalog'; +import {ComponentFixture, TestBed} from '@angular/core/testing'; +import {AudioPlayer} from './audio'; +import type {AudioPlayerNode} from '../types'; +import {Theme} from '../rendering/theming'; +import {ChangeDetectionStrategy} from '@angular/core'; +import {MessageProcessor} from '../data/processor'; +import {Catalog} from '../rendering/catalog'; describe('AudioPlayer Component', () => { let component: AudioPlayer; @@ -33,7 +33,7 @@ describe('AudioPlayer Component', () => { type: 'AudioPlayer', weight: 1, properties: { - url: { literalString: 'https://example.com/audio.mp3' }, + url: {literalString: 'https://example.com/audio.mp3'}, }, }; @@ -44,18 +44,18 @@ describe('AudioPlayer Component', () => { 'getData', ]); mockTheme = new Theme(); - mockTheme.additionalStyles = { AudioPlayer: { backgroundColor: 'red' } } as any; + mockTheme.additionalStyles = {AudioPlayer: {backgroundColor: 'red'}} as any; await TestBed.configureTestingModule({ imports: [AudioPlayer], providers: [ - { provide: MessageProcessor, useValue: mockProcessor }, - { provide: Theme, useValue: mockTheme }, - { provide: Catalog, useValue: {} }, + {provide: MessageProcessor, useValue: mockProcessor}, + {provide: Theme, useValue: mockTheme}, + {provide: Catalog, useValue: {}}, ], }) .overrideComponent(AudioPlayer, { - set: { changeDetection: ChangeDetectionStrategy.Default }, + set: {changeDetection: ChangeDetectionStrategy.Default}, }) .compileComponents(); diff --git a/renderers/angular/src/v0_8/components/audio.ts b/renderers/angular/src/v0_8/components/audio.ts index d337b5780..bbced37e0 100644 --- a/renderers/angular/src/v0_8/components/audio.ts +++ b/renderers/angular/src/v0_8/components/audio.ts @@ -14,9 +14,9 @@ * limitations under the License. */ -import { ChangeDetectionStrategy, Component, computed, input } from '@angular/core'; -import { DynamicComponent } from '../rendering/dynamic-component'; -import type { AudioPlayerNode, StringValue } from '../types'; +import {ChangeDetectionStrategy, Component, computed, input} from '@angular/core'; +import {DynamicComponent} from '../rendering/dynamic-component'; +import type {AudioPlayerNode, StringValue} from '../types'; @Component({ selector: 'a2ui-audio', diff --git a/renderers/angular/src/v0_8/components/button.spec.ts b/renderers/angular/src/v0_8/components/button.spec.ts index d72853cf4..cf0790c54 100644 --- a/renderers/angular/src/v0_8/components/button.spec.ts +++ b/renderers/angular/src/v0_8/components/button.spec.ts @@ -14,14 +14,14 @@ * limitations under the License. */ -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { Button } from './button'; -import { MessageProcessor } from '../data/processor'; -import { Theme } from '../rendering/theming'; -import { Catalog } from '../rendering/catalog'; -import { Renderer } from '../rendering/renderer'; -import type { Action, ButtonNode, A2UIClientEventMessage } from '../types'; -import { Directive, Input } from '@angular/core'; +import {ComponentFixture, TestBed} from '@angular/core/testing'; +import {Button} from './button'; +import {MessageProcessor} from '../data/processor'; +import {Theme} from '../rendering/theming'; +import {Catalog} from '../rendering/catalog'; +import {Renderer} from '../rendering/renderer'; +import type {Action, ButtonNode, A2UIClientEventMessage} from '../types'; +import {Directive, Input} from '@angular/core'; // Mock Renderer directive to avoid full tree rendering issues for isolated unit tests @Directive({ @@ -49,7 +49,7 @@ describe('Button Component', () => { type: 'Button', weight: 1, properties: { - child: { id: 'text-1', type: 'Text', properties: { text: 'Click Me' } }, + child: {id: 'text-1', type: 'Text', properties: {text: 'Click Me'}}, action: mockAction, }, }; @@ -63,22 +63,22 @@ describe('Button Component', () => { mockProcessor.dispatch.and.returnValue(Promise.resolve([])); mockTheme = new Theme(); - mockTheme.components = { Button: 'btn-class' } as any; - mockTheme.additionalStyles = { Button: { color: 'red' } } as any; + mockTheme.components = {Button: 'btn-class'} as any; + mockTheme.additionalStyles = {Button: {color: 'red'}} as any; const mockCatalog = {}; await TestBed.configureTestingModule({ imports: [Button], providers: [ - { provide: MessageProcessor, useValue: mockProcessor }, - { provide: Theme, useValue: mockTheme }, - { provide: Catalog, useValue: mockCatalog }, + {provide: MessageProcessor, useValue: mockProcessor}, + {provide: Theme, useValue: mockTheme}, + {provide: Catalog, useValue: mockCatalog}, ], }) .overrideComponent(Button, { - remove: { imports: [Renderer] }, - add: { imports: [MockRenderer] }, + remove: {imports: [Renderer]}, + add: {imports: [MockRenderer]}, }) .compileComponents(); diff --git a/renderers/angular/src/v0_8/components/button.ts b/renderers/angular/src/v0_8/components/button.ts index fbfc6f3c3..0ff926a8b 100644 --- a/renderers/angular/src/v0_8/components/button.ts +++ b/renderers/angular/src/v0_8/components/button.ts @@ -14,10 +14,10 @@ * limitations under the License. */ -import { ChangeDetectionStrategy, Component, input } from '@angular/core'; -import type { ButtonNode, Action, AnyComponentNode } from '../types'; -import { DynamicComponent } from '../rendering/dynamic-component'; -import { Renderer } from '../rendering/renderer'; +import {ChangeDetectionStrategy, Component, input} from '@angular/core'; +import type {ButtonNode, Action, AnyComponentNode} from '../types'; +import {DynamicComponent} from '../rendering/dynamic-component'; +import {Renderer} from '../rendering/renderer'; @Component({ selector: 'a2ui-button', diff --git a/renderers/angular/src/v0_8/components/card.spec.ts b/renderers/angular/src/v0_8/components/card.spec.ts index f16d92f0a..42fd4f895 100644 --- a/renderers/angular/src/v0_8/components/card.spec.ts +++ b/renderers/angular/src/v0_8/components/card.spec.ts @@ -14,13 +14,13 @@ * limitations under the License. */ -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { Card } from './card'; -import { MessageProcessor } from '../data/processor'; -import { Theme } from '../rendering/theming'; -import { Catalog } from '../rendering/catalog'; -import type { CardNode } from '../types'; -import { Directive, Input, ChangeDetectionStrategy } from '@angular/core'; +import {ComponentFixture, TestBed} from '@angular/core/testing'; +import {Card} from './card'; +import {MessageProcessor} from '../data/processor'; +import {Theme} from '../rendering/theming'; +import {Catalog} from '../rendering/catalog'; +import type {CardNode} from '../types'; +import {Directive, Input, ChangeDetectionStrategy} from '@angular/core'; @Directive({ selector: '[a2ui-renderer]', @@ -46,21 +46,21 @@ describe('Card Component', () => { type: 'Card', weight: 1, properties: { - child: { id: 'dummy-1', type: 'Text', properties: { text: 'Empty' } }, + child: {id: 'dummy-1', type: 'Text', properties: {text: 'Empty'}}, children: [], }, }; beforeEach(async () => { mockTheme = new Theme(); - mockTheme.components = { Card: 'card-class' } as any; + mockTheme.components = {Card: 'card-class'} as any; await TestBed.configureTestingModule({ imports: [Card], providers: [ - { provide: MessageProcessor, useValue: {} }, - { provide: Theme, useValue: mockTheme }, - { provide: Catalog, useValue: {} }, + {provide: MessageProcessor, useValue: {}}, + {provide: Theme, useValue: mockTheme}, + {provide: Catalog, useValue: {}}, ], }) .overrideComponent(Card, { @@ -93,7 +93,7 @@ describe('Card Component', () => { }); it('should render child if provided', () => { - const childNode = { id: 'child-1', type: 'Text', properties: {} }; + const childNode = {id: 'child-1', type: 'Text', properties: {}}; fixture.componentRef.setInput('child', childNode); fixture.detectChanges(); @@ -107,8 +107,8 @@ describe('Card Component', () => { it('should render children if provided', () => { const childrenNodes = [ - { id: 'child-1', type: 'Text', properties: {} }, - { id: 'child-2', type: 'Text', properties: {} }, + {id: 'child-1', type: 'Text', properties: {}}, + {id: 'child-2', type: 'Text', properties: {}}, ]; fixture.componentRef.setInput('children', childrenNodes); fixture.detectChanges(); diff --git a/renderers/angular/src/v0_8/components/card.ts b/renderers/angular/src/v0_8/components/card.ts index 0ee465bfc..165bde4cb 100644 --- a/renderers/angular/src/v0_8/components/card.ts +++ b/renderers/angular/src/v0_8/components/card.ts @@ -14,10 +14,10 @@ * limitations under the License. */ -import { ChangeDetectionStrategy, Component, input } from '@angular/core'; -import { DynamicComponent } from '../rendering/dynamic-component'; -import { Renderer } from '../rendering/renderer'; -import type { AnyComponentNode, CardNode } from '../types'; +import {ChangeDetectionStrategy, Component, input} from '@angular/core'; +import {DynamicComponent} from '../rendering/dynamic-component'; +import {Renderer} from '../rendering/renderer'; +import type {AnyComponentNode, CardNode} from '../types'; @Component({ selector: 'a2ui-card', diff --git a/renderers/angular/src/v0_8/components/checkbox.spec.ts b/renderers/angular/src/v0_8/components/checkbox.spec.ts index 8c6153b8c..4e85be139 100644 --- a/renderers/angular/src/v0_8/components/checkbox.spec.ts +++ b/renderers/angular/src/v0_8/components/checkbox.spec.ts @@ -14,11 +14,11 @@ * limitations under the License. */ -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { Checkbox } from './checkbox'; -import { MessageProcessor } from '../data/processor'; -import { Theme } from '../rendering/theming'; -import type { A2UIClientEventMessage, CheckboxNode } from '../types'; +import {ComponentFixture, TestBed} from '@angular/core/testing'; +import {Checkbox} from './checkbox'; +import {MessageProcessor} from '../data/processor'; +import {Theme} from '../rendering/theming'; +import type {A2UIClientEventMessage, CheckboxNode} from '../types'; describe('Checkbox Component', () => { let component: Checkbox; @@ -30,8 +30,8 @@ describe('Checkbox Component', () => { type: 'CheckBox', weight: 1, properties: { - label: { literalString: 'Accept Terms' }, - value: { literalBoolean: false }, + label: {literalString: 'Accept Terms'}, + value: {literalBoolean: false}, }, }; @@ -46,8 +46,8 @@ describe('Checkbox Component', () => { await TestBed.configureTestingModule({ imports: [Checkbox], providers: [ - { provide: MessageProcessor, useValue: mockProcessor }, - { provide: Theme, useValue: new Theme() }, + {provide: MessageProcessor, useValue: mockProcessor}, + {provide: Theme, useValue: new Theme()}, ], }).compileComponents(); @@ -57,8 +57,8 @@ describe('Checkbox Component', () => { fixture.componentRef.setInput('surfaceId', 'surface-1'); fixture.componentRef.setInput('component', mockNode); fixture.componentRef.setInput('weight', 1); - fixture.componentRef.setInput('label', { literalString: 'Accept Terms' }); - fixture.componentRef.setInput('value', { literalBoolean: false }); + fixture.componentRef.setInput('label', {literalString: 'Accept Terms'}); + fixture.componentRef.setInput('value', {literalBoolean: false}); fixture.detectChanges(); }); @@ -76,7 +76,7 @@ describe('Checkbox Component', () => { const inputEl = fixture.nativeElement.querySelector('input'); expect(inputEl.checked).toBeFalse(); - fixture.componentRef.setInput('value', { literalBoolean: true }); + fixture.componentRef.setInput('value', {literalBoolean: true}); fixture.detectChanges(); expect(inputEl.checked).toBeTrue(); }); diff --git a/renderers/angular/src/v0_8/components/checkbox.ts b/renderers/angular/src/v0_8/components/checkbox.ts index 1650de704..eba7cdb6f 100644 --- a/renderers/angular/src/v0_8/components/checkbox.ts +++ b/renderers/angular/src/v0_8/components/checkbox.ts @@ -14,9 +14,9 @@ * limitations under the License. */ -import { ChangeDetectionStrategy, Component, computed, input } from '@angular/core'; -import { DynamicComponent } from '../rendering/dynamic-component'; -import type { BooleanValue, CheckboxNode, StringValue } from '../types'; +import {ChangeDetectionStrategy, Component, computed, input} from '@angular/core'; +import {DynamicComponent} from '../rendering/dynamic-component'; +import type {BooleanValue, CheckboxNode, StringValue} from '../types'; @Component({ selector: 'a2ui-checkbox', @@ -66,14 +66,14 @@ export class Checkbox extends DynamicComponent { checkedNode.path as string, this.component().dataContextPath, ), - contents: [{ key: '.', valueBoolean: checked }], + contents: [{key: '.', valueBoolean: checked}], }, }, ]); } else { this.sendAction({ name: 'toggle', - context: [{ key: 'checked', value: { literalBoolean: checked } }], + context: [{key: 'checked', value: {literalBoolean: checked}}], }); } } diff --git a/renderers/angular/src/v0_8/components/column.spec.ts b/renderers/angular/src/v0_8/components/column.spec.ts index eb872f38f..9e954d5c1 100644 --- a/renderers/angular/src/v0_8/components/column.spec.ts +++ b/renderers/angular/src/v0_8/components/column.spec.ts @@ -14,13 +14,13 @@ * limitations under the License. */ -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { Column } from './column'; -import { MessageProcessor } from '../data/processor'; -import { Theme } from '../rendering/theming'; -import { Catalog } from '../rendering/catalog'; -import type { ColumnNode } from '../types'; -import { Directive, Input, ChangeDetectionStrategy } from '@angular/core'; +import {ComponentFixture, TestBed} from '@angular/core/testing'; +import {Column} from './column'; +import {MessageProcessor} from '../data/processor'; +import {Theme} from '../rendering/theming'; +import {Catalog} from '../rendering/catalog'; +import type {ColumnNode} from '../types'; +import {Directive, Input, ChangeDetectionStrategy} from '@angular/core'; @Directive({ selector: '[a2ui-renderer]', @@ -46,20 +46,20 @@ describe('Column Component', () => { type: 'Column', weight: 1, properties: { - children: [{ id: 'child-1', type: 'Text', properties: {} }], + children: [{id: 'child-1', type: 'Text', properties: {}}], }, }; beforeEach(async () => { mockTheme = new Theme(); - mockTheme.components = { Column: { 'custom-col': true } } as any; + mockTheme.components = {Column: {'custom-col': true}} as any; await TestBed.configureTestingModule({ imports: [Column], providers: [ - { provide: MessageProcessor, useValue: {} }, - { provide: Theme, useValue: mockTheme }, - { provide: Catalog, useValue: {} }, + {provide: MessageProcessor, useValue: {}}, + {provide: Theme, useValue: mockTheme}, + {provide: Catalog, useValue: {}}, ], }) .overrideComponent(Column, { diff --git a/renderers/angular/src/v0_8/components/column.ts b/renderers/angular/src/v0_8/components/column.ts index 1e0218bb0..a8b2b99c7 100644 --- a/renderers/angular/src/v0_8/components/column.ts +++ b/renderers/angular/src/v0_8/components/column.ts @@ -14,10 +14,10 @@ * limitations under the License. */ -import { ChangeDetectionStrategy, Component, computed, input } from '@angular/core'; -import type { AnyComponentNode, ColumnNode, ResolvedColumn } from '../types'; -import { DynamicComponent } from '../rendering/dynamic-component'; -import { Renderer } from '../rendering/renderer'; +import {ChangeDetectionStrategy, Component, computed, input} from '@angular/core'; +import type {AnyComponentNode, ColumnNode, ResolvedColumn} from '../types'; +import {DynamicComponent} from '../rendering/dynamic-component'; +import {Renderer} from '../rendering/renderer'; @Component({ selector: 'a2ui-column', diff --git a/renderers/angular/src/v0_8/components/datetime-input.spec.ts b/renderers/angular/src/v0_8/components/datetime-input.spec.ts index 43f5bd047..205efec48 100644 --- a/renderers/angular/src/v0_8/components/datetime-input.spec.ts +++ b/renderers/angular/src/v0_8/components/datetime-input.spec.ts @@ -14,13 +14,13 @@ * limitations under the License. */ -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { DateTimeInput } from './datetime-input'; -import type { A2UIClientEventMessage, DateTimeInputNode } from '../types'; -import { Theme } from '../rendering/theming'; -import { ChangeDetectionStrategy } from '@angular/core'; -import { MessageProcessor } from '../data/processor'; -import { Catalog } from '../rendering/catalog'; +import {ComponentFixture, TestBed} from '@angular/core/testing'; +import {DateTimeInput} from './datetime-input'; +import type {A2UIClientEventMessage, DateTimeInputNode} from '../types'; +import {Theme} from '../rendering/theming'; +import {ChangeDetectionStrategy} from '@angular/core'; +import {MessageProcessor} from '../data/processor'; +import {Catalog} from '../rendering/catalog'; describe('DateTimeInput Component', () => { let component: DateTimeInput; @@ -33,7 +33,7 @@ describe('DateTimeInput Component', () => { type: 'DateTimeInput', weight: 1, properties: { - value: { literalString: '2023-10-27' }, + value: {literalString: '2023-10-27'}, }, }; @@ -44,22 +44,22 @@ describe('DateTimeInput Component', () => { mockTheme = new Theme(); mockTheme.components = { DateTimeInput: { - container: { 'dt-container': true }, - label: { 'dt-label': true }, - element: { 'dt-element': true }, + container: {'dt-container': true}, + label: {'dt-label': true}, + element: {'dt-element': true}, }, } as any; await TestBed.configureTestingModule({ imports: [DateTimeInput], providers: [ - { provide: MessageProcessor, useValue: mockProcessor }, - { provide: Theme, useValue: mockTheme }, - { provide: Catalog, useValue: {} }, + {provide: MessageProcessor, useValue: mockProcessor}, + {provide: Theme, useValue: mockTheme}, + {provide: Catalog, useValue: {}}, ], }) .overrideComponent(DateTimeInput, { - set: { changeDetection: ChangeDetectionStrategy.Default }, + set: {changeDetection: ChangeDetectionStrategy.Default}, }) .compileComponents(); @@ -70,7 +70,7 @@ describe('DateTimeInput Component', () => { fixture.componentRef.setInput('component', mockDatetimeNode); fixture.componentRef.setInput('weight', 1); fixture.componentRef.setInput('value', mockDatetimeNode.properties.value); - fixture.componentRef.setInput('label', { literalString: 'Select Date' }); + fixture.componentRef.setInput('label', {literalString: 'Select Date'}); fixture.detectChanges(); }); @@ -118,7 +118,7 @@ describe('DateTimeInput Component', () => { expect(message.userAction!.name).toBe('change'); // Verify context - expect(message.userAction!.context).toEqual({ value: '2023-10-28' }); + expect(message.userAction!.context).toEqual({value: '2023-10-28'}); }); it('should apply theme classes', () => { diff --git a/renderers/angular/src/v0_8/components/datetime-input.ts b/renderers/angular/src/v0_8/components/datetime-input.ts index 7d9224105..8b10ac544 100644 --- a/renderers/angular/src/v0_8/components/datetime-input.ts +++ b/renderers/angular/src/v0_8/components/datetime-input.ts @@ -14,9 +14,9 @@ * limitations under the License. */ -import { ChangeDetectionStrategy, Component, computed, input } from '@angular/core'; -import { DynamicComponent } from '../rendering/dynamic-component'; -import type { DateTimeInputNode, StringValue } from '../types'; +import {ChangeDetectionStrategy, Component, computed, input} from '@angular/core'; +import {DynamicComponent} from '../rendering/dynamic-component'; +import type {DateTimeInputNode, StringValue} from '../types'; @Component({ selector: 'a2ui-datetime-input', @@ -74,12 +74,12 @@ export class DateTimeInput extends DynamicComponent { valueNode.path as string, this.component().dataContextPath, ), - contents: [{ key: '.', valueString: value }], + contents: [{key: '.', valueString: value}], }, }, ]); } else { - this.handleAction('change', { value }); + this.handleAction('change', {value}); } } @@ -88,7 +88,7 @@ export class DateTimeInput extends DynamicComponent { name, context: Object.entries(context).map(([key, val]) => ({ key, - value: typeof val === 'number' ? { literalNumber: val } : { literalString: String(val) }, + value: typeof val === 'number' ? {literalNumber: val} : {literalString: String(val)}, })), }); } diff --git a/renderers/angular/src/v0_8/components/divider.spec.ts b/renderers/angular/src/v0_8/components/divider.spec.ts index 7533e473b..f57b83321 100644 --- a/renderers/angular/src/v0_8/components/divider.spec.ts +++ b/renderers/angular/src/v0_8/components/divider.spec.ts @@ -14,11 +14,11 @@ * limitations under the License. */ -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { Divider } from './divider'; -import { Theme } from '../rendering/theming'; -import { MessageProcessor } from '../data/processor'; -import type { DividerNode } from '../types'; +import {ComponentFixture, TestBed} from '@angular/core/testing'; +import {Divider} from './divider'; +import {Theme} from '../rendering/theming'; +import {MessageProcessor} from '../data/processor'; +import type {DividerNode} from '../types'; describe('Divider Component', () => { let component: Divider; @@ -34,13 +34,13 @@ describe('Divider Component', () => { beforeEach(async () => { mockTheme = new Theme(); - mockTheme.components = { Divider: 'divider-class' } as any; + mockTheme.components = {Divider: 'divider-class'} as any; await TestBed.configureTestingModule({ imports: [Divider], providers: [ - { provide: Theme, useValue: mockTheme }, - { provide: MessageProcessor, useValue: {} }, + {provide: Theme, useValue: mockTheme}, + {provide: MessageProcessor, useValue: {}}, ], }).compileComponents(); diff --git a/renderers/angular/src/v0_8/components/divider.ts b/renderers/angular/src/v0_8/components/divider.ts index 56152205c..e04b49ed7 100644 --- a/renderers/angular/src/v0_8/components/divider.ts +++ b/renderers/angular/src/v0_8/components/divider.ts @@ -14,9 +14,9 @@ * limitations under the License. */ -import { ChangeDetectionStrategy, Component } from '@angular/core'; -import type { DividerNode } from '../types'; -import { DynamicComponent } from '../rendering/dynamic-component'; +import {ChangeDetectionStrategy, Component} from '@angular/core'; +import type {DividerNode} from '../types'; +import {DynamicComponent} from '../rendering/dynamic-component'; @Component({ selector: 'a2ui-divider', diff --git a/renderers/angular/src/v0_8/components/icon.spec.ts b/renderers/angular/src/v0_8/components/icon.spec.ts index 8e481cba0..f5ba56a9f 100644 --- a/renderers/angular/src/v0_8/components/icon.spec.ts +++ b/renderers/angular/src/v0_8/components/icon.spec.ts @@ -14,12 +14,12 @@ * limitations under the License. */ -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { Icon } from './icon'; -import { MessageProcessor } from '../data/processor'; -import { Theme } from '../rendering/theming'; -import { Catalog } from '../rendering/catalog'; -import { By } from '@angular/platform-browser'; +import {ComponentFixture, TestBed} from '@angular/core/testing'; +import {Icon} from './icon'; +import {MessageProcessor} from '../data/processor'; +import {Theme} from '../rendering/theming'; +import {Catalog} from '../rendering/catalog'; +import {By} from '@angular/platform-browser'; describe('Icon Component', () => { let component: Icon; @@ -28,17 +28,17 @@ describe('Icon Component', () => { beforeEach(async () => { mockTheme = new Theme(); - mockTheme.components = { Icon: 'icon-class' } as any; + mockTheme.components = {Icon: 'icon-class'} as any; await TestBed.configureTestingModule({ imports: [Icon], providers: [ { provide: MessageProcessor, - useValue: { resolvePrimitive: (p: any) => p?.literalString || p }, + useValue: {resolvePrimitive: (p: any) => p?.literalString || p}, }, - { provide: Theme, useValue: mockTheme }, - { provide: Catalog, useValue: {} }, + {provide: Theme, useValue: mockTheme}, + {provide: Catalog, useValue: {}}, ], }).compileComponents(); @@ -46,7 +46,7 @@ describe('Icon Component', () => { component = fixture.componentInstance; fixture.componentRef.setInput('surfaceId', 'surface-1'); - fixture.componentRef.setInput('component', { id: 'icon-1', type: 'Icon', weight: 1 }); + fixture.componentRef.setInput('component', {id: 'icon-1', type: 'Icon', weight: 1}); fixture.componentRef.setInput('weight', 1); fixture.componentRef.setInput('name', null); }); @@ -56,7 +56,7 @@ describe('Icon Component', () => { }); it('should render icon name inside span if provided', () => { - fixture.componentRef.setInput('name', { literalString: 'home' }); + fixture.componentRef.setInput('name', {literalString: 'home'}); fixture.detectChanges(); const spanEl = fixture.debugElement.query(By.css('.g-icon')); diff --git a/renderers/angular/src/v0_8/components/icon.ts b/renderers/angular/src/v0_8/components/icon.ts index 4aeee2829..757c1114e 100644 --- a/renderers/angular/src/v0_8/components/icon.ts +++ b/renderers/angular/src/v0_8/components/icon.ts @@ -14,9 +14,9 @@ * limitations under the License. */ -import { ChangeDetectionStrategy, Component, computed, input } from '@angular/core'; -import { DynamicComponent } from '../rendering/dynamic-component'; -import type { IconNode, StringValue } from '../types'; +import {ChangeDetectionStrategy, Component, computed, input} from '@angular/core'; +import {DynamicComponent} from '../rendering/dynamic-component'; +import type {IconNode, StringValue} from '../types'; @Component({ selector: 'a2ui-icon', diff --git a/renderers/angular/src/v0_8/components/image.spec.ts b/renderers/angular/src/v0_8/components/image.spec.ts index b314a96aa..162637f35 100644 --- a/renderers/angular/src/v0_8/components/image.spec.ts +++ b/renderers/angular/src/v0_8/components/image.spec.ts @@ -2,12 +2,12 @@ * Copyright 2026 Google LLC */ -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { Image } from './image'; -import { MessageProcessor } from '../data/processor'; -import { Theme } from '../rendering/theming'; -import { Catalog } from '../rendering/catalog'; -import { By } from '@angular/platform-browser'; +import {ComponentFixture, TestBed} from '@angular/core/testing'; +import {Image} from './image'; +import {MessageProcessor} from '../data/processor'; +import {Theme} from '../rendering/theming'; +import {Catalog} from '../rendering/catalog'; +import {By} from '@angular/platform-browser'; describe('Image Component', () => { let component: Image; @@ -18,17 +18,17 @@ describe('Image Component', () => { mockTheme = new Theme(); mockTheme.components = { Image: { - all: { 'image-all-class': true }, - Avatar: { 'image-avatar-class': true }, + all: {'image-all-class': true}, + Avatar: {'image-avatar-class': true}, }, } as any; await TestBed.configureTestingModule({ imports: [Image], providers: [ - { provide: MessageProcessor, useValue: { resolvePrimitive: (p: any) => p?.value || p } }, - { provide: Theme, useValue: mockTheme }, - { provide: Catalog, useValue: {} }, + {provide: MessageProcessor, useValue: {resolvePrimitive: (p: any) => p?.value || p}}, + {provide: Theme, useValue: mockTheme}, + {provide: Catalog, useValue: {}}, ], }).compileComponents(); @@ -37,7 +37,7 @@ describe('Image Component', () => { // Set required inputs fixture.componentRef.setInput('surfaceId', 'surf-1'); - fixture.componentRef.setInput('component', { id: 'img-1', type: 'Image', weight: 1 }); + fixture.componentRef.setInput('component', {id: 'img-1', type: 'Image', weight: 1}); fixture.componentRef.setInput('weight', 1); fixture.componentRef.setInput('usageHint', null); }); @@ -47,7 +47,7 @@ describe('Image Component', () => { }); it('should render if url is provided', () => { - fixture.componentRef.setInput('url', { literalString: 'http://example.com/a.png' }); + fixture.componentRef.setInput('url', {literalString: 'http://example.com/a.png'}); fixture.detectChanges(); const imgEl = fixture.debugElement.query(By.css('img')); @@ -60,8 +60,8 @@ describe('Image Component', () => { }); it('should render with altText if provided', () => { - fixture.componentRef.setInput('url', { literalString: 'http://example.com/a.png' }); - fixture.componentRef.setInput('altText', { literalString: 'A beautiful sunset' }); + fixture.componentRef.setInput('url', {literalString: 'http://example.com/a.png'}); + fixture.componentRef.setInput('altText', {literalString: 'A beautiful sunset'}); fixture.detectChanges(); const imgEl = fixture.debugElement.query(By.css('img')); @@ -78,7 +78,7 @@ describe('Image Component', () => { }); it('should apply usageHint class if provided', () => { - fixture.componentRef.setInput('url', { literalString: 'http://example.com/a.png' }); + fixture.componentRef.setInput('url', {literalString: 'http://example.com/a.png'}); fixture.componentRef.setInput('usageHint', 'Avatar'); fixture.detectChanges(); diff --git a/renderers/angular/src/v0_8/components/image.ts b/renderers/angular/src/v0_8/components/image.ts index 297c32945..de633740e 100644 --- a/renderers/angular/src/v0_8/components/image.ts +++ b/renderers/angular/src/v0_8/components/image.ts @@ -14,11 +14,11 @@ * limitations under the License. */ -import { ChangeDetectionStrategy, Component, computed, input } from '@angular/core'; +import {ChangeDetectionStrategy, Component, computed, input} from '@angular/core'; import * as Primitives from '@a2ui/web_core/types/primitives'; import * as Styles from '@a2ui/web_core/styles/index'; -import type { ImageNode, ResolvedImage } from '../types'; -import { DynamicComponent } from '../rendering/dynamic-component'; +import type {ImageNode, ResolvedImage} from '../types'; +import {DynamicComponent} from '../rendering/dynamic-component'; @Component({ selector: 'a2ui-image', diff --git a/renderers/angular/src/v0_8/components/list.spec.ts b/renderers/angular/src/v0_8/components/list.spec.ts index 54f46b5e8..cd79042a0 100644 --- a/renderers/angular/src/v0_8/components/list.spec.ts +++ b/renderers/angular/src/v0_8/components/list.spec.ts @@ -14,14 +14,14 @@ * limitations under the License. */ -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { List } from './list'; -import { MessageProcessor } from '../data/processor'; -import { Theme } from '../rendering/theming'; -import { Catalog } from '../rendering/catalog'; -import type { ListNode } from '../types'; -import { Directive, Input, ChangeDetectionStrategy } from '@angular/core'; -import { By } from '@angular/platform-browser'; +import {ComponentFixture, TestBed} from '@angular/core/testing'; +import {List} from './list'; +import {MessageProcessor} from '../data/processor'; +import {Theme} from '../rendering/theming'; +import {Catalog} from '../rendering/catalog'; +import type {ListNode} from '../types'; +import {Directive, Input, ChangeDetectionStrategy} from '@angular/core'; +import {By} from '@angular/platform-browser'; @Directive({ selector: '[a2ui-renderer]', @@ -47,20 +47,20 @@ describe('List Component', () => { type: 'List', weight: 1, properties: { - children: [{ id: 'child-1', type: 'Text', properties: {} }], + children: [{id: 'child-1', type: 'Text', properties: {}}], }, }; beforeEach(async () => { mockTheme = new Theme(); - mockTheme.components = { List: 'list-class' } as any; + mockTheme.components = {List: 'list-class'} as any; await TestBed.configureTestingModule({ imports: [List], providers: [ - { provide: MessageProcessor, useValue: {} }, - { provide: Theme, useValue: mockTheme }, - { provide: Catalog, useValue: {} }, + {provide: MessageProcessor, useValue: {}}, + {provide: Theme, useValue: mockTheme}, + {provide: Catalog, useValue: {}}, ], }) .overrideComponent(List, { diff --git a/renderers/angular/src/v0_8/components/list.ts b/renderers/angular/src/v0_8/components/list.ts index 53a56ba6d..c0ad5f01a 100644 --- a/renderers/angular/src/v0_8/components/list.ts +++ b/renderers/angular/src/v0_8/components/list.ts @@ -14,10 +14,10 @@ * limitations under the License. */ -import { ChangeDetectionStrategy, Component, input } from '@angular/core'; -import type { AnyComponentNode, ListNode, ResolvedList } from '../types'; -import { DynamicComponent } from '../rendering/dynamic-component'; -import { Renderer } from '../rendering/renderer'; +import {ChangeDetectionStrategy, Component, input} from '@angular/core'; +import type {AnyComponentNode, ListNode, ResolvedList} from '../types'; +import {DynamicComponent} from '../rendering/dynamic-component'; +import {Renderer} from '../rendering/renderer'; @Component({ selector: 'a2ui-list', diff --git a/renderers/angular/src/v0_8/components/modal.spec.ts b/renderers/angular/src/v0_8/components/modal.spec.ts index 7d6465831..330b1a4b2 100644 --- a/renderers/angular/src/v0_8/components/modal.spec.ts +++ b/renderers/angular/src/v0_8/components/modal.spec.ts @@ -14,14 +14,14 @@ * limitations under the License. */ -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { Modal } from './modal'; -import { MessageProcessor } from '../data/processor'; -import { Theme } from '../rendering/theming'; -import { Catalog } from '../rendering/catalog'; -import type { AnyComponentNode } from '../types'; -import { Directive, Input, ChangeDetectionStrategy } from '@angular/core'; -import { By } from '@angular/platform-browser'; +import {ComponentFixture, TestBed} from '@angular/core/testing'; +import {Modal} from './modal'; +import {MessageProcessor} from '../data/processor'; +import {Theme} from '../rendering/theming'; +import {Catalog} from '../rendering/catalog'; +import type {AnyComponentNode} from '../types'; +import {Directive, Input, ChangeDetectionStrategy} from '@angular/core'; +import {By} from '@angular/platform-browser'; @Directive({ selector: '[a2ui-renderer]', @@ -45,12 +45,12 @@ describe('Modal Component', () => { const mockEntryPoint: AnyComponentNode = { id: 'btn-1', type: 'Button', - properties: { text: 'Open' }, + properties: {text: 'Open'}, }; const mockContent: AnyComponentNode = { id: 'text-1', type: 'Text', - properties: { text: 'Hello' }, + properties: {text: 'Hello'}, }; beforeEach(async () => { @@ -65,9 +65,9 @@ describe('Modal Component', () => { await TestBed.configureTestingModule({ imports: [Modal], providers: [ - { provide: MessageProcessor, useValue: {} }, - { provide: Theme, useValue: mockTheme }, - { provide: Catalog, useValue: {} }, + {provide: MessageProcessor, useValue: {}}, + {provide: Theme, useValue: mockTheme}, + {provide: Catalog, useValue: {}}, ], }) .overrideComponent(Modal, { @@ -86,7 +86,7 @@ describe('Modal Component', () => { fixture.componentRef.setInput('surfaceId', 'surface-1'); fixture.componentRef.setInput('entryPointChild', mockEntryPoint); fixture.componentRef.setInput('contentChild', mockContent); - fixture.componentRef.setInput('component', { id: 'modal-1', type: 'Modal', weight: 1 }); + fixture.componentRef.setInput('component', {id: 'modal-1', type: 'Modal', weight: 1}); fixture.componentRef.setInput('weight', 1); fixture.detectChanges(); diff --git a/renderers/angular/src/v0_8/components/modal.ts b/renderers/angular/src/v0_8/components/modal.ts index 8d2872bcc..1e0e62953 100644 --- a/renderers/angular/src/v0_8/components/modal.ts +++ b/renderers/angular/src/v0_8/components/modal.ts @@ -14,10 +14,10 @@ * limitations under the License. */ -import { ChangeDetectionStrategy, Component, input, signal } from '@angular/core'; -import { DynamicComponent } from '../rendering/dynamic-component'; -import { Renderer } from '../rendering/renderer'; -import type { AnyComponentNode, ModalNode } from '../types'; +import {ChangeDetectionStrategy, Component, input, signal} from '@angular/core'; +import {DynamicComponent} from '../rendering/dynamic-component'; +import {Renderer} from '../rendering/renderer'; +import type {AnyComponentNode, ModalNode} from '../types'; @Component({ selector: 'a2ui-modal', diff --git a/renderers/angular/src/v0_8/components/multiple-choice.spec.ts b/renderers/angular/src/v0_8/components/multiple-choice.spec.ts index 767323fea..68465d6f9 100644 --- a/renderers/angular/src/v0_8/components/multiple-choice.spec.ts +++ b/renderers/angular/src/v0_8/components/multiple-choice.spec.ts @@ -14,13 +14,13 @@ * limitations under the License. */ -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { MultipleChoice } from './multiple-choice'; -import { MessageProcessor } from '../data/processor'; -import { Theme } from '../rendering/theming'; -import { Catalog } from '../rendering/catalog'; -import { By } from '@angular/platform-browser'; -import { ChangeDetectionStrategy } from '@angular/core'; +import {ComponentFixture, TestBed} from '@angular/core/testing'; +import {MultipleChoice} from './multiple-choice'; +import {MessageProcessor} from '../data/processor'; +import {Theme} from '../rendering/theming'; +import {Catalog} from '../rendering/catalog'; +import {By} from '@angular/platform-browser'; +import {ChangeDetectionStrategy} from '@angular/core'; describe('MultipleChoice Component', () => { let component: MultipleChoice; @@ -29,8 +29,8 @@ describe('MultipleChoice Component', () => { let mockProcessor: jasmine.SpyObj; const mockOptions = [ - { label: { literalString: 'Option 1' } as any, value: 'opt1' }, - { label: { literalString: 'Option 2' } as any, value: 'opt2' }, + {label: {literalString: 'Option 1'} as any, value: 'opt1'}, + {label: {literalString: 'Option 2'} as any, value: 'opt2'}, ]; beforeEach(async () => { @@ -53,9 +53,9 @@ describe('MultipleChoice Component', () => { await TestBed.configureTestingModule({ imports: [MultipleChoice], providers: [ - { provide: MessageProcessor, useValue: mockProcessor }, - { provide: Theme, useValue: mockTheme }, - { provide: Catalog, useValue: {} }, + {provide: MessageProcessor, useValue: mockProcessor}, + {provide: Theme, useValue: mockTheme}, + {provide: Catalog, useValue: {}}, ], }) .overrideComponent(MultipleChoice, { @@ -69,11 +69,11 @@ describe('MultipleChoice Component', () => { component = fixture.componentInstance; fixture.componentRef.setInput('surfaceId', 'surface-1'); - fixture.componentRef.setInput('component', { id: 'mc-1', type: 'MultipleChoice', weight: 1 }); + fixture.componentRef.setInput('component', {id: 'mc-1', type: 'MultipleChoice', weight: 1}); fixture.componentRef.setInput('weight', 1); - fixture.componentRef.setInput('label', { literalString: 'Select an option' }); + fixture.componentRef.setInput('label', {literalString: 'Select an option'}); fixture.componentRef.setInput('options', mockOptions); - fixture.componentRef.setInput('selections', { literalArray: ['opt1'] }); + fixture.componentRef.setInput('selections', {literalArray: ['opt1']}); fixture.detectChanges(); }); @@ -107,6 +107,6 @@ describe('MultipleChoice Component', () => { const message = mockProcessor.dispatch.calls.mostRecent().args[0]; expect(message.userAction).toBeTruthy(); expect(message.userAction!.name).toBe('change'); - expect(message.userAction!.context).toEqual({ value: 'opt2' }); + expect(message.userAction!.context).toEqual({value: 'opt2'}); }); }); diff --git a/renderers/angular/src/v0_8/components/multiple-choice.ts b/renderers/angular/src/v0_8/components/multiple-choice.ts index bced5a576..b6981430a 100644 --- a/renderers/angular/src/v0_8/components/multiple-choice.ts +++ b/renderers/angular/src/v0_8/components/multiple-choice.ts @@ -14,9 +14,9 @@ * limitations under the License. */ -import { ChangeDetectionStrategy, Component, computed, input } from '@angular/core'; -import { DynamicComponent } from '../rendering/dynamic-component'; -import type { AnyComponentNode, MultipleChoiceNode, StringValue } from '../types'; +import {ChangeDetectionStrategy, Component, computed, input} from '@angular/core'; +import {DynamicComponent} from '../rendering/dynamic-component'; +import type {AnyComponentNode, MultipleChoiceNode, StringValue} from '../types'; @Component({ selector: 'a2ui-multiple-choice', @@ -49,7 +49,7 @@ import type { AnyComponentNode, MultipleChoiceNode, StringValue } from '../types }) export class MultipleChoice extends DynamicComponent { readonly label = input(null); - readonly options = input.required<{ label: StringValue; value: string }[]>(); + readonly options = input.required<{label: StringValue; value: string}[]>(); readonly selections = input.required(); protected readonly selectId = super.getUniqueId('a2ui-multiple-choice'); @@ -57,7 +57,7 @@ export class MultipleChoice extends DynamicComponent { protected readonly resolvedLabel = computed(() => this.resolvePrimitive(this.label())); protected readonly resolvedOptions = computed(() => - this.options().map((opt) => ({ + this.options().map(opt => ({ label: this.resolvePrimitive(opt.label), value: opt.value, })), @@ -89,12 +89,12 @@ export class MultipleChoice extends DynamicComponent { selectionsNode.path as string, this.component().dataContextPath, ), - contents: [{ key: '.', valueString: JSON.stringify({ literalArray: [value] }) }], + contents: [{key: '.', valueString: JSON.stringify({literalArray: [value]})}], }, }, ]); } else { - this.handleAction('change', { value }); + this.handleAction('change', {value}); } } @@ -103,7 +103,7 @@ export class MultipleChoice extends DynamicComponent { name, context: Object.entries(context).map(([key, val]) => ({ key, - value: typeof val === 'number' ? { literalNumber: val } : { literalString: String(val) }, + value: typeof val === 'number' ? {literalNumber: val} : {literalString: String(val)}, })), }); } diff --git a/renderers/angular/src/v0_8/components/row-integration.spec.ts b/renderers/angular/src/v0_8/components/row-integration.spec.ts index 536d54425..05c120ed8 100644 --- a/renderers/angular/src/v0_8/components/row-integration.spec.ts +++ b/renderers/angular/src/v0_8/components/row-integration.spec.ts @@ -14,14 +14,14 @@ * limitations under the License. */ -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { Renderer } from '../rendering/renderer'; -import { Catalog } from '../rendering/catalog'; -import { DEFAULT_CATALOG } from '../catalog'; -import { Theme } from '../rendering/theming'; -import { MessageProcessor } from '../data/processor'; -import { MarkdownRenderer } from '../data/markdown'; -import { Component } from '@angular/core'; +import {ComponentFixture, TestBed} from '@angular/core/testing'; +import {Renderer} from '../rendering/renderer'; +import {Catalog} from '../rendering/catalog'; +import {DEFAULT_CATALOG} from '../catalog'; +import {Theme} from '../rendering/theming'; +import {MessageProcessor} from '../data/processor'; +import {MarkdownRenderer} from '../data/markdown'; +import {Component} from '@angular/core'; @Component({ template: ` { await TestBed.configureTestingModule({ imports: [TestHost], providers: [ - { provide: MessageProcessor, useValue: processor }, - { provide: Catalog, useValue: DEFAULT_CATALOG }, + {provide: MessageProcessor, useValue: processor}, + {provide: Catalog, useValue: DEFAULT_CATALOG}, { provide: MarkdownRenderer, useValue: { @@ -67,8 +67,8 @@ describe('Row Component Integration (Real Renderer)', () => { const theme = TestBed.inject(Theme); theme.update({ components: { - Row: { 'a2ui-row': true }, - Text: { all: { 'a2ui-text': true } }, + Row: {'a2ui-row': true}, + Text: {all: {'a2ui-text': true}}, } as any, elements: {} as any, markdown: { @@ -99,12 +99,12 @@ describe('Row Component Integration (Real Renderer)', () => { { type: 'Text', id: 'child-1', - properties: { text: { literalString: 'Child 1' } }, + properties: {text: {literalString: 'Child 1'}}, }, { type: 'Text', id: 'child-2', - properties: { text: { literalString: 'Child 2' } }, + properties: {text: {literalString: 'Child 2'}}, }, ], }, diff --git a/renderers/angular/src/v0_8/components/row.spec.ts b/renderers/angular/src/v0_8/components/row.spec.ts index a021c0091..dd30d6bbd 100644 --- a/renderers/angular/src/v0_8/components/row.spec.ts +++ b/renderers/angular/src/v0_8/components/row.spec.ts @@ -14,13 +14,13 @@ * limitations under the License. */ -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { Row } from './row'; -import { MessageProcessor } from '../data/processor'; -import { Theme } from '../rendering/theming'; -import { Catalog } from '../rendering/catalog'; -import type { RowNode } from '../types'; -import { Directive, Input, ChangeDetectionStrategy } from '@angular/core'; +import {ComponentFixture, TestBed} from '@angular/core/testing'; +import {Row} from './row'; +import {MessageProcessor} from '../data/processor'; +import {Theme} from '../rendering/theming'; +import {Catalog} from '../rendering/catalog'; +import type {RowNode} from '../types'; +import {Directive, Input, ChangeDetectionStrategy} from '@angular/core'; @Directive({ selector: '[a2ui-renderer]', @@ -46,20 +46,20 @@ describe('Row Component', () => { type: 'Row', weight: 1, properties: { - children: [{ id: 'child-1', type: 'Text', properties: {} }], + children: [{id: 'child-1', type: 'Text', properties: {}}], }, }; beforeEach(async () => { mockTheme = new Theme(); - mockTheme.components = { Row: { 'custom-row': true } } as any; + mockTheme.components = {Row: {'custom-row': true}} as any; await TestBed.configureTestingModule({ imports: [Row], providers: [ - { provide: MessageProcessor, useValue: {} }, - { provide: Theme, useValue: mockTheme }, - { provide: Catalog, useValue: {} }, + {provide: MessageProcessor, useValue: {}}, + {provide: Theme, useValue: mockTheme}, + {provide: Catalog, useValue: {}}, ], }) .overrideComponent(Row, { diff --git a/renderers/angular/src/v0_8/components/row.ts b/renderers/angular/src/v0_8/components/row.ts index 2c45e2cd3..e4348f747 100644 --- a/renderers/angular/src/v0_8/components/row.ts +++ b/renderers/angular/src/v0_8/components/row.ts @@ -14,10 +14,10 @@ * limitations under the License. */ -import { ChangeDetectionStrategy, Component, computed, input } from '@angular/core'; -import { DynamicComponent } from '../rendering/dynamic-component'; -import { Renderer } from '../rendering/renderer'; -import type { AnyComponentNode, ResolvedRow, RowNode } from '../types'; +import {ChangeDetectionStrategy, Component, computed, input} from '@angular/core'; +import {DynamicComponent} from '../rendering/dynamic-component'; +import {Renderer} from '../rendering/renderer'; +import type {AnyComponentNode, ResolvedRow, RowNode} from '../types'; @Component({ selector: 'a2ui-row', diff --git a/renderers/angular/src/v0_8/components/slider.spec.ts b/renderers/angular/src/v0_8/components/slider.spec.ts index f470af148..1fc2bc08a 100644 --- a/renderers/angular/src/v0_8/components/slider.spec.ts +++ b/renderers/angular/src/v0_8/components/slider.spec.ts @@ -14,13 +14,13 @@ * limitations under the License. */ -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { Slider } from './slider'; -import { MessageProcessor } from '../data/processor'; -import { Theme } from '../rendering/theming'; -import { Catalog } from '../rendering/catalog'; -import { By } from '@angular/platform-browser'; -import { ChangeDetectionStrategy } from '@angular/core'; +import {ComponentFixture, TestBed} from '@angular/core/testing'; +import {Slider} from './slider'; +import {MessageProcessor} from '../data/processor'; +import {Theme} from '../rendering/theming'; +import {Catalog} from '../rendering/catalog'; +import {By} from '@angular/platform-browser'; +import {ChangeDetectionStrategy} from '@angular/core'; describe('Slider Component', () => { let component: Slider; @@ -48,9 +48,9 @@ describe('Slider Component', () => { await TestBed.configureTestingModule({ imports: [Slider], providers: [ - { provide: MessageProcessor, useValue: mockProcessor }, - { provide: Theme, useValue: mockTheme }, - { provide: Catalog, useValue: {} }, + {provide: MessageProcessor, useValue: mockProcessor}, + {provide: Theme, useValue: mockTheme}, + {provide: Catalog, useValue: {}}, ], }) .overrideComponent(Slider, { @@ -64,10 +64,10 @@ describe('Slider Component', () => { component = fixture.componentInstance; fixture.componentRef.setInput('surfaceId', 'surface-1'); - fixture.componentRef.setInput('component', { id: 'slider-1', type: 'Slider', weight: 1 }); + fixture.componentRef.setInput('component', {id: 'slider-1', type: 'Slider', weight: 1}); fixture.componentRef.setInput('weight', 1); - fixture.componentRef.setInput('label', { literalString: 'Volume' }); - fixture.componentRef.setInput('value', { literalNumber: 50 }); + fixture.componentRef.setInput('label', {literalString: 'Volume'}); + fixture.componentRef.setInput('value', {literalNumber: 50}); fixture.componentRef.setInput('minValue', 0); fixture.componentRef.setInput('maxValue', 100); @@ -102,6 +102,6 @@ describe('Slider Component', () => { const message = mockProcessor.dispatch.calls.mostRecent().args[0]; expect(message.userAction).toBeTruthy(); expect(message.userAction!.name).toBe('change'); - expect(message.userAction!.context).toEqual({ value: 75 }); + expect(message.userAction!.context).toEqual({value: 75}); }); }); diff --git a/renderers/angular/src/v0_8/components/slider.ts b/renderers/angular/src/v0_8/components/slider.ts index 42d8c625c..14e6807b3 100644 --- a/renderers/angular/src/v0_8/components/slider.ts +++ b/renderers/angular/src/v0_8/components/slider.ts @@ -14,9 +14,9 @@ * limitations under the License. */ -import { ChangeDetectionStrategy, Component, computed, input } from '@angular/core'; -import type { NumberValue, SliderNode, StringValue } from '../types'; -import { DynamicComponent } from '../rendering/dynamic-component'; +import {ChangeDetectionStrategy, Component, computed, input} from '@angular/core'; +import type {NumberValue, SliderNode, StringValue} from '../types'; +import {DynamicComponent} from '../rendering/dynamic-component'; @Component({ selector: 'a2ui-slider', @@ -69,12 +69,12 @@ export class Slider extends DynamicComponent { valueNode.path as string, this.component().dataContextPath, ), - contents: [{ key: '.', valueNumber: value }], + contents: [{key: '.', valueNumber: value}], }, }, ]); } else { - this.handleAction('change', { value }); + this.handleAction('change', {value}); } } @@ -83,7 +83,7 @@ export class Slider extends DynamicComponent { name, context: Object.entries(context).map(([key, val]) => ({ key, - value: typeof val === 'number' ? { literalNumber: val } : { literalString: String(val) }, + value: typeof val === 'number' ? {literalNumber: val} : {literalString: String(val)}, })), }); } diff --git a/renderers/angular/src/v0_8/components/surface.spec.ts b/renderers/angular/src/v0_8/components/surface.spec.ts index b40b7dac2..aedf7be6a 100644 --- a/renderers/angular/src/v0_8/components/surface.spec.ts +++ b/renderers/angular/src/v0_8/components/surface.spec.ts @@ -14,11 +14,11 @@ * limitations under the License. */ -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { Surface } from './surface'; -import { MessageProcessor } from '../data/processor'; -import type { AnyComponentNode, Surface as SurfaceType } from '../types'; -import { Directive, Input, ChangeDetectionStrategy } from '@angular/core'; +import {ComponentFixture, TestBed} from '@angular/core/testing'; +import {Surface} from './surface'; +import {MessageProcessor} from '../data/processor'; +import type {AnyComponentNode, Surface as SurfaceType} from '../types'; +import {Directive, Input, ChangeDetectionStrategy} from '@angular/core'; @Directive({ selector: '[a2ui-renderer]', @@ -57,7 +57,7 @@ describe('Surface Component', () => { await TestBed.configureTestingModule({ imports: [Surface], - providers: [{ provide: MessageProcessor, useValue: mockProcessor }], + providers: [{provide: MessageProcessor, useValue: mockProcessor}], }) .overrideComponent(Surface, { set: { diff --git a/renderers/angular/src/v0_8/components/surface.ts b/renderers/angular/src/v0_8/components/surface.ts index f7eaa7ff4..3a9088694 100644 --- a/renderers/angular/src/v0_8/components/surface.ts +++ b/renderers/angular/src/v0_8/components/surface.ts @@ -14,10 +14,10 @@ * limitations under the License. */ -import { ChangeDetectionStrategy, Component, computed, inject, input } from '@angular/core'; -import { MessageProcessor } from '../data'; -import { Renderer } from '../rendering/renderer'; -import type { Surface as SurfaceType, SurfaceID } from '../types'; +import {ChangeDetectionStrategy, Component, computed, inject, input} from '@angular/core'; +import {MessageProcessor} from '../data'; +import {Renderer} from '../rendering/renderer'; +import type {Surface as SurfaceType, SurfaceID} from '../types'; @Component({ selector: 'a2ui-surface', @@ -39,7 +39,7 @@ import type { Surface as SurfaceType, SurfaceID } from '../types'; export class Surface { private readonly processor = inject(MessageProcessor); readonly surfaceId = input.required(); - readonly surfaceInput = input(null, { alias: 'surface' }); + readonly surfaceInput = input(null, {alias: 'surface'}); protected readonly surface = computed(() => { this.processor.version(); // Track dependency on in-place mutations diff --git a/renderers/angular/src/v0_8/components/tabs.spec.ts b/renderers/angular/src/v0_8/components/tabs.spec.ts index b0db58613..e2cf583c5 100644 --- a/renderers/angular/src/v0_8/components/tabs.spec.ts +++ b/renderers/angular/src/v0_8/components/tabs.spec.ts @@ -14,13 +14,13 @@ * limitations under the License. */ -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { Tabs } from './tabs'; -import { MessageProcessor } from '../data/processor'; -import { Theme } from '../rendering/theming'; -import { Catalog } from '../rendering/catalog'; -import { Directive, Input, ChangeDetectionStrategy } from '@angular/core'; -import { By } from '@angular/platform-browser'; +import {ComponentFixture, TestBed} from '@angular/core/testing'; +import {Tabs} from './tabs'; +import {MessageProcessor} from '../data/processor'; +import {Theme} from '../rendering/theming'; +import {Catalog} from '../rendering/catalog'; +import {Directive, Input, ChangeDetectionStrategy} from '@angular/core'; +import {By} from '@angular/platform-browser'; @Directive({ selector: '[a2ui-renderer]', @@ -43,12 +43,12 @@ describe('Tabs Component', () => { const mockTabItems = [ { - title: { literalString: 'Tab 1' } as any, - child: { id: 'child-1', type: 'Text', properties: { text: 'Content 1' } }, + title: {literalString: 'Tab 1'} as any, + child: {id: 'child-1', type: 'Text', properties: {text: 'Content 1'}}, }, { - title: { literalString: 'Tab 2' } as any, - child: { id: 'child-2', type: 'Text', properties: { text: 'Content 2' } }, + title: {literalString: 'Tab 2'} as any, + child: {id: 'child-2', type: 'Text', properties: {text: 'Content 2'}}, }, ]; @@ -59,7 +59,7 @@ describe('Tabs Component', () => { container: 'tabs-container', controls: { all: 'tabs-controls-all', - selected: { 'tabs-controls-selected': true }, + selected: {'tabs-controls-selected': true}, }, }, } as any; @@ -69,10 +69,10 @@ describe('Tabs Component', () => { providers: [ { provide: MessageProcessor, - useValue: { resolvePrimitive: (p: any) => p?.literalString || p }, + useValue: {resolvePrimitive: (p: any) => p?.literalString || p}, }, - { provide: Theme, useValue: mockTheme }, - { provide: Catalog, useValue: {} }, + {provide: Theme, useValue: mockTheme}, + {provide: Catalog, useValue: {}}, ], }) .overrideComponent(Tabs, { @@ -89,7 +89,7 @@ describe('Tabs Component', () => { component = fixture.componentInstance; fixture.componentRef.setInput('surfaceId', 'surface-1'); - fixture.componentRef.setInput('component', { id: 'tabs-1', type: 'Tabs', weight: 1 }); + fixture.componentRef.setInput('component', {id: 'tabs-1', type: 'Tabs', weight: 1}); fixture.componentRef.setInput('weight', 1); fixture.componentRef.setInput('tabItems', mockTabItems); diff --git a/renderers/angular/src/v0_8/components/tabs.ts b/renderers/angular/src/v0_8/components/tabs.ts index 97dd17a57..8f49935fd 100644 --- a/renderers/angular/src/v0_8/components/tabs.ts +++ b/renderers/angular/src/v0_8/components/tabs.ts @@ -14,10 +14,10 @@ * limitations under the License. */ -import { ChangeDetectionStrategy, Component, input, signal } from '@angular/core'; -import { DynamicComponent } from '../rendering/dynamic-component'; -import { Renderer } from '../rendering/renderer'; -import type { ResolvedTabs, TabsNode } from '../types'; +import {ChangeDetectionStrategy, Component, input, signal} from '@angular/core'; +import {DynamicComponent} from '../rendering/dynamic-component'; +import {Renderer} from '../rendering/renderer'; +import type {ResolvedTabs, TabsNode} from '../types'; @Component({ selector: 'a2ui-tabs', diff --git a/renderers/angular/src/v0_8/components/text-field.spec.ts b/renderers/angular/src/v0_8/components/text-field.spec.ts index df3b947fc..ed39e5ce9 100644 --- a/renderers/angular/src/v0_8/components/text-field.spec.ts +++ b/renderers/angular/src/v0_8/components/text-field.spec.ts @@ -14,13 +14,13 @@ * limitations under the License. */ -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { TextField } from './text-field'; -import { MessageProcessor } from '../data/processor'; -import { Theme } from '../rendering/theming'; -import { Catalog } from '../rendering/catalog'; -import { By } from '@angular/platform-browser'; -import { ChangeDetectionStrategy } from '@angular/core'; +import {ComponentFixture, TestBed} from '@angular/core/testing'; +import {TextField} from './text-field'; +import {MessageProcessor} from '../data/processor'; +import {Theme} from '../rendering/theming'; +import {Catalog} from '../rendering/catalog'; +import {By} from '@angular/platform-browser'; +import {ChangeDetectionStrategy} from '@angular/core'; describe('TextField Component', () => { let component: TextField; @@ -48,9 +48,9 @@ describe('TextField Component', () => { await TestBed.configureTestingModule({ imports: [TextField], providers: [ - { provide: MessageProcessor, useValue: mockProcessor }, - { provide: Theme, useValue: mockTheme }, - { provide: Catalog, useValue: {} }, + {provide: MessageProcessor, useValue: mockProcessor}, + {provide: Theme, useValue: mockTheme}, + {provide: Catalog, useValue: {}}, ], }) .overrideComponent(TextField, { @@ -64,10 +64,10 @@ describe('TextField Component', () => { component = fixture.componentInstance; fixture.componentRef.setInput('surfaceId', 'surface-1'); - fixture.componentRef.setInput('component', { id: 'tf-1', type: 'TextField', weight: 1 }); + fixture.componentRef.setInput('component', {id: 'tf-1', type: 'TextField', weight: 1}); fixture.componentRef.setInput('weight', 1); - fixture.componentRef.setInput('label', { literalString: 'Name' }); - fixture.componentRef.setInput('text', { literalString: 'John Doe' }); + fixture.componentRef.setInput('label', {literalString: 'Name'}); + fixture.componentRef.setInput('text', {literalString: 'John Doe'}); fixture.detectChanges(); }); @@ -111,6 +111,6 @@ describe('TextField Component', () => { const message = mockProcessor.dispatch.calls.mostRecent().args[0]; expect(message.userAction).toBeTruthy(); expect(message.userAction!.name).toBe('input'); - expect(message.userAction!.context).toEqual({ value: 'Jane Doe' }); + expect(message.userAction!.context).toEqual({value: 'Jane Doe'}); }); }); diff --git a/renderers/angular/src/v0_8/components/text-field.ts b/renderers/angular/src/v0_8/components/text-field.ts index babc9bf35..c71e0370e 100644 --- a/renderers/angular/src/v0_8/components/text-field.ts +++ b/renderers/angular/src/v0_8/components/text-field.ts @@ -14,9 +14,9 @@ * limitations under the License. */ -import { ChangeDetectionStrategy, Component, computed, input } from '@angular/core'; -import { DynamicComponent } from '../rendering/dynamic-component'; -import type { ResolvedTextField, StringValue, TextFieldNode } from '../types'; +import {ChangeDetectionStrategy, Component, computed, input} from '@angular/core'; +import {DynamicComponent} from '../rendering/dynamic-component'; +import type {ResolvedTextField, StringValue, TextFieldNode} from '../types'; @Component({ selector: 'a2ui-text-field', @@ -75,12 +75,12 @@ export class TextField extends DynamicComponent { textNode.path as string, this.component().dataContextPath, ), - contents: [{ key: '.', valueString: value }], + contents: [{key: '.', valueString: value}], }, }, ]); } else { - this.handleAction('input', { value }); + this.handleAction('input', {value}); } } @@ -89,7 +89,7 @@ export class TextField extends DynamicComponent { name, context: Object.entries(context).map(([key, val]) => ({ key, - value: typeof val === 'number' ? { literalNumber: val } : { literalString: String(val) }, + value: typeof val === 'number' ? {literalNumber: val} : {literalString: String(val)}, })), }); } diff --git a/renderers/angular/src/v0_8/components/text.spec.ts b/renderers/angular/src/v0_8/components/text.spec.ts index 487d6dc25..9e864e421 100644 --- a/renderers/angular/src/v0_8/components/text.spec.ts +++ b/renderers/angular/src/v0_8/components/text.spec.ts @@ -14,13 +14,13 @@ * limitations under the License. */ -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { Text } from './text'; -import { MessageProcessor } from '../data/processor'; -import { Theme } from '../rendering/theming'; -import { Catalog } from '../rendering/catalog'; -import { MarkdownRenderer } from '../data/markdown'; -import { By } from '@angular/platform-browser'; +import {ComponentFixture, TestBed} from '@angular/core/testing'; +import {Text} from './text'; +import {MessageProcessor} from '../data/processor'; +import {Theme} from '../rendering/theming'; +import {Catalog} from '../rendering/catalog'; +import {MarkdownRenderer} from '../data/markdown'; +import {By} from '@angular/platform-browser'; describe('Text Component', () => { let component: Text; @@ -32,11 +32,11 @@ describe('Text Component', () => { mockTheme = new Theme(); mockTheme.components = { Text: { - all: { 'base-all': true }, - h1: { 'style-h1': true }, - h2: { 'style-h2': true }, - body: { 'style-body': true }, - caption: { 'style-caption': true }, + all: {'base-all': true}, + h1: {'style-h1': true}, + h2: {'style-h2': true}, + body: {'style-body': true}, + caption: {'style-caption': true}, }, } as any; @@ -48,10 +48,10 @@ describe('Text Component', () => { await TestBed.configureTestingModule({ imports: [Text], providers: [ - { provide: MessageProcessor, useValue: {} }, - { provide: Theme, useValue: mockTheme }, - { provide: Catalog, useValue: {} }, - { provide: MarkdownRenderer, useValue: mockMarkdownRenderer }, + {provide: MessageProcessor, useValue: {}}, + {provide: Theme, useValue: mockTheme}, + {provide: Catalog, useValue: {}}, + {provide: MarkdownRenderer, useValue: mockMarkdownRenderer}, ], }) // Text component uses ChangeDetectionStrategy.Eager originally! @@ -61,9 +61,9 @@ describe('Text Component', () => { component = fixture.componentInstance; fixture.componentRef.setInput('surfaceId', 'surface-1'); - fixture.componentRef.setInput('component', { id: 'text-1', type: 'Text', weight: 1 }); + fixture.componentRef.setInput('component', {id: 'text-1', type: 'Text', weight: 1}); fixture.componentRef.setInput('weight', 1); - fixture.componentRef.setInput('text', { literalString: 'Hello World' }); + fixture.componentRef.setInput('text', {literalString: 'Hello World'}); fixture.componentRef.setInput('usageHint', 'body'); fixture.detectChanges(); diff --git a/renderers/angular/src/v0_8/components/text.ts b/renderers/angular/src/v0_8/components/text.ts index ab3fce11a..4682044fb 100644 --- a/renderers/angular/src/v0_8/components/text.ts +++ b/renderers/angular/src/v0_8/components/text.ts @@ -22,12 +22,12 @@ import { input, ViewEncapsulation, } from '@angular/core'; -import { AsyncPipe } from '@angular/common'; -import { DynamicComponent } from '../rendering/dynamic-component'; +import {AsyncPipe} from '@angular/common'; +import {DynamicComponent} from '../rendering/dynamic-component'; import * as Primitives from '@a2ui/web_core/types/primitives'; import * as Styles from '@a2ui/web_core/styles/index'; -import type { ResolvedText, TextNode } from '../types'; -import { MarkdownRenderer } from '../data/markdown'; +import type {ResolvedText, TextNode} from '../types'; +import {MarkdownRenderer} from '../data/markdown'; interface HintedStyles { h1: Record; @@ -143,6 +143,6 @@ export class Text extends DynamicComponent { } const expected = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'caption', 'body']; - return expected.every((v) => v in styles); + return expected.every(v => v in styles); } } diff --git a/renderers/angular/src/v0_8/components/video.spec.ts b/renderers/angular/src/v0_8/components/video.spec.ts index 4d604595d..dce45f6c6 100644 --- a/renderers/angular/src/v0_8/components/video.spec.ts +++ b/renderers/angular/src/v0_8/components/video.spec.ts @@ -14,13 +14,13 @@ * limitations under the License. */ -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { Video } from './video'; -import type { VideoNode } from '../types'; -import { Theme } from '../rendering/theming'; -import { ChangeDetectionStrategy } from '@angular/core'; -import { MessageProcessor } from '../data/processor'; -import { Catalog } from '../rendering/catalog'; +import {ComponentFixture, TestBed} from '@angular/core/testing'; +import {Video} from './video'; +import type {VideoNode} from '../types'; +import {Theme} from '../rendering/theming'; +import {ChangeDetectionStrategy} from '@angular/core'; +import {MessageProcessor} from '../data/processor'; +import {Catalog} from '../rendering/catalog'; describe('Video Component', () => { let component: Video; @@ -33,7 +33,7 @@ describe('Video Component', () => { type: 'Video', weight: 1, properties: { - url: { literalString: 'https://example.com/video.mp4' }, + url: {literalString: 'https://example.com/video.mp4'}, }, }; @@ -44,19 +44,19 @@ describe('Video Component', () => { 'getData', ]); mockTheme = new Theme(); - mockTheme.components = { Video: { 'vid-class': true } } as any; - mockTheme.additionalStyles = { Video: { borderColor: 'blue' } } as any; + mockTheme.components = {Video: {'vid-class': true}} as any; + mockTheme.additionalStyles = {Video: {borderColor: 'blue'}} as any; await TestBed.configureTestingModule({ imports: [Video], providers: [ - { provide: MessageProcessor, useValue: mockProcessor }, - { provide: Theme, useValue: mockTheme }, - { provide: Catalog, useValue: {} }, + {provide: MessageProcessor, useValue: mockProcessor}, + {provide: Theme, useValue: mockTheme}, + {provide: Catalog, useValue: {}}, ], }) .overrideComponent(Video, { - set: { changeDetection: ChangeDetectionStrategy.Default }, + set: {changeDetection: ChangeDetectionStrategy.Default}, }) .compileComponents(); diff --git a/renderers/angular/src/v0_8/components/video.ts b/renderers/angular/src/v0_8/components/video.ts index 93f4e877d..3e85ba31d 100644 --- a/renderers/angular/src/v0_8/components/video.ts +++ b/renderers/angular/src/v0_8/components/video.ts @@ -14,9 +14,9 @@ * limitations under the License. */ -import { ChangeDetectionStrategy, Component, computed, input } from '@angular/core'; -import { DynamicComponent } from '../rendering/dynamic-component'; -import type { StringValue, VideoNode } from '../types'; +import {ChangeDetectionStrategy, Component, computed, input} from '@angular/core'; +import {DynamicComponent} from '../rendering/dynamic-component'; +import type {StringValue, VideoNode} from '../types'; @Component({ selector: 'a2ui-video', diff --git a/renderers/angular/src/v0_8/config.ts b/renderers/angular/src/v0_8/config.ts index fe2508510..4c5c2376c 100644 --- a/renderers/angular/src/v0_8/config.ts +++ b/renderers/angular/src/v0_8/config.ts @@ -14,13 +14,13 @@ * limitations under the License. */ -import { EnvironmentProviders, makeEnvironmentProviders } from '@angular/core'; -import { Catalog, Theme } from './rendering'; -import type { Theme as ThemeType } from './types'; +import {EnvironmentProviders, makeEnvironmentProviders} from '@angular/core'; +import {Catalog, Theme} from './rendering'; +import type {Theme as ThemeType} from './types'; -export function provideA2UI(config: { catalog: Catalog; theme: ThemeType }): EnvironmentProviders { +export function provideA2UI(config: {catalog: Catalog; theme: ThemeType}): EnvironmentProviders { return makeEnvironmentProviders([ - { provide: Catalog, useValue: config.catalog }, + {provide: Catalog, useValue: config.catalog}, { provide: Theme, useFactory: () => { diff --git a/renderers/angular/src/v0_8/data/index.ts b/renderers/angular/src/v0_8/data/index.ts index dabe62c15..1379d413e 100644 --- a/renderers/angular/src/v0_8/data/index.ts +++ b/renderers/angular/src/v0_8/data/index.ts @@ -16,4 +16,4 @@ export * from './processor'; export * from './types'; -export { provideMarkdownRenderer } from './markdown'; +export {provideMarkdownRenderer} from './markdown'; diff --git a/renderers/angular/src/v0_8/data/markdown.ts b/renderers/angular/src/v0_8/data/markdown.ts index ab44b1103..4603cb0d7 100644 --- a/renderers/angular/src/v0_8/data/markdown.ts +++ b/renderers/angular/src/v0_8/data/markdown.ts @@ -14,8 +14,8 @@ * limitations under the License. */ -import { Injectable } from '@angular/core'; -import type { MarkdownRenderer as MarkdownRendererType, MarkdownRendererOptions } from '../types'; +import {Injectable} from '@angular/core'; +import type {MarkdownRenderer as MarkdownRendererType, MarkdownRendererOptions} from '../types'; @Injectable({ providedIn: 'root', @@ -33,7 +33,7 @@ export class DefaultMarkdownRenderer extends MarkdownRenderer { override async render(markdown: string, options?: MarkdownRendererOptions): Promise { try { // @ts-ignore - optional peer dependency - const { renderMarkdown } = await import('@a2ui/markdown-it'); + const {renderMarkdown} = await import('@a2ui/markdown-it'); return await renderMarkdown(markdown, options); } catch (e) { if (!DefaultMarkdownRenderer.warningLogged) { @@ -57,5 +57,5 @@ export function provideMarkdownRenderer(renderFn?: MarkdownRendererType) { }, }; } - return { provide: MarkdownRenderer, useClass: DefaultMarkdownRenderer }; + return {provide: MarkdownRenderer, useClass: DefaultMarkdownRenderer}; } diff --git a/renderers/angular/src/v0_8/data/processor.spec.ts b/renderers/angular/src/v0_8/data/processor.spec.ts index 1565a8bba..00cd95819 100644 --- a/renderers/angular/src/v0_8/data/processor.spec.ts +++ b/renderers/angular/src/v0_8/data/processor.spec.ts @@ -14,10 +14,10 @@ * limitations under the License. */ -import { TestBed } from '@angular/core/testing'; -import { MessageProcessor, A2UIClientEvent } from './processor'; -import { Catalog } from '../rendering/catalog'; -import type { A2UIClientEventMessage, AnyComponentNode, ServerToClientMessage } from '../types'; +import {TestBed} from '@angular/core/testing'; +import {MessageProcessor, A2UIClientEvent} from './processor'; +import {Catalog} from '../rendering/catalog'; +import type {A2UIClientEventMessage, AnyComponentNode, ServerToClientMessage} from '../types'; import * as WebCore from '@a2ui/web_core/v0_8'; describe('MessageProcessor', () => { @@ -28,7 +28,7 @@ describe('MessageProcessor', () => { mockCatalog = {}; TestBed.configureTestingModule({ - providers: [MessageProcessor, { provide: Catalog, useValue: mockCatalog }], + providers: [MessageProcessor, {provide: Catalog, useValue: mockCatalog}], }); service = TestBed.inject(MessageProcessor); }); @@ -47,7 +47,7 @@ describe('MessageProcessor', () => { expect(baseProcessor.processMessages).toHaveBeenCalledWith(messages); }); - it('should dispatch events and emit to observable', (done) => { + it('should dispatch events and emit to observable', done => { const mockMessage: A2UIClientEventMessage = { userAction: { name: 'click', @@ -68,10 +68,10 @@ describe('MessageProcessor', () => { it('should resolve dispatch promise when completion is triggered', async () => { const mockMessage: A2UIClientEventMessage = { - userAction: { name: 'click', sourceComponentId: '1', surfaceId: '1', timestamp: '' }, + userAction: {name: 'click', sourceComponentId: '1', surfaceId: '1', timestamp: ''}, }; - const replyMessages: ServerToClientMessage[] = [{ type: 'UpdateSurface' } as any]; + const replyMessages: ServerToClientMessage[] = [{type: 'UpdateSurface'} as any]; // Setup subscription to trigger completion service.events.subscribe((event: A2UIClientEvent) => { @@ -86,7 +86,7 @@ describe('MessageProcessor', () => { const baseProcessor = (service as any).baseProcessor; spyOn(baseProcessor, 'getData').and.returnValue('mock-value'); - const node = { id: '1', type: 'Text' } as any as AnyComponentNode; + const node = {id: '1', type: 'Text'} as any as AnyComponentNode; const result = service.getData(node, 'path/to/data', 'surf-1'); expect(baseProcessor.getData).toHaveBeenCalledWith(node, 'path/to/data', 'surf-1'); @@ -97,7 +97,7 @@ describe('MessageProcessor', () => { const baseProcessor = (service as any).baseProcessor; spyOn(baseProcessor, 'setData'); - const node = { id: '1', type: 'Text' } as any as AnyComponentNode; + const node = {id: '1', type: 'Text'} as any as AnyComponentNode; service.setData(node, 'path/to/data', 'new-value', 'surf-1'); expect(baseProcessor.setData).toHaveBeenCalledWith(node, 'path/to/data', 'new-value', 'surf-1'); @@ -134,7 +134,7 @@ describe('MessageProcessor', () => { id: readyComponentId, type: 'Text', properties: { - text: { literalString: 'Ready to render' }, + text: {literalString: 'Ready to render'}, }, }, dataModel: new Map(), diff --git a/renderers/angular/src/v0_8/data/processor.ts b/renderers/angular/src/v0_8/data/processor.ts index e8efdc8c1..cb7775c48 100644 --- a/renderers/angular/src/v0_8/data/processor.ts +++ b/renderers/angular/src/v0_8/data/processor.ts @@ -14,11 +14,11 @@ * limitations under the License. */ -import { Injectable, signal } from '@angular/core'; -import { Subject, Observable } from 'rxjs'; +import {Injectable, signal} from '@angular/core'; +import {Subject, Observable} from 'rxjs'; import * as WebCore from '@a2ui/web_core/v0_8'; -import type { A2UIClientEventMessage, AnyComponentNode, ServerToClientMessage } from '../types'; +import type {A2UIClientEventMessage, AnyComponentNode, ServerToClientMessage} from '../types'; export interface A2UIClientEvent { message: A2UIClientEventMessage; @@ -50,7 +50,7 @@ export class MessageProcessor { * This should be called after any update to the underlying base processor's surfaces. */ private notify() { - this.versionSignal.update((v) => v + 1); + this.versionSignal.update(v => v + 1); } processMessages(messages: ServerToClientMessage[]) { @@ -62,12 +62,12 @@ export class MessageProcessor { const completion = new Subject(); const promise = new Promise((resolve, reject) => { completion.subscribe({ - next: (msgs) => resolve(msgs), - error: (err) => reject(err), + next: msgs => resolve(msgs), + error: err => reject(err), }); }); - this.eventsSubject.next({ message, completion }); + this.eventsSubject.next({message, completion}); return promise; } diff --git a/renderers/angular/src/v0_8/data/types.ts b/renderers/angular/src/v0_8/data/types.ts index d1e58f404..d143dc873 100644 --- a/renderers/angular/src/v0_8/data/types.ts +++ b/renderers/angular/src/v0_8/data/types.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import type { ServerToClientMessage } from '../types'; +import type {ServerToClientMessage} from '../types'; export interface A2TextPayload { kind: 'text'; @@ -26,4 +26,4 @@ export interface A2DataPayload { data: ServerToClientMessage; } -export type A2AServerPayload = Array | { error: string }; +export type A2AServerPayload = Array | {error: string}; diff --git a/renderers/angular/src/v0_8/rendering/catalog.ts b/renderers/angular/src/v0_8/rendering/catalog.ts index ee4039a74..4e04dcfc9 100644 --- a/renderers/angular/src/v0_8/rendering/catalog.ts +++ b/renderers/angular/src/v0_8/rendering/catalog.ts @@ -14,9 +14,9 @@ * limitations under the License. */ -import { Binding, InjectionToken, Type } from '@angular/core'; -import { DynamicComponent } from './dynamic-component'; -import type { AnyComponentNode } from '../types'; +import {Binding, InjectionToken, Type} from '@angular/core'; +import {DynamicComponent} from './dynamic-component'; +import type {AnyComponentNode} from '../types'; export type CatalogLoader = () => | Promise>> diff --git a/renderers/angular/src/v0_8/rendering/dynamic-component.ts b/renderers/angular/src/v0_8/rendering/dynamic-component.ts index 17ad90856..6f6d2fce3 100644 --- a/renderers/angular/src/v0_8/rendering/dynamic-component.ts +++ b/renderers/angular/src/v0_8/rendering/dynamic-component.ts @@ -14,9 +14,9 @@ * limitations under the License. */ -import { Directive, inject, input } from '@angular/core'; -import { MessageProcessor } from '../data'; -import { Theme } from './theming'; +import {Directive, inject, input} from '@angular/core'; +import {MessageProcessor} from '../data'; +import {Theme} from './theming'; import type { A2UIClientEventMessage, Action, diff --git a/renderers/angular/src/v0_8/rendering/renderer.spec.ts b/renderers/angular/src/v0_8/rendering/renderer.spec.ts index 232f8c28f..837b8f8a0 100644 --- a/renderers/angular/src/v0_8/rendering/renderer.spec.ts +++ b/renderers/angular/src/v0_8/rendering/renderer.spec.ts @@ -14,10 +14,10 @@ * limitations under the License. */ -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { Renderer } from './renderer'; -import { Catalog } from './catalog'; -import { Component, Input } from '@angular/core'; +import {ComponentFixture, TestBed} from '@angular/core/testing'; +import {Renderer} from './renderer'; +import {Catalog} from './catalog'; +import {Component, Input} from '@angular/core'; @Component({ selector: 'test-comp', @@ -51,12 +51,12 @@ describe('v0.8 Renderer', () => { beforeEach(async () => { mockCatalog = { - TestComp: { type: () => TestComp }, + TestComp: {type: () => TestComp}, }; await TestBed.configureTestingModule({ imports: [TestHost], - providers: [{ provide: Catalog, useValue: mockCatalog }], + providers: [{provide: Catalog, useValue: mockCatalog}], }).compileComponents(); fixture = TestBed.createComponent(TestHost); @@ -66,7 +66,7 @@ describe('v0.8 Renderer', () => { fixture.componentInstance.surfaceId = 'surf-1'; fixture.componentInstance.component = { type: 'TestComp', - properties: { text: 'Hello v0.8' }, + properties: {text: 'Hello v0.8'}, weight: 10, }; @@ -84,7 +84,7 @@ describe('v0.8 Renderer', () => { fixture.componentInstance.surfaceId = 'surf-1'; fixture.componentInstance.component = { type: 'TestComp', - properties: { text: 'Async Hello' }, + properties: {text: 'Async Hello'}, }; fixture.detectChanges(); @@ -113,7 +113,7 @@ describe('v0.8 Renderer', () => { fixture.componentInstance.surfaceId = 'surf-1'; fixture.componentInstance.component = { type: 'TestComp', - properties: { text: 'Function Hello' }, + properties: {text: 'Function Hello'}, }; fixture.detectChanges(); @@ -158,12 +158,12 @@ describe('v0.8 Renderer Regression Tests', () => { beforeEach(async () => { mockCatalog = { - CompWithInputs: { type: () => CompWithInputs }, + CompWithInputs: {type: () => CompWithInputs}, }; await TestBed.configureTestingModule({ imports: [TestHost], - providers: [{ provide: Catalog, useValue: mockCatalog }], + providers: [{provide: Catalog, useValue: mockCatalog}], }).compileComponents(); fixture = TestBed.createComponent(TestHost); @@ -213,14 +213,14 @@ describe('v0.8 Renderer Regression Tests', () => { } } - mockCatalog['CompWithChildren'] = { type: () => CompWithChildren }; + mockCatalog['CompWithChildren'] = {type: () => CompWithChildren}; fixture.componentInstance.surfaceId = 'surf-1'; fixture.componentInstance.component = { type: 'CompWithChildren', properties: { - children: [{ id: 'child-1' }], - child: { id: 'child-2' }, + children: [{id: 'child-1'}], + child: {id: 'child-2'}, }, }; @@ -228,8 +228,8 @@ describe('v0.8 Renderer Regression Tests', () => { await fixture.whenStable(); fixture.detectChanges(); - expect(setCapture.children).toEqual([{ id: 'child-1' }]); - expect(setCapture.child).toEqual({ id: 'child-2' }); + expect(setCapture.children).toEqual([{id: 'child-1'}]); + expect(setCapture.child).toEqual({id: 'child-2'}); }); it('should gracefully handle components with missing properties', async () => { diff --git a/renderers/angular/src/v0_8/rendering/renderer.ts b/renderers/angular/src/v0_8/rendering/renderer.ts index 9482dee24..4bb1e40cd 100644 --- a/renderers/angular/src/v0_8/rendering/renderer.ts +++ b/renderers/angular/src/v0_8/rendering/renderer.ts @@ -24,12 +24,12 @@ import { PLATFORM_ID, ComponentRef, } from '@angular/core'; -import { DOCUMENT, isPlatformBrowser } from '@angular/common'; -import { structuralStyles } from '@a2ui/web_core/styles/index'; -import { Catalog } from './catalog'; -import { MessageProcessor } from '../data'; -import type { AnyComponentNode, SurfaceID } from '../types'; -import { DynamicComponent } from './dynamic-component'; +import {DOCUMENT, isPlatformBrowser} from '@angular/common'; +import {structuralStyles} from '@a2ui/web_core/styles/index'; +import {Catalog} from './catalog'; +import {MessageProcessor} from '../data'; +import type {AnyComponentNode, SurfaceID} from '../types'; +import {DynamicComponent} from './dynamic-component'; @Directive({ selector: '[a2ui-renderer]', @@ -120,7 +120,7 @@ export class Renderer { const componentTypeOrPromise = this.resolveComponentType(config); if (componentTypeOrPromise instanceof Promise) { - componentTypeOrPromise.then((componentType) => { + componentTypeOrPromise.then(componentType => { // Ensure we are still supposed to render this component if (this.currentId === node.id && this.currentType === node.type) { const componentRef = container.createComponent(componentType) as ComponentRef< diff --git a/renderers/angular/src/v0_8/rendering/theming.ts b/renderers/angular/src/v0_8/rendering/theming.ts index fe6ed0622..762106d62 100644 --- a/renderers/angular/src/v0_8/rendering/theming.ts +++ b/renderers/angular/src/v0_8/rendering/theming.ts @@ -14,8 +14,8 @@ * limitations under the License. */ -import { Injectable } from '@angular/core'; -import type { Theme as ThemeType } from '../types'; +import {Injectable} from '@angular/core'; +import type {Theme as ThemeType} from '../types'; @Injectable({ providedIn: 'root', diff --git a/renderers/angular/src/v0_8/v0_8_integration.spec.ts b/renderers/angular/src/v0_8/v0_8_integration.spec.ts index 6c85663e6..e9496322b 100644 --- a/renderers/angular/src/v0_8/v0_8_integration.spec.ts +++ b/renderers/angular/src/v0_8/v0_8_integration.spec.ts @@ -14,14 +14,14 @@ * limitations under the License. */ -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { Renderer } from './rendering/renderer'; -import { Catalog } from './rendering/catalog'; -import { DEFAULT_CATALOG } from './catalog'; -import { Theme } from './rendering/theming'; -import { MessageProcessor } from './data/processor'; -import { MarkdownRenderer } from './data/markdown'; -import { Component } from '@angular/core'; +import {ComponentFixture, TestBed} from '@angular/core/testing'; +import {Renderer} from './rendering/renderer'; +import {Catalog} from './rendering/catalog'; +import {DEFAULT_CATALOG} from './catalog'; +import {Theme} from './rendering/theming'; +import {MessageProcessor} from './data/processor'; +import {MarkdownRenderer} from './data/markdown'; +import {Component} from '@angular/core'; import * as restaurantCardMock from './test_data/mocks/restaurant-card.json'; import * as contactCardMock from './test_data/mocks/contact-card.json'; @@ -30,7 +30,7 @@ import * as contactCardMock from './test_data/mocks/contact-card.json'; * This handles the v0.8 format where children are often referenced by ID. */ function resolveComponentTree(messages: any[], rootId: string): any { - const surfaceUpdate = messages.find((m) => m.surfaceUpdate)?.surfaceUpdate; + const surfaceUpdate = messages.find(m => m.surfaceUpdate)?.surfaceUpdate; if (!surfaceUpdate) return null; const componentMap = new Map(surfaceUpdate.components.map((c: any) => [c.id, c])); @@ -48,7 +48,7 @@ function resolveComponentTree(messages: any[], rootId: string): any { // If it's in the { id, component: { Type: { ... } } } format if (idOrNode.component) { const type = Object.keys(idOrNode.component)[0]; - const properties = { ...idOrNode.component[type] }; + const properties = {...idOrNode.component[type]}; // Recursively resolve children if (properties.child) { @@ -109,8 +109,8 @@ describe('v0.8 Angular Renderer Integration', () => { await TestBed.configureTestingModule({ imports: [TestHost], providers: [ - { provide: MessageProcessor, useValue: processor }, - { provide: Catalog, useValue: DEFAULT_CATALOG }, + {provide: MessageProcessor, useValue: processor}, + {provide: Catalog, useValue: DEFAULT_CATALOG}, { provide: MarkdownRenderer, useValue: { @@ -124,22 +124,22 @@ describe('v0.8 Angular Renderer Integration', () => { theme.update({ components: { Text: { - all: { 'a2ui-text': true }, - h1: { 'a2ui-text-h1': true }, - h2: { 'a2ui-text-h2': true }, - h3: { 'a2ui-text-h3': true }, - h4: { 'a2ui-text-h4': true }, - h5: { 'a2ui-text-h5': true }, - body: { 'common-body': true }, - caption: { 'caption-style': true }, + all: {'a2ui-text': true}, + h1: {'a2ui-text-h1': true}, + h2: {'a2ui-text-h2': true}, + h3: {'a2ui-text-h3': true}, + h4: {'a2ui-text-h4': true}, + h5: {'a2ui-text-h5': true}, + body: {'common-body': true}, + caption: {'caption-style': true}, }, - Card: { 'a2ui-card': true }, - Row: { 'a2ui-row': true }, - Column: { 'a2ui-column': true }, - Image: { all: { 'a2ui-image': true }, avatar: { 'avatar-style': true } }, - Divider: { 'a2ui-divider': true }, - Icon: { 'a2ui-icon': true }, - Button: { 'a2ui-button': true }, + Card: {'a2ui-card': true}, + Row: {'a2ui-row': true}, + Column: {'a2ui-column': true}, + Image: {all: {'a2ui-image': true}, avatar: {'avatar-style': true}}, + Divider: {'a2ui-divider': true}, + Icon: {'a2ui-icon': true}, + Button: {'a2ui-button': true}, } as any, elements: {} as any, markdown: { @@ -165,7 +165,7 @@ describe('v0.8 Angular Renderer Integration', () => { fixture.componentInstance.component = { type: 'Row', id: 'row-1', - properties: { children: [] }, + properties: {children: []}, }; fixture.detectChanges(); @@ -194,7 +194,7 @@ describe('v0.8 Angular Renderer Integration', () => { type: 'Text', id: 'name', properties: { - text: { path: '/name' }, + text: {path: '/name'}, usageHint: 'h2', }, }, @@ -229,7 +229,7 @@ describe('v0.8 Angular Renderer Integration', () => { id: 'wrapper-test', component: { Text: { - text: { literalString: 'Wrapped Text' }, + text: {literalString: 'Wrapped Text'}, }, }, }; @@ -249,7 +249,7 @@ describe('v0.8 Angular Renderer Integration', () => { type: 'Text', id: 'text-1', properties: { - text: { literalString: 'Resilient' }, + text: {literalString: 'Resilient'}, unknownProp: 'should not crash', }, }; diff --git a/renderers/angular/src/v0_9/catalog/basic/audio-player.component.ts b/renderers/angular/src/v0_9/catalog/basic/audio-player.component.ts index 0728dfed3..f284de4b2 100644 --- a/renderers/angular/src/v0_9/catalog/basic/audio-player.component.ts +++ b/renderers/angular/src/v0_9/catalog/basic/audio-player.component.ts @@ -14,9 +14,9 @@ * limitations under the License. */ -import { Component, computed, ChangeDetectionStrategy } from '@angular/core'; -import { BasicCatalogComponent } from './basic-catalog-component'; -import { AudioPlayerApi } from '@a2ui/web_core/v0_9/basic_catalog'; +import {Component, computed, ChangeDetectionStrategy} from '@angular/core'; +import {BasicCatalogComponent} from './basic-catalog-component'; +import {AudioPlayerApi} from '@a2ui/web_core/v0_9/basic_catalog'; /** * Angular implementation of the A2UI AudioPlayer component (v0.9). diff --git a/renderers/angular/src/v0_9/catalog/basic/basic-catalog-component.spec.ts b/renderers/angular/src/v0_9/catalog/basic/basic-catalog-component.spec.ts index d9cde7469..2f0946920 100644 --- a/renderers/angular/src/v0_9/catalog/basic/basic-catalog-component.spec.ts +++ b/renderers/angular/src/v0_9/catalog/basic/basic-catalog-component.spec.ts @@ -14,12 +14,12 @@ * limitations under the License. */ -import { Component } from '@angular/core'; -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { BasicCatalogComponent } from './basic-catalog-component'; -import { A2uiRendererService, A2UI_RENDERER_CONFIG } from '../../core/a2ui-renderer.service'; -import { BasicCatalog } from './basic-catalog'; -import { ComponentApi } from '@a2ui/web_core/v0_9'; +import {Component} from '@angular/core'; +import {ComponentFixture, TestBed} from '@angular/core/testing'; +import {BasicCatalogComponent} from './basic-catalog-component'; +import {A2uiRendererService, A2UI_RENDERER_CONFIG} from '../../core/a2ui-renderer.service'; +import {BasicCatalog} from './basic-catalog'; +import {ComponentApi} from '@a2ui/web_core/v0_9'; import z from 'zod'; export const TestComponentApi = { @@ -36,8 +36,7 @@ export const TestComponentApi = { template: '
Test
', standalone: true, }) -class TestBasicComp extends BasicCatalogComponent { -} +class TestBasicComp extends BasicCatalogComponent {} describe('BasicCatalogComponent', () => { let fixture: ComponentFixture; diff --git a/renderers/angular/src/v0_9/catalog/basic/basic-catalog-component.ts b/renderers/angular/src/v0_9/catalog/basic/basic-catalog-component.ts index be6237c6b..285ae8623 100644 --- a/renderers/angular/src/v0_9/catalog/basic/basic-catalog-component.ts +++ b/renderers/angular/src/v0_9/catalog/basic/basic-catalog-component.ts @@ -14,11 +14,11 @@ * limitations under the License. */ -import { Directive, computed, HostBinding, inject } from '@angular/core'; -import { injectBasicCatalogStyles } from '@a2ui/web_core/v0_9/basic_catalog'; -import { A2uiRendererService } from '../../core/a2ui-renderer.service'; -import { ComponentApi } from '@a2ui/web_core/v0_9'; -import { CatalogComponent } from '../../core/catalog_component'; +import {Directive, computed, HostBinding, inject} from '@angular/core'; +import {injectBasicCatalogStyles} from '@a2ui/web_core/v0_9/basic_catalog'; +import {A2uiRendererService} from '../../core/a2ui-renderer.service'; +import {ComponentApi} from '@a2ui/web_core/v0_9'; +import {CatalogComponent} from '../../core/catalog_component'; /** * Base class for A2UI basic catalog components in Angular. @@ -27,7 +27,9 @@ import { CatalogComponent } from '../../core/catalog_component'; * Also binds the primary brand color to the host element. */ @Directive() -export abstract class BasicCatalogComponent extends CatalogComponent { +export abstract class BasicCatalogComponent< + Api extends ComponentApi, +> extends CatalogComponent { protected rendererService = inject(A2uiRendererService); readonly surface = computed(() => { @@ -63,4 +65,3 @@ export abstract class BasicCatalogComponent extends Ca return this.primaryColor() || null; } } - diff --git a/renderers/angular/src/v0_9/catalog/basic/basic-catalog.ts b/renderers/angular/src/v0_9/catalog/basic/basic-catalog.ts index 52abefdc6..b72515022 100644 --- a/renderers/angular/src/v0_9/catalog/basic/basic-catalog.ts +++ b/renderers/angular/src/v0_9/catalog/basic/basic-catalog.ts @@ -14,26 +14,26 @@ * limitations under the License. */ -import { Injectable } from '@angular/core'; -import { AngularCatalog, AngularComponentImplementation } from '../types'; -import { TextComponent } from './text.component'; -import { RowComponent } from './row.component'; -import { ColumnComponent } from './column.component'; -import { ButtonComponent } from './button.component'; -import { TextFieldComponent } from './text-field.component'; -import { ImageComponent } from './image.component'; -import { IconComponent } from './icon.component'; -import { VideoComponent } from './video.component'; -import { AudioPlayerComponent } from './audio-player.component'; -import { ListComponent } from './list.component'; -import { CardComponent } from './card.component'; -import { TabsComponent } from './tabs.component'; -import { ModalComponent } from './modal.component'; -import { DividerComponent } from './divider.component'; -import { CheckBoxComponent } from './check-box.component'; -import { ChoicePickerComponent } from './choice-picker.component'; -import { SliderComponent } from './slider.component'; -import { DateTimeInputComponent } from './date-time-input.component'; +import {Injectable} from '@angular/core'; +import {AngularCatalog, AngularComponentImplementation} from '../types'; +import {TextComponent} from './text.component'; +import {RowComponent} from './row.component'; +import {ColumnComponent} from './column.component'; +import {ButtonComponent} from './button.component'; +import {TextFieldComponent} from './text-field.component'; +import {ImageComponent} from './image.component'; +import {IconComponent} from './icon.component'; +import {VideoComponent} from './video.component'; +import {AudioPlayerComponent} from './audio-player.component'; +import {ListComponent} from './list.component'; +import {CardComponent} from './card.component'; +import {TabsComponent} from './tabs.component'; +import {ModalComponent} from './modal.component'; +import {DividerComponent} from './divider.component'; +import {CheckBoxComponent} from './check-box.component'; +import {ChoicePickerComponent} from './choice-picker.component'; +import {SliderComponent} from './slider.component'; +import {DateTimeInputComponent} from './date-time-input.component'; import { BASIC_FUNCTIONS, @@ -56,30 +56,30 @@ import { SliderApi, DateTimeInputApi, } from '@a2ui/web_core/v0_9/basic_catalog'; -import { FunctionImplementation } from '@a2ui/web_core/v0_9'; +import {FunctionImplementation} from '@a2ui/web_core/v0_9'; /** * The set of default Angular implementations for each component in the basic catalog. */ const DEFAULT_COMPONENT_IMPLEMENTATIONS: Record = { - text: { ...TextApi, component: TextComponent }, - row: { ...RowApi, component: RowComponent }, - column: { ...ColumnApi, component: ColumnComponent }, - button: { ...ButtonApi, component: ButtonComponent }, - textField: { ...TextFieldApi, component: TextFieldComponent }, - image: { ...ImageApi, component: ImageComponent }, - icon: { ...IconApi, component: IconComponent }, - video: { ...VideoApi, component: VideoComponent }, - audioPlayer: { ...AudioPlayerApi, component: AudioPlayerComponent }, - list: { ...ListApi, component: ListComponent }, - card: { ...CardApi, component: CardComponent }, - tabs: { ...TabsApi, component: TabsComponent }, - modal: { ...ModalApi, component: ModalComponent }, - divider: { ...DividerApi, component: DividerComponent }, - checkBox: { ...CheckBoxApi, component: CheckBoxComponent }, - choicePicker: { ...ChoicePickerApi, component: ChoicePickerComponent }, - slider: { ...SliderApi, component: SliderComponent }, - dateTimeInput: { ...DateTimeInputApi, component: DateTimeInputComponent }, + text: {...TextApi, component: TextComponent}, + row: {...RowApi, component: RowComponent}, + column: {...ColumnApi, component: ColumnComponent}, + button: {...ButtonApi, component: ButtonComponent}, + textField: {...TextFieldApi, component: TextFieldComponent}, + image: {...ImageApi, component: ImageComponent}, + icon: {...IconApi, component: IconComponent}, + video: {...VideoApi, component: VideoComponent}, + audioPlayer: {...AudioPlayerApi, component: AudioPlayerComponent}, + list: {...ListApi, component: ListComponent}, + card: {...CardApi, component: CardComponent}, + tabs: {...TabsApi, component: TabsComponent}, + modal: {...ModalApi, component: ModalComponent}, + divider: {...DividerApi, component: DividerComponent}, + checkBox: {...CheckBoxApi, component: CheckBoxComponent}, + choicePicker: {...ChoicePickerApi, component: ChoicePickerComponent}, + slider: {...SliderApi, component: SliderComponent}, + dateTimeInput: {...DateTimeInputApi, component: DateTimeInputComponent}, } as const; /** @@ -120,7 +120,7 @@ export const BASIC_COMPONENTS: AngularComponentImplementation[] = Object.values( /** * The set of client-side functions provided by the basic catalog. */ -export { BASIC_FUNCTIONS }; +export {BASIC_FUNCTIONS}; /** * A base class for basic catalogs, providing extensibility for non-DI use cases. @@ -134,7 +134,7 @@ export class BasicCatalogBase extends AngularCatalog { const components: AngularComponentImplementation[] = [ ...Object.entries(DEFAULT_COMPONENT_IMPLEMENTATIONS).map(([key, defaultValue]) => { const impl = (overrides as any)[key] ?? defaultValue; - return { ...impl, name: impl.name || key }; + return {...impl, name: impl.name || key}; }), ...(options.extraComponents ?? []), ]; diff --git a/renderers/angular/src/v0_9/catalog/basic/button.component.spec.ts b/renderers/angular/src/v0_9/catalog/basic/button.component.spec.ts index 0e56ec8ae..bdd7a2bc2 100644 --- a/renderers/angular/src/v0_9/catalog/basic/button.component.spec.ts +++ b/renderers/angular/src/v0_9/catalog/basic/button.component.spec.ts @@ -14,13 +14,13 @@ * limitations under the License. */ -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { Component, input, signal } from '@angular/core'; -import { ButtonComponent } from './button.component'; -import { ComponentModel } from '@a2ui/web_core/v0_9'; -import { A2uiRendererService } from '../../core/a2ui-renderer.service'; -import { ComponentBinder } from '../../core/component-binder.service'; -import { By } from '@angular/platform-browser'; +import {ComponentFixture, TestBed} from '@angular/core/testing'; +import {Component, input, signal} from '@angular/core'; +import {ButtonComponent} from './button.component'; +import {ComponentModel} from '@a2ui/web_core/v0_9'; +import {A2uiRendererService} from '../../core/a2ui-renderer.service'; +import {ComponentBinder} from '../../core/component-binder.service'; +import {By} from '@angular/platform-browser'; describe('ButtonComponent', () => { let component: ButtonComponent; @@ -33,7 +33,7 @@ describe('ButtonComponent', () => { mockSurface = { dispatchAction: jasmine.createSpy('dispatchAction'), componentsModel: new Map([ - ['child1', new ComponentModel('child1', 'Text', { text: 'Child Content' })], + ['child1', new ComponentModel('child1', 'Text', {text: 'Child Content'})], ]), catalog: { id: 'test-catalog', @@ -70,13 +70,13 @@ describe('ButtonComponent', () => { }; const mockBinder = jasmine.createSpyObj('ComponentBinder', ['bind']); - mockBinder.bind.and.returnValue({ text: { value: () => 'bound text' } }); + mockBinder.bind.and.returnValue({text: {value: () => 'bound text'}}); await TestBed.configureTestingModule({ imports: [ButtonComponent], providers: [ - { provide: A2uiRendererService, useValue: mockRendererService }, - { provide: ComponentBinder, useValue: mockBinder }, + {provide: A2uiRendererService, useValue: mockRendererService}, + {provide: ComponentBinder, useValue: mockBinder}, ], }).compileComponents(); @@ -85,11 +85,11 @@ describe('ButtonComponent', () => { fixture.componentRef.setInput('surfaceId', 'surf1'); fixture.componentRef.setInput('componentId', 'comp1'); fixture.componentRef.setInput('props', { - variant: { value: signal('primary'), raw: 'primary', onUpdate: () => {} }, - child: { value: signal({ id: 'child1', basePath: '/' }), raw: 'child1', onUpdate: () => {} }, + variant: {value: signal('primary'), raw: 'primary', onUpdate: () => {}}, + child: {value: signal({id: 'child1', basePath: '/'}), raw: 'child1', onUpdate: () => {}}, action: { - value: signal({ type: 'test-action', data: {} }), - raw: { type: 'test-action', data: {} }, + value: signal({type: 'test-action', data: {}}), + raw: {type: 'test-action', data: {}}, onUpdate: () => {}, }, }); @@ -139,13 +139,13 @@ describe('ButtonComponent', () => { fixture.detectChanges(); const host = fixture.debugElement.query(By.css('a2ui-v09-component-host')); expect(host).toBeTruthy(); - expect(host.componentInstance.componentKey()).toEqual({ id: 'child1', basePath: '/' }); + expect(host.componentInstance.componentKey()).toEqual({id: 'child1', basePath: '/'}); }); it('should not show child component host if child prop is absent', () => { fixture.componentRef.setInput('props', { ...component.props(), - child: { value: signal(null), raw: null, onUpdate: () => {} }, + child: {value: signal(null), raw: null, onUpdate: () => {}}, }); fixture.detectChanges(); const host = fixture.debugElement.query(By.css('a2ui-v09-component-host')); @@ -157,7 +157,7 @@ describe('ButtonComponent', () => { fixture.componentRef.setInput('props', { ...component.props(), - isValid: { value: isValidSig, raw: true, onUpdate: () => {} }, + isValid: {value: isValidSig, raw: true, onUpdate: () => {}}, }); fixture.detectChanges(); @@ -170,7 +170,7 @@ describe('ButtonComponent', () => { }); it('should override the button default background color when primary color is set', () => { - mockSurface.theme = { primaryColor: 'red' }; + mockSurface.theme = {primaryColor: 'red'}; fixture.detectChanges(); const button = fixture.debugElement.query(By.css('button')); const computedStyle = window.getComputedStyle(button.nativeElement); diff --git a/renderers/angular/src/v0_9/catalog/basic/button.component.ts b/renderers/angular/src/v0_9/catalog/basic/button.component.ts index a3699b13b..849ab51db 100644 --- a/renderers/angular/src/v0_9/catalog/basic/button.component.ts +++ b/renderers/angular/src/v0_9/catalog/basic/button.component.ts @@ -14,11 +14,11 @@ * limitations under the License. */ -import { Component, computed, ChangeDetectionStrategy } from '@angular/core'; -import { ComponentHostComponent } from '../../core/component-host.component'; -import { DataContext } from '@a2ui/web_core/v0_9'; -import { BasicCatalogComponent } from './basic-catalog-component'; -import { ButtonApi } from '@a2ui/web_core/v0_9/basic_catalog'; +import {Component, computed, ChangeDetectionStrategy} from '@angular/core'; +import {ComponentHostComponent} from '../../core/component-host.component'; +import {DataContext} from '@a2ui/web_core/v0_9'; +import {BasicCatalogComponent} from './basic-catalog-component'; +import {ButtonApi} from '@a2ui/web_core/v0_9/basic_catalog'; /** * Angular implementation of the A2UI Button component (v0.9). @@ -96,7 +96,6 @@ import { ButtonApi } from '@a2ui/web_core/v0_9/basic_catalog'; changeDetection: ChangeDetectionStrategy.OnPush, }) export class ButtonComponent extends BasicCatalogComponent { - readonly variant = computed(() => this.props()['variant']?.value() ?? 'default'); readonly child = computed(() => this.props()['child']?.value()); readonly action = computed(() => this.props()['action']?.value()); @@ -112,4 +111,4 @@ export class ButtonComponent extends BasicCatalogComponent { } } } -} \ No newline at end of file +} diff --git a/renderers/angular/src/v0_9/catalog/basic/card.component.ts b/renderers/angular/src/v0_9/catalog/basic/card.component.ts index a269cd2f7..ab891b2a7 100644 --- a/renderers/angular/src/v0_9/catalog/basic/card.component.ts +++ b/renderers/angular/src/v0_9/catalog/basic/card.component.ts @@ -14,10 +14,10 @@ * limitations under the License. */ -import { Component, computed, ChangeDetectionStrategy } from '@angular/core'; -import { ComponentHostComponent } from '../../core/component-host.component'; -import { BasicCatalogComponent } from './basic-catalog-component'; -import { CardApi } from '@a2ui/web_core/v0_9/basic_catalog'; +import {Component, computed, ChangeDetectionStrategy} from '@angular/core'; +import {ComponentHostComponent} from '../../core/component-host.component'; +import {BasicCatalogComponent} from './basic-catalog-component'; +import {CardApi} from '@a2ui/web_core/v0_9/basic_catalog'; /** * Angular implementation of the A2UI Card component (v0.9). diff --git a/renderers/angular/src/v0_9/catalog/basic/check-box.component.ts b/renderers/angular/src/v0_9/catalog/basic/check-box.component.ts index 00824ce04..c864a6127 100644 --- a/renderers/angular/src/v0_9/catalog/basic/check-box.component.ts +++ b/renderers/angular/src/v0_9/catalog/basic/check-box.component.ts @@ -14,9 +14,9 @@ * limitations under the License. */ -import { Component, computed, ChangeDetectionStrategy } from '@angular/core'; -import { BasicCatalogComponent } from './basic-catalog-component'; -import { CheckBoxApi } from '@a2ui/web_core/v0_9/basic_catalog'; +import {Component, computed, ChangeDetectionStrategy} from '@angular/core'; +import {BasicCatalogComponent} from './basic-catalog-component'; +import {CheckBoxApi} from '@a2ui/web_core/v0_9/basic_catalog'; /** * Angular implementation of the A2UI CheckBox component (v0.9). diff --git a/renderers/angular/src/v0_9/catalog/basic/choice-picker.component.ts b/renderers/angular/src/v0_9/catalog/basic/choice-picker.component.ts index 5dd5d35bd..dd7280470 100644 --- a/renderers/angular/src/v0_9/catalog/basic/choice-picker.component.ts +++ b/renderers/angular/src/v0_9/catalog/basic/choice-picker.component.ts @@ -14,9 +14,9 @@ * limitations under the License. */ -import { Component, computed, ChangeDetectionStrategy } from '@angular/core'; -import { BasicCatalogComponent } from './basic-catalog-component'; -import { ChoicePickerApi } from '@a2ui/web_core/v0_9/basic_catalog'; +import {Component, computed, ChangeDetectionStrategy} from '@angular/core'; +import {BasicCatalogComponent} from './basic-catalog-component'; +import {ChoicePickerApi} from '@a2ui/web_core/v0_9/basic_catalog'; /** * Angular implementation of the A2UI ChoicePicker component (v0.9). @@ -128,9 +128,7 @@ import { ChoicePickerApi } from '@a2ui/web_core/v0_9/basic_catalog'; }) export class ChoicePickerComponent extends BasicCatalogComponent { readonly displayStyle = computed(() => this.props()['displayStyle']?.value()); - readonly options = computed( - () => this.props()['options']?.value() || [], - ); + readonly options = computed(() => this.props()['options']?.value() || []); readonly variant = computed(() => this.props()['variant']?.value()); readonly selectedValue = computed(() => this.props()['value']?.value()); diff --git a/renderers/angular/src/v0_9/catalog/basic/column.component.spec.ts b/renderers/angular/src/v0_9/catalog/basic/column.component.spec.ts index 7eb9a603e..d562f5e1d 100644 --- a/renderers/angular/src/v0_9/catalog/basic/column.component.spec.ts +++ b/renderers/angular/src/v0_9/catalog/basic/column.component.spec.ts @@ -14,13 +14,13 @@ * limitations under the License. */ -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { Component, input, signal } from '@angular/core'; -import { ColumnComponent } from './column.component'; -import { ComponentModel } from '@a2ui/web_core/v0_9'; -import { A2uiRendererService } from '../../core/a2ui-renderer.service'; -import { ComponentBinder } from '../../core/component-binder.service'; -import { By } from '@angular/platform-browser'; +import {ComponentFixture, TestBed} from '@angular/core/testing'; +import {Component, input, signal} from '@angular/core'; +import {ColumnComponent} from './column.component'; +import {ComponentModel} from '@a2ui/web_core/v0_9'; +import {A2uiRendererService} from '../../core/a2ui-renderer.service'; +import {ComponentBinder} from '../../core/component-binder.service'; +import {By} from '@angular/platform-browser'; @Component({ standalone: true, @@ -51,7 +51,7 @@ describe('ColumnComponent', () => { ]), catalog: { id: 'test-catalog', - components: new Map([['Child', { component: DummyChild }]]), + components: new Map([['Child', {component: DummyChild}]]), }, }; @@ -64,13 +64,13 @@ describe('ColumnComponent', () => { }; mockBinder = jasmine.createSpyObj('ComponentBinder', ['bind']); - mockBinder.bind.and.returnValue({ text: { value: () => 'bound' } }); + mockBinder.bind.and.returnValue({text: {value: () => 'bound'}}); await TestBed.configureTestingModule({ imports: [ColumnComponent], providers: [ - { provide: A2uiRendererService, useValue: mockRendererService }, - { provide: ComponentBinder, useValue: mockBinder }, + {provide: A2uiRendererService, useValue: mockRendererService}, + {provide: ComponentBinder, useValue: mockBinder}, ], }).compileComponents(); @@ -78,8 +78,8 @@ describe('ColumnComponent', () => { component = fixture.componentInstance; fixture.componentRef.setInput('surfaceId', 'surf1'); fixture.componentRef.setInput('props', { - justify: { value: signal('start'), raw: 'start', onUpdate: () => {} }, - align: { value: signal('stretch'), raw: 'stretch', onUpdate: () => {} }, + justify: {value: signal('start'), raw: 'start', onUpdate: () => {}}, + align: {value: signal('stretch'), raw: 'stretch', onUpdate: () => {}}, children: { value: signal(['child1', 'child2']), raw: ['child1', 'child2'], @@ -103,7 +103,7 @@ describe('ColumnComponent', () => { it('should apply flex style from weight prop', () => { fixture.componentRef.setInput('props', { ...component.props(), - weight: { value: signal(2), raw: 2, onUpdate: () => {} }, + weight: {value: signal(2), raw: 2, onUpdate: () => {}}, }); fixture.detectChanges(); const style = window.getComputedStyle(fixture.debugElement.nativeElement); @@ -113,7 +113,7 @@ describe('ColumnComponent', () => { it('should apply flex style from weight prop when value is 0', () => { fixture.componentRef.setInput('props', { ...component.props(), - weight: { value: signal(0), raw: 0, onUpdate: () => {} }, + weight: {value: signal(0), raw: 0, onUpdate: () => {}}, }); fixture.detectChanges(); const style = window.getComputedStyle(fixture.debugElement.nativeElement); @@ -123,7 +123,7 @@ describe('ColumnComponent', () => { it('should not apply flex style when weight prop is null', () => { fixture.componentRef.setInput('props', { ...component.props(), - weight: { value: signal(null), raw: null, onUpdate: () => {} }, + weight: {value: signal(null), raw: null, onUpdate: () => {}}, }); fixture.detectChanges(); const style = window.getComputedStyle(fixture.debugElement.nativeElement); @@ -135,8 +135,8 @@ describe('ColumnComponent', () => { fixture.detectChanges(); const hosts = fixture.debugElement.queryAll(By.css('a2ui-v09-component-host')); expect(hosts.length).toBe(2); - expect(hosts[0].componentInstance.componentKey()).toEqual({ id: 'child1', basePath: '/' }); - expect(hosts[1].componentInstance.componentKey()).toEqual({ id: 'child2', basePath: '/' }); + expect(hosts[0].componentInstance.componentKey()).toEqual({id: 'child1', basePath: '/'}); + expect(hosts[1].componentInstance.componentKey()).toEqual({id: 'child2', basePath: '/'}); }); it('should render repeating children', () => { @@ -181,8 +181,8 @@ describe('ColumnComponent', () => { it('should handle missing children property', () => { fixture.componentRef.setInput('props', { - justify: { value: signal('start'), raw: 'start', onUpdate: () => {} }, - align: { value: signal('stretch'), raw: 'stretch', onUpdate: () => {} }, + justify: {value: signal('start'), raw: 'start', onUpdate: () => {}}, + align: {value: signal('stretch'), raw: 'stretch', onUpdate: () => {}}, }); fixture.detectChanges(); const hosts = fixture.debugElement.queryAll(By.css('a2ui-v09-component-host')); diff --git a/renderers/angular/src/v0_9/catalog/basic/column.component.ts b/renderers/angular/src/v0_9/catalog/basic/column.component.ts index c5ef815c0..1723451d7 100644 --- a/renderers/angular/src/v0_9/catalog/basic/column.component.ts +++ b/renderers/angular/src/v0_9/catalog/basic/column.component.ts @@ -14,13 +14,13 @@ * limitations under the License. */ -import { Component, computed, ChangeDetectionStrategy } from '@angular/core'; -import { ComponentHostComponent } from '../../core/component-host.component'; +import {Component, computed, ChangeDetectionStrategy} from '@angular/core'; +import {ComponentHostComponent} from '../../core/component-host.component'; -import { getNormalizedPath } from '../../core/utils'; -import { BasicCatalogComponent } from './basic-catalog-component'; -import { JUSTIFY_MAP, ALIGN_MAP } from './utils'; -import { ColumnApi } from '@a2ui/web_core/v0_9/basic_catalog'; +import {getNormalizedPath} from '../../core/utils'; +import {BasicCatalogComponent} from './basic-catalog-component'; +import {JUSTIFY_MAP, ALIGN_MAP} from './utils'; +import {ColumnApi} from '@a2ui/web_core/v0_9/basic_catalog'; /** * Angular implementation of the A2UI Column component (v0.9). @@ -54,7 +54,7 @@ import { ColumnApi } from '@a2ui/web_core/v0_9/basic_catalog'; @if (isRepeating()) { @for (item of children(); track item; let i = $index) { @@ -88,11 +88,11 @@ export class ColumnComponent extends BasicCatalogComponent { protected readonly normalizedChildren = computed(() => { if (this.isRepeating()) return []; - return this.children().map((child) => { + return this.children().map(child => { if (typeof child === 'object' && child !== null && 'id' in child) { - return child as { id: string; basePath: string }; + return child as {id: string; basePath: string}; } - return { id: child as string, basePath: this.dataContextPath() }; + return {id: child as string, basePath: this.dataContextPath()}; }); }); diff --git a/renderers/angular/src/v0_9/catalog/basic/complex-components.spec.ts b/renderers/angular/src/v0_9/catalog/basic/complex-components.spec.ts index ed0495eb4..f9cad0419 100644 --- a/renderers/angular/src/v0_9/catalog/basic/complex-components.spec.ts +++ b/renderers/angular/src/v0_9/catalog/basic/complex-components.spec.ts @@ -14,20 +14,20 @@ * limitations under the License. */ -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { Component, signal as angularSignal, input } from '@angular/core'; -import { CheckBoxComponent } from './check-box.component'; -import { ChoicePickerComponent } from './choice-picker.component'; -import { SliderComponent } from './slider.component'; -import { DateTimeInputComponent } from './date-time-input.component'; -import { ListComponent } from './list.component'; -import { TabsComponent } from './tabs.component'; -import { ComponentModel } from '@a2ui/web_core/v0_9'; -import { ModalComponent } from './modal.component'; -import { BoundProperty } from '../../core/types'; -import { A2uiRendererService } from '../../core/a2ui-renderer.service'; -import { ComponentBinder } from '../../core/component-binder.service'; -import { By } from '@angular/platform-browser'; +import {ComponentFixture, TestBed} from '@angular/core/testing'; +import {Component, signal as angularSignal, input} from '@angular/core'; +import {CheckBoxComponent} from './check-box.component'; +import {ChoicePickerComponent} from './choice-picker.component'; +import {SliderComponent} from './slider.component'; +import {DateTimeInputComponent} from './date-time-input.component'; +import {ListComponent} from './list.component'; +import {TabsComponent} from './tabs.component'; +import {ComponentModel} from '@a2ui/web_core/v0_9'; +import {ModalComponent} from './modal.component'; +import {BoundProperty} from '../../core/types'; +import {A2uiRendererService} from '../../core/a2ui-renderer.service'; +import {ComponentBinder} from '../../core/component-binder.service'; +import {By} from '@angular/platform-browser'; describe('Complex Components', () => { let mockRendererService: any; @@ -38,25 +38,19 @@ describe('Complex Components', () => { surfaceGroup: { getSurface: jasmine.createSpy('getSurface').and.returnValue({ componentsModel: new Map([ - ['child-1', new ComponentModel('child-1', 'Text', { text: { value: 'Child 1' } })], - ['child-2', new ComponentModel('child-2', 'Text', { text: { value: 'Child 2' } })], - [ - 'content-1', - new ComponentModel('content-1', 'Text', { text: { value: 'Content 1' } }), - ], - [ - 'content-2', - new ComponentModel('content-2', 'Text', { text: { value: 'Content 2' } }), - ], - ['trigger-btn', new ComponentModel('trigger-btn', 'Text', { text: { value: 'Open' } })], + ['child-1', new ComponentModel('child-1', 'Text', {text: {value: 'Child 1'}})], + ['child-2', new ComponentModel('child-2', 'Text', {text: {value: 'Child 2'}})], + ['content-1', new ComponentModel('content-1', 'Text', {text: {value: 'Content 1'}})], + ['content-2', new ComponentModel('content-2', 'Text', {text: {value: 'Content 2'}})], + ['trigger-btn', new ComponentModel('trigger-btn', 'Text', {text: {value: 'Open'}})], [ 'modal-content', - new ComponentModel('modal-content', 'Text', { text: { value: 'Modal' } }), + new ComponentModel('modal-content', 'Text', {text: {value: 'Modal'}}), ], ]), catalog: { id: 'mock-catalog', - components: new Map([['Text', { type: 'Text', component: DummyTextComponent }]]), + components: new Map([['Text', {type: 'Text', component: DummyTextComponent}]]), }, }), }, @@ -93,8 +87,8 @@ describe('Complex Components', () => { await TestBed.configureTestingModule({ imports: [CheckBoxComponent], providers: [ - { provide: A2uiRendererService, useValue: mockRendererService }, - { provide: ComponentBinder, useValue: mockBinder }, + {provide: A2uiRendererService, useValue: mockRendererService}, + {provide: ComponentBinder, useValue: mockBinder}, ], }).compileComponents(); @@ -124,7 +118,7 @@ describe('Complex Components', () => { const onUpdateSpy = jasmine.createSpy('onUpdate'); fixture.componentRef.setInput('props', { label: createBoundProperty('Check me'), - value: { value: angularSignal(false), raw: false, onUpdate: onUpdateSpy }, + value: {value: angularSignal(false), raw: false, onUpdate: onUpdateSpy}, }); fixture.detectChanges(); const input = fixture.nativeElement.querySelector('input'); @@ -138,9 +132,9 @@ describe('Complex Components', () => { value: createBoundProperty(true), }); mockRendererService.surfaceGroup.getSurface.and.returnValue({ - theme: { primaryColor: 'rgb(255, 0, 0)' }, + theme: {primaryColor: 'rgb(255, 0, 0)'}, componentsModel: new Map(), - catalog: { components: new Map() }, + catalog: {components: new Map()}, }); fixture.detectChanges(); @@ -159,8 +153,8 @@ describe('Complex Components', () => { await TestBed.configureTestingModule({ imports: [ChoicePickerComponent], providers: [ - { provide: A2uiRendererService, useValue: mockRendererService }, - { provide: ComponentBinder, useValue: mockBinder }, + {provide: A2uiRendererService, useValue: mockRendererService}, + {provide: ComponentBinder, useValue: mockBinder}, ], }).compileComponents(); @@ -180,8 +174,8 @@ describe('Complex Components', () => { fixture.componentRef.setInput('props', { label: createBoundProperty('Pick one'), options: createBoundProperty([ - { label: 'Opt 1', value: '1' }, - { label: 'Opt 2', value: '2' }, + {label: 'Opt 1', value: '1'}, + {label: 'Opt 2', value: '2'}, ]), value: createBoundProperty('1'), variant: createBoundProperty('mutuallyExclusive'), @@ -198,10 +192,10 @@ describe('Complex Components', () => { fixture.componentRef.setInput('props', { label: createBoundProperty('Pick one'), options: createBoundProperty([ - { label: 'Opt 1', value: '1' }, - { label: 'Opt 2', value: '2' }, + {label: 'Opt 1', value: '1'}, + {label: 'Opt 2', value: '2'}, ]), - value: { value: angularSignal('1'), raw: '1', onUpdate: onUpdateSpy }, + value: {value: angularSignal('1'), raw: '1', onUpdate: onUpdateSpy}, variant: createBoundProperty('mutuallyExclusive'), displayStyle: createBoundProperty('checkbox'), }); @@ -215,10 +209,10 @@ describe('Complex Components', () => { const onUpdateSpy = jasmine.createSpy('onUpdate'); fixture.componentRef.setInput('props', { options: createBoundProperty([ - { label: 'Chip 1', value: 'c1' }, - { label: 'Chip 2', value: 'c2' }, + {label: 'Chip 1', value: 'c1'}, + {label: 'Chip 2', value: 'c2'}, ]), - value: { value: angularSignal(['c1']), raw: ['c1'], onUpdate: onUpdateSpy }, + value: {value: angularSignal(['c1']), raw: ['c1'], onUpdate: onUpdateSpy}, variant: createBoundProperty('multipleSelection'), displayStyle: createBoundProperty('chips'), }); @@ -244,8 +238,8 @@ describe('Complex Components', () => { await TestBed.configureTestingModule({ imports: [SliderComponent], providers: [ - { provide: A2uiRendererService, useValue: mockRendererService }, - { provide: ComponentBinder, useValue: mockBinder }, + {provide: A2uiRendererService, useValue: mockRendererService}, + {provide: ComponentBinder, useValue: mockBinder}, ], }).compileComponents(); @@ -276,7 +270,7 @@ describe('Complex Components', () => { it('should call onUpdate when slider value changes', () => { const onUpdateSpy = jasmine.createSpy('onUpdate'); fixture.componentRef.setInput('props', { - value: { value: angularSignal(50), raw: 50, onUpdate: onUpdateSpy }, + value: {value: angularSignal(50), raw: 50, onUpdate: onUpdateSpy}, }); fixture.detectChanges(); const input = fixture.nativeElement.querySelector('input'); @@ -294,8 +288,8 @@ describe('Complex Components', () => { await TestBed.configureTestingModule({ imports: [DateTimeInputComponent], providers: [ - { provide: A2uiRendererService, useValue: mockRendererService }, - { provide: ComponentBinder, useValue: mockBinder }, + {provide: A2uiRendererService, useValue: mockRendererService}, + {provide: ComponentBinder, useValue: mockBinder}, ], }).compileComponents(); @@ -372,8 +366,8 @@ describe('Complex Components', () => { await TestBed.configureTestingModule({ imports: [ListComponent], providers: [ - { provide: A2uiRendererService, useValue: mockRendererService }, - { provide: ComponentBinder, useValue: mockBinder }, + {provide: A2uiRendererService, useValue: mockRendererService}, + {provide: ComponentBinder, useValue: mockBinder}, ], }).compileComponents(); @@ -459,8 +453,8 @@ describe('Complex Components', () => { await TestBed.configureTestingModule({ imports: [TabsComponent], providers: [ - { provide: A2uiRendererService, useValue: mockRendererService }, - { provide: ComponentBinder, useValue: mockBinder }, + {provide: A2uiRendererService, useValue: mockRendererService}, + {provide: ComponentBinder, useValue: mockBinder}, ], }).compileComponents(); @@ -478,8 +472,8 @@ describe('Complex Components', () => { it('should render tabs and switch content', () => { fixture.componentRef.setInput('props', { tabs: createBoundProperty([ - { title: 'Tab 1', child: 'content-1' }, - { title: 'Tab 2', child: 'content-2' }, + {title: 'Tab 1', child: 'content-1'}, + {title: 'Tab 2', child: 'content-2'}, ]), }); fixture.detectChanges(); @@ -488,12 +482,12 @@ describe('Complex Components', () => { expect(tabs[0].textContent).toContain('Tab 1'); let host = fixture.debugElement.query(By.css('a2ui-v09-component-host')); - expect(host.componentInstance.componentKey()).toEqual({ id: 'content-1', basePath: '/' }); + expect(host.componentInstance.componentKey()).toEqual({id: 'content-1', basePath: '/'}); tabs[1].click(); fixture.detectChanges(); host = fixture.debugElement.query(By.css('a2ui-v09-component-host')); - expect(host.componentInstance.componentKey()).toEqual({ id: 'content-2', basePath: '/' }); + expect(host.componentInstance.componentKey()).toEqual({id: 'content-2', basePath: '/'}); }); it('should handle missing tabs property', () => { @@ -521,8 +515,8 @@ describe('Complex Components', () => { await TestBed.configureTestingModule({ imports: [ModalComponent], providers: [ - { provide: A2uiRendererService, useValue: mockRendererService }, - { provide: ComponentBinder, useValue: mockBinder }, + {provide: A2uiRendererService, useValue: mockRendererService}, + {provide: ComponentBinder, useValue: mockBinder}, ], }).compileComponents(); @@ -539,8 +533,8 @@ describe('Complex Components', () => { it('should render trigger and open modal on click', () => { fixture.componentRef.setInput('props', { - trigger: createBoundProperty({ id: 'trigger-btn', basePath: '/' }), - content: createBoundProperty({ id: 'modal-content', basePath: '/' }), + trigger: createBoundProperty({id: 'trigger-btn', basePath: '/'}), + content: createBoundProperty({id: 'modal-content', basePath: '/'}), }); fixture.detectChanges(); const triggerHost = fixture.debugElement.query( @@ -569,8 +563,8 @@ describe('Complex Components', () => { it('should close modal when close button clicked', () => { fixture.componentRef.setInput('props', { - trigger: createBoundProperty({ id: 'trigger-btn', basePath: '/' }), - content: createBoundProperty({ id: 'modal-content', basePath: '/' }), + trigger: createBoundProperty({id: 'trigger-btn', basePath: '/'}), + content: createBoundProperty({id: 'modal-content', basePath: '/'}), }); fixture.detectChanges(); @@ -585,8 +579,8 @@ describe('Complex Components', () => { it('should close modal when overlay clicked', () => { fixture.componentRef.setInput('props', { - trigger: createBoundProperty({ id: 'trigger-btn', basePath: '/' }), - content: createBoundProperty({ id: 'modal-content', basePath: '/' }), + trigger: createBoundProperty({id: 'trigger-btn', basePath: '/'}), + content: createBoundProperty({id: 'modal-content', basePath: '/'}), }); fixture.detectChanges(); diff --git a/renderers/angular/src/v0_9/catalog/basic/date-time-input.component.ts b/renderers/angular/src/v0_9/catalog/basic/date-time-input.component.ts index 211143ac9..88beba62b 100644 --- a/renderers/angular/src/v0_9/catalog/basic/date-time-input.component.ts +++ b/renderers/angular/src/v0_9/catalog/basic/date-time-input.component.ts @@ -14,9 +14,9 @@ * limitations under the License. */ -import { Component, computed, ChangeDetectionStrategy } from '@angular/core'; -import { BasicCatalogComponent } from './basic-catalog-component'; -import { DateTimeInputApi } from '@a2ui/web_core/v0_9/basic_catalog'; +import {Component, computed, ChangeDetectionStrategy} from '@angular/core'; +import {BasicCatalogComponent} from './basic-catalog-component'; +import {DateTimeInputApi} from '@a2ui/web_core/v0_9/basic_catalog'; /** * Angular implementation of the A2UI DateTimeInput component (v0.9). diff --git a/renderers/angular/src/v0_9/catalog/basic/divider.component.ts b/renderers/angular/src/v0_9/catalog/basic/divider.component.ts index c790e078c..106b1ad85 100644 --- a/renderers/angular/src/v0_9/catalog/basic/divider.component.ts +++ b/renderers/angular/src/v0_9/catalog/basic/divider.component.ts @@ -14,9 +14,9 @@ * limitations under the License. */ -import { Component, computed, ChangeDetectionStrategy } from '@angular/core'; -import { BasicCatalogComponent } from './basic-catalog-component'; -import { DividerApi } from '@a2ui/web_core/v0_9/basic_catalog'; +import {Component, computed, ChangeDetectionStrategy} from '@angular/core'; +import {BasicCatalogComponent} from './basic-catalog-component'; +import {DividerApi} from '@a2ui/web_core/v0_9/basic_catalog'; /** * Angular implementation of the A2UI Divider component (v0.9). diff --git a/renderers/angular/src/v0_9/catalog/basic/icon.component.ts b/renderers/angular/src/v0_9/catalog/basic/icon.component.ts index bb4934c9b..18b508396 100644 --- a/renderers/angular/src/v0_9/catalog/basic/icon.component.ts +++ b/renderers/angular/src/v0_9/catalog/basic/icon.component.ts @@ -14,10 +14,10 @@ * limitations under the License. */ -import { Component, computed, ChangeDetectionStrategy } from '@angular/core'; -import { BasicCatalogComponent } from './basic-catalog-component'; -import { IconApi } from '@a2ui/web_core/v0_9/basic_catalog'; -import { AnyDuringSchemaAlignment } from '../types'; +import {Component, computed, ChangeDetectionStrategy} from '@angular/core'; +import {BasicCatalogComponent} from './basic-catalog-component'; +import {IconApi} from '@a2ui/web_core/v0_9/basic_catalog'; +import {AnyDuringSchemaAlignment} from '../types'; const ICON_NAME_OVERRIDES: Record = { play: 'play_arrow', @@ -103,6 +103,6 @@ export class IconComponent extends BasicCatalogComponent { if (typeof name !== 'string') return ''; if (ICON_NAME_OVERRIDES[name]) return ICON_NAME_OVERRIDES[name]; // Convert camelCase to snake_case for Material Icons - return name.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`); + return name.replace(/[A-Z]/g, letter => `_${letter.toLowerCase()}`); }); } diff --git a/renderers/angular/src/v0_9/catalog/basic/image.component.ts b/renderers/angular/src/v0_9/catalog/basic/image.component.ts index 0408b9ff3..bb8b48e53 100644 --- a/renderers/angular/src/v0_9/catalog/basic/image.component.ts +++ b/renderers/angular/src/v0_9/catalog/basic/image.component.ts @@ -14,9 +14,9 @@ * limitations under the License. */ -import { Component, computed, ChangeDetectionStrategy } from '@angular/core'; -import { BasicCatalogComponent } from './basic-catalog-component'; -import { ImageApi } from '@a2ui/web_core/v0_9/basic_catalog'; +import {Component, computed, ChangeDetectionStrategy} from '@angular/core'; +import {BasicCatalogComponent} from './basic-catalog-component'; +import {ImageApi} from '@a2ui/web_core/v0_9/basic_catalog'; /** * Angular implementation of the A2UI Image component (v0.9). diff --git a/renderers/angular/src/v0_9/catalog/basic/list.component.ts b/renderers/angular/src/v0_9/catalog/basic/list.component.ts index 50d31ec5a..f334836a6 100644 --- a/renderers/angular/src/v0_9/catalog/basic/list.component.ts +++ b/renderers/angular/src/v0_9/catalog/basic/list.component.ts @@ -14,11 +14,11 @@ * limitations under the License. */ -import { Component, computed, ChangeDetectionStrategy } from '@angular/core'; -import { ComponentHostComponent } from '../../core/component-host.component'; -import { BasicCatalogComponent } from './basic-catalog-component'; -import { Child } from '../../core/component-binder.service'; -import { ListApi } from '@a2ui/web_core/v0_9/basic_catalog'; +import {Component, computed, ChangeDetectionStrategy} from '@angular/core'; +import {ComponentHostComponent} from '../../core/component-host.component'; +import {BasicCatalogComponent} from './basic-catalog-component'; +import {Child} from '../../core/component-binder.service'; +import {ListApi} from '@a2ui/web_core/v0_9/basic_catalog'; /** * Angular implementation of the A2UI List component (v0.9). diff --git a/renderers/angular/src/v0_9/catalog/basic/modal.component.ts b/renderers/angular/src/v0_9/catalog/basic/modal.component.ts index 547c01ead..612e242b6 100644 --- a/renderers/angular/src/v0_9/catalog/basic/modal.component.ts +++ b/renderers/angular/src/v0_9/catalog/basic/modal.component.ts @@ -14,10 +14,10 @@ * limitations under the License. */ -import { Component, computed, ChangeDetectionStrategy, signal } from '@angular/core'; -import { ComponentHostComponent } from '../../core/component-host.component'; -import { BasicCatalogComponent } from './basic-catalog-component'; -import { ModalApi } from '@a2ui/web_core/v0_9/basic_catalog'; +import {Component, computed, ChangeDetectionStrategy, signal} from '@angular/core'; +import {ComponentHostComponent} from '../../core/component-host.component'; +import {BasicCatalogComponent} from './basic-catalog-component'; +import {ModalApi} from '@a2ui/web_core/v0_9/basic_catalog'; /** * Angular implementation of the A2UI Modal component (v0.9). diff --git a/renderers/angular/src/v0_9/catalog/basic/row.component.spec.ts b/renderers/angular/src/v0_9/catalog/basic/row.component.spec.ts index 175b47c7e..2b7e59c23 100644 --- a/renderers/angular/src/v0_9/catalog/basic/row.component.spec.ts +++ b/renderers/angular/src/v0_9/catalog/basic/row.component.spec.ts @@ -14,13 +14,13 @@ * limitations under the License. */ -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { Component, input, signal } from '@angular/core'; -import { RowComponent } from './row.component'; -import { ComponentModel } from '@a2ui/web_core/v0_9'; -import { A2uiRendererService } from '../../core/a2ui-renderer.service'; -import { ComponentBinder } from '../../core/component-binder.service'; -import { By } from '@angular/platform-browser'; +import {ComponentFixture, TestBed} from '@angular/core/testing'; +import {Component, input, signal} from '@angular/core'; +import {RowComponent} from './row.component'; +import {ComponentModel} from '@a2ui/web_core/v0_9'; +import {A2uiRendererService} from '../../core/a2ui-renderer.service'; +import {ComponentBinder} from '../../core/component-binder.service'; +import {By} from '@angular/platform-browser'; @Component({ standalone: true, @@ -51,7 +51,7 @@ describe('RowComponent', () => { ]), catalog: { id: 'test-catalog', - components: new Map([['Child', { component: DummyChild }]]), + components: new Map([['Child', {component: DummyChild}]]), }, }; @@ -64,13 +64,13 @@ describe('RowComponent', () => { }; mockBinder = jasmine.createSpyObj('ComponentBinder', ['bind']); - mockBinder.bind.and.returnValue({ text: { value: () => 'bound' } }); + mockBinder.bind.and.returnValue({text: {value: () => 'bound'}}); await TestBed.configureTestingModule({ imports: [RowComponent], providers: [ - { provide: A2uiRendererService, useValue: mockRendererService }, - { provide: ComponentBinder, useValue: mockBinder }, + {provide: A2uiRendererService, useValue: mockRendererService}, + {provide: ComponentBinder, useValue: mockBinder}, ], }).compileComponents(); @@ -78,8 +78,8 @@ describe('RowComponent', () => { component = fixture.componentInstance; fixture.componentRef.setInput('surfaceId', 'surf1'); fixture.componentRef.setInput('props', { - justify: { value: signal('center'), raw: 'center', onUpdate: () => {} }, - align: { value: signal('baseline'), raw: 'baseline', onUpdate: () => {} }, + justify: {value: signal('center'), raw: 'center', onUpdate: () => {}}, + align: {value: signal('baseline'), raw: 'baseline', onUpdate: () => {}}, children: { value: signal(['child1', 'child2']), raw: ['child1', 'child2'], @@ -104,8 +104,8 @@ describe('RowComponent', () => { fixture.detectChanges(); const hosts = fixture.debugElement.queryAll(By.css('a2ui-v09-component-host')); expect(hosts.length).toBe(2); - expect(hosts[0].componentInstance.componentKey()).toEqual({ id: 'child1', basePath: '/' }); - expect(hosts[1].componentInstance.componentKey()).toEqual({ id: 'child2', basePath: '/' }); + expect(hosts[0].componentInstance.componentKey()).toEqual({id: 'child1', basePath: '/'}); + expect(hosts[1].componentInstance.componentKey()).toEqual({id: 'child2', basePath: '/'}); }); it('should render repeating children', () => { @@ -150,8 +150,8 @@ describe('RowComponent', () => { it('should handle missing children property', () => { fixture.componentRef.setInput('props', { - justify: { value: signal('center'), raw: 'center', onUpdate: () => {} }, - align: { value: signal('baseline'), raw: 'baseline', onUpdate: () => {} }, + justify: {value: signal('center'), raw: 'center', onUpdate: () => {}}, + align: {value: signal('baseline'), raw: 'baseline', onUpdate: () => {}}, }); fixture.detectChanges(); const hosts = fixture.debugElement.queryAll(By.css('a2ui-v09-component-host')); diff --git a/renderers/angular/src/v0_9/catalog/basic/row.component.ts b/renderers/angular/src/v0_9/catalog/basic/row.component.ts index f38dd3f0d..4f1a9282e 100644 --- a/renderers/angular/src/v0_9/catalog/basic/row.component.ts +++ b/renderers/angular/src/v0_9/catalog/basic/row.component.ts @@ -14,12 +14,12 @@ * limitations under the License. */ -import { Component, computed, ChangeDetectionStrategy } from '@angular/core'; -import { ComponentHostComponent } from '../../core/component-host.component'; -import { getNormalizedPath } from '../../core/utils'; -import { BasicCatalogComponent } from './basic-catalog-component'; -import { JUSTIFY_MAP, ALIGN_MAP } from './utils'; -import { RowApi } from '@a2ui/web_core/v0_9/basic_catalog'; +import {Component, computed, ChangeDetectionStrategy} from '@angular/core'; +import {ComponentHostComponent} from '../../core/component-host.component'; +import {getNormalizedPath} from '../../core/utils'; +import {BasicCatalogComponent} from './basic-catalog-component'; +import {JUSTIFY_MAP, ALIGN_MAP} from './utils'; +import {RowApi} from '@a2ui/web_core/v0_9/basic_catalog'; /** * Angular implementation of the A2UI Row component (v0.9). @@ -52,7 +52,7 @@ import { RowApi } from '@a2ui/web_core/v0_9/basic_catalog'; @if (isRepeating()) { @for (item of children(); track item; let i = $index) { @@ -86,11 +86,11 @@ export class RowComponent extends BasicCatalogComponent { protected readonly normalizedChildren = computed(() => { if (this.isRepeating()) return []; - return this.children().map((child) => { + return this.children().map(child => { if (typeof child === 'object' && child !== null && 'id' in child) { - return child as { id: string; basePath: string }; + return child as {id: string; basePath: string}; } - return { id: child as string, basePath: this.dataContextPath() }; + return {id: child as string, basePath: this.dataContextPath()}; }); }); diff --git a/renderers/angular/src/v0_9/catalog/basic/simple-components.spec.ts b/renderers/angular/src/v0_9/catalog/basic/simple-components.spec.ts index 8961251c1..a37bf3f14 100644 --- a/renderers/angular/src/v0_9/catalog/basic/simple-components.spec.ts +++ b/renderers/angular/src/v0_9/catalog/basic/simple-components.spec.ts @@ -14,19 +14,19 @@ * limitations under the License. */ -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { Component, signal as angularSignal, input } from '@angular/core'; -import { By } from '@angular/platform-browser'; -import { DividerComponent } from './divider.component'; -import { ImageComponent } from './image.component'; -import { IconComponent } from './icon.component'; -import { VideoComponent } from './video.component'; -import { AudioPlayerComponent } from './audio-player.component'; -import { ComponentModel } from '@a2ui/web_core/v0_9'; -import { CardComponent } from './card.component'; -import { BoundProperty } from '../../core/types'; -import { A2uiRendererService } from '../../core/a2ui-renderer.service'; -import { ComponentBinder } from '../../core/component-binder.service'; +import {ComponentFixture, TestBed} from '@angular/core/testing'; +import {Component, signal as angularSignal, input} from '@angular/core'; +import {By} from '@angular/platform-browser'; +import {DividerComponent} from './divider.component'; +import {ImageComponent} from './image.component'; +import {IconComponent} from './icon.component'; +import {VideoComponent} from './video.component'; +import {AudioPlayerComponent} from './audio-player.component'; +import {ComponentModel} from '@a2ui/web_core/v0_9'; +import {CardComponent} from './card.component'; +import {BoundProperty} from '../../core/types'; +import {A2uiRendererService} from '../../core/a2ui-renderer.service'; +import {ComponentBinder} from '../../core/component-binder.service'; describe('Simple Components', () => { let mockRendererService: any; @@ -37,25 +37,19 @@ describe('Simple Components', () => { surfaceGroup: { getSurface: jasmine.createSpy('getSurface').and.returnValue({ componentsModel: new Map([ - ['child-1', new ComponentModel('child-1', 'Text', { text: { value: 'Child 1' } })], - ['child-2', new ComponentModel('child-2', 'Text', { text: { value: 'Child 2' } })], - [ - 'content-1', - new ComponentModel('content-1', 'Text', { text: { value: 'Content 1' } }), - ], - [ - 'content-2', - new ComponentModel('content-2', 'Text', { text: { value: 'Content 2' } }), - ], - ['trigger-btn', new ComponentModel('trigger-btn', 'Text', { text: { value: 'Open' } })], + ['child-1', new ComponentModel('child-1', 'Text', {text: {value: 'Child 1'}})], + ['child-2', new ComponentModel('child-2', 'Text', {text: {value: 'Child 2'}})], + ['content-1', new ComponentModel('content-1', 'Text', {text: {value: 'Content 1'}})], + ['content-2', new ComponentModel('content-2', 'Text', {text: {value: 'Content 2'}})], + ['trigger-btn', new ComponentModel('trigger-btn', 'Text', {text: {value: 'Open'}})], [ 'modal-content', - new ComponentModel('modal-content', 'Text', { text: { value: 'Modal' } }), + new ComponentModel('modal-content', 'Text', {text: {value: 'Modal'}}), ], ]), catalog: { id: 'mock-catalog', - components: new Map([['Text', { type: 'Text', component: DummyTextComponent }]]), + components: new Map([['Text', {type: 'Text', component: DummyTextComponent}]]), }, }), }, @@ -92,8 +86,8 @@ describe('Simple Components', () => { await TestBed.configureTestingModule({ imports: [DividerComponent], providers: [ - { provide: A2uiRendererService, useValue: mockRendererService }, - { provide: ComponentBinder, useValue: mockBinder }, + {provide: A2uiRendererService, useValue: mockRendererService}, + {provide: ComponentBinder, useValue: mockBinder}, ], }).compileComponents(); }); @@ -137,8 +131,8 @@ describe('Simple Components', () => { await TestBed.configureTestingModule({ imports: [ImageComponent], providers: [ - { provide: A2uiRendererService, useValue: mockRendererService }, - { provide: ComponentBinder, useValue: mockBinder }, + {provide: A2uiRendererService, useValue: mockRendererService}, + {provide: ComponentBinder, useValue: mockBinder}, ], }).compileComponents(); }); @@ -207,8 +201,8 @@ describe('Simple Components', () => { await TestBed.configureTestingModule({ imports: [IconComponent], providers: [ - { provide: A2uiRendererService, useValue: mockRendererService }, - { provide: ComponentBinder, useValue: mockBinder }, + {provide: A2uiRendererService, useValue: mockRendererService}, + {provide: ComponentBinder, useValue: mockBinder}, ], }).compileComponents(); }); @@ -281,7 +275,7 @@ describe('Simple Components', () => { it('should render path icon', () => { fixture.componentRef.setInput('props', { - name: createBoundProperty({ path: 'M10 10...' }), + name: createBoundProperty({path: 'M10 10...'}), }); fixture.detectChanges(); const svg = fixture.nativeElement.querySelector('svg'); @@ -297,8 +291,8 @@ describe('Simple Components', () => { await TestBed.configureTestingModule({ imports: [VideoComponent], providers: [ - { provide: A2uiRendererService, useValue: mockRendererService }, - { provide: ComponentBinder, useValue: mockBinder }, + {provide: A2uiRendererService, useValue: mockRendererService}, + {provide: ComponentBinder, useValue: mockBinder}, ], }).compileComponents(); }); @@ -342,8 +336,8 @@ describe('Simple Components', () => { await TestBed.configureTestingModule({ imports: [AudioPlayerComponent], providers: [ - { provide: A2uiRendererService, useValue: mockRendererService }, - { provide: ComponentBinder, useValue: mockBinder }, + {provide: A2uiRendererService, useValue: mockRendererService}, + {provide: ComponentBinder, useValue: mockBinder}, ], }).compileComponents(); }); @@ -397,8 +391,8 @@ describe('Simple Components', () => { await TestBed.configureTestingModule({ imports: [CardComponent], providers: [ - { provide: A2uiRendererService, useValue: mockRendererService }, - { provide: ComponentBinder, useValue: mockBinder }, + {provide: A2uiRendererService, useValue: mockRendererService}, + {provide: ComponentBinder, useValue: mockBinder}, ], }).compileComponents(); }); @@ -417,12 +411,12 @@ describe('Simple Components', () => { it('should render component-host for child', () => { fixture.componentRef.setInput('props', { - child: createBoundProperty({ id: 'child-1', basePath: '/' }), + child: createBoundProperty({id: 'child-1', basePath: '/'}), }); fixture.detectChanges(); const host = fixture.debugElement.query(By.css('a2ui-v09-component-host')); expect(host).toBeTruthy(); - expect(host.componentInstance.componentKey()).toEqual({ id: 'child-1', basePath: '/' }); + expect(host.componentInstance.componentKey()).toEqual({id: 'child-1', basePath: '/'}); }); }); }); diff --git a/renderers/angular/src/v0_9/catalog/basic/slider.component.ts b/renderers/angular/src/v0_9/catalog/basic/slider.component.ts index e7489a48d..2b7a44d1e 100644 --- a/renderers/angular/src/v0_9/catalog/basic/slider.component.ts +++ b/renderers/angular/src/v0_9/catalog/basic/slider.component.ts @@ -14,9 +14,9 @@ * limitations under the License. */ -import { Component, computed, ChangeDetectionStrategy } from '@angular/core'; -import { BasicCatalogComponent } from './basic-catalog-component'; -import { SliderApi } from '@a2ui/web_core/v0_9/basic_catalog'; +import {Component, computed, ChangeDetectionStrategy} from '@angular/core'; +import {BasicCatalogComponent} from './basic-catalog-component'; +import {SliderApi} from '@a2ui/web_core/v0_9/basic_catalog'; /** * Angular implementation of the A2UI Slider component (v0.9). diff --git a/renderers/angular/src/v0_9/catalog/basic/tabs.component.ts b/renderers/angular/src/v0_9/catalog/basic/tabs.component.ts index a7f8c5ff7..de96e3656 100644 --- a/renderers/angular/src/v0_9/catalog/basic/tabs.component.ts +++ b/renderers/angular/src/v0_9/catalog/basic/tabs.component.ts @@ -14,10 +14,10 @@ * limitations under the License. */ -import { Component, computed, ChangeDetectionStrategy, signal } from '@angular/core'; -import { ComponentHostComponent } from '../../core/component-host.component'; -import { BasicCatalogComponent } from './basic-catalog-component'; -import { TabsApi } from '@a2ui/web_core/v0_9/basic_catalog'; +import {Component, computed, ChangeDetectionStrategy, signal} from '@angular/core'; +import {ComponentHostComponent} from '../../core/component-host.component'; +import {BasicCatalogComponent} from './basic-catalog-component'; +import {TabsApi} from '@a2ui/web_core/v0_9/basic_catalog'; /** * Angular implementation of the A2UI Tabs component (v0.9). @@ -105,9 +105,9 @@ export class TabsComponent extends BasicCatalogComponent { const child = this.activeTab()?.child; if (!child) return null; if (typeof child === 'object' && child !== null && 'id' in child) { - return child as { id: string; basePath: string }; + return child as {id: string; basePath: string}; } - return { id: child as string, basePath: this.dataContextPath() }; + return {id: child as string, basePath: this.dataContextPath()}; }); setActiveTab(index: number) { diff --git a/renderers/angular/src/v0_9/catalog/basic/text-field.component.spec.ts b/renderers/angular/src/v0_9/catalog/basic/text-field.component.spec.ts index f010f5af0..4faaac0a0 100644 --- a/renderers/angular/src/v0_9/catalog/basic/text-field.component.spec.ts +++ b/renderers/angular/src/v0_9/catalog/basic/text-field.component.spec.ts @@ -14,11 +14,11 @@ * limitations under the License. */ -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { TextFieldComponent } from './text-field.component'; -import { signal } from '@angular/core'; -import { A2uiRendererService, A2UI_RENDERER_CONFIG } from '../../core/a2ui-renderer.service'; -import { By } from '@angular/platform-browser'; +import {ComponentFixture, TestBed} from '@angular/core/testing'; +import {TextFieldComponent} from './text-field.component'; +import {signal} from '@angular/core'; +import {A2uiRendererService, A2UI_RENDERER_CONFIG} from '../../core/a2ui-renderer.service'; +import {By} from '@angular/platform-browser'; describe('TextFieldComponent', () => { let component: TextFieldComponent; @@ -27,24 +27,21 @@ describe('TextFieldComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ imports: [TextFieldComponent], - providers: [ - A2uiRendererService, - { provide: A2UI_RENDERER_CONFIG, useValue: { catalogs: [] } }, - ], + providers: [A2uiRendererService, {provide: A2UI_RENDERER_CONFIG, useValue: {catalogs: []}}], }).compileComponents(); fixture = TestBed.createComponent(TextFieldComponent); component = fixture.componentInstance; fixture.componentRef.setInput('surfaceId', 'surf1'); fixture.componentRef.setInput('props', { - label: { value: signal('Username'), raw: 'Username', onUpdate: () => {} }, + label: {value: signal('Username'), raw: 'Username', onUpdate: () => {}}, value: { value: signal('testuser'), raw: 'testuser', onUpdate: jasmine.createSpy('onUpdate'), }, - placeholder: { value: signal('Enter username'), raw: 'Enter username', onUpdate: () => {} }, - variant: { value: signal('text'), raw: 'text', onUpdate: () => {} }, + placeholder: {value: signal('Enter username'), raw: 'Enter username', onUpdate: () => {}}, + variant: {value: signal('text'), raw: 'text', onUpdate: () => {}}, }); }); @@ -62,7 +59,7 @@ describe('TextFieldComponent', () => { it('should not render label if not provided', () => { fixture.componentRef.setInput('props', { ...component.props(), - label: { value: signal(null), raw: null, onUpdate: () => {} }, + label: {value: signal(null), raw: null, onUpdate: () => {}}, }); fixture.detectChanges(); const label = fixture.debugElement.query(By.css('label')); @@ -81,13 +78,13 @@ describe('TextFieldComponent', () => { fixture.componentRef.setInput('props', { ...component.props(), - variant: { value: signal('obscured'), raw: 'obscured', onUpdate: () => {} }, + variant: {value: signal('obscured'), raw: 'obscured', onUpdate: () => {}}, }); expect(component.inputType()).toBe('password'); fixture.componentRef.setInput('props', { ...component.props(), - variant: { value: signal('number'), raw: 'number', onUpdate: () => {} }, + variant: {value: signal('number'), raw: 'number', onUpdate: () => {}}, }); expect(component.inputType()).toBe('number'); }); @@ -96,7 +93,7 @@ describe('TextFieldComponent', () => { fixture.detectChanges(); const input = fixture.debugElement.query(By.css('input')); input.nativeElement.value = 'newuser'; - input.triggerEventHandler('input', { target: input.nativeElement }); + input.triggerEventHandler('input', {target: input.nativeElement}); expect(component.props()['value']!.onUpdate).toHaveBeenCalledWith('newuser'); }); @@ -107,8 +104,8 @@ describe('TextFieldComponent', () => { fixture.componentRef.setInput('props', { ...component.props(), - isValid: { value: isValidSig, raw: true, onUpdate: () => {} }, - validationErrors: { value: errorsSig, raw: [], onUpdate: () => {} }, + isValid: {value: isValidSig, raw: true, onUpdate: () => {}}, + validationErrors: {value: errorsSig, raw: [], onUpdate: () => {}}, }); fixture.detectChanges(); @@ -128,7 +125,7 @@ describe('TextFieldComponent', () => { it('should handle multiple error messages', () => { fixture.componentRef.setInput('props', { ...component.props(), - isValid: { value: signal(false), raw: false, onUpdate: () => {} }, + isValid: {value: signal(false), raw: false, onUpdate: () => {}}, validationErrors: { value: signal(['Error 1', 'Error 2']), raw: ['Error 1', 'Error 2'], diff --git a/renderers/angular/src/v0_9/catalog/basic/text-field.component.ts b/renderers/angular/src/v0_9/catalog/basic/text-field.component.ts index a16c828d5..8d77a3187 100644 --- a/renderers/angular/src/v0_9/catalog/basic/text-field.component.ts +++ b/renderers/angular/src/v0_9/catalog/basic/text-field.component.ts @@ -14,11 +14,10 @@ * limitations under the License. */ -import { Component, computed, ChangeDetectionStrategy } from '@angular/core'; -import { BasicCatalogComponent } from './basic-catalog-component'; -import { TextFieldApi } from '@a2ui/web_core/v0_9/basic_catalog'; -import { AnyDuringSchemaAlignment } from '../types'; - +import {Component, computed, ChangeDetectionStrategy} from '@angular/core'; +import {BasicCatalogComponent} from './basic-catalog-component'; +import {TextFieldApi} from '@a2ui/web_core/v0_9/basic_catalog'; +import {AnyDuringSchemaAlignment} from '../types'; /** * Angular implementation of the A2UI TextField component (v0.9). @@ -99,7 +98,9 @@ import { AnyDuringSchemaAlignment } from '../types'; export class TextFieldComponent extends BasicCatalogComponent { readonly label = computed(() => this.props()['label']?.value()); readonly value = computed(() => this.props()['value']?.value() || ''); - readonly placeholder = computed(() => (this.props() as AnyDuringSchemaAlignment)['placeholder']?.value() || ''); + readonly placeholder = computed( + () => (this.props() as AnyDuringSchemaAlignment)['placeholder']?.value() || '', + ); readonly variant = computed(() => this.props()['variant']?.value()); readonly inputType = computed(() => { diff --git a/renderers/angular/src/v0_9/catalog/basic/text.component.spec.ts b/renderers/angular/src/v0_9/catalog/basic/text.component.spec.ts index 392251e2d..72219dedb 100644 --- a/renderers/angular/src/v0_9/catalog/basic/text.component.spec.ts +++ b/renderers/angular/src/v0_9/catalog/basic/text.component.spec.ts @@ -14,12 +14,12 @@ * limitations under the License. */ -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { TextComponent } from './text.component'; -import { By } from '@angular/platform-browser'; -import { signal } from '@angular/core'; -import { MarkdownRenderer } from '../../core/markdown'; -import { A2uiRendererService, A2UI_RENDERER_CONFIG } from '../../core/a2ui-renderer.service'; +import {ComponentFixture, TestBed} from '@angular/core/testing'; +import {TextComponent} from './text.component'; +import {By} from '@angular/platform-browser'; +import {signal} from '@angular/core'; +import {MarkdownRenderer} from '../../core/markdown'; +import {A2uiRendererService, A2UI_RENDERER_CONFIG} from '../../core/a2ui-renderer.service'; describe('TextComponent', () => { let component: TextComponent; @@ -33,9 +33,9 @@ describe('TextComponent', () => { await TestBed.configureTestingModule({ imports: [TextComponent], providers: [ - { provide: MarkdownRenderer, useValue: mockMarkdownRenderer }, + {provide: MarkdownRenderer, useValue: mockMarkdownRenderer}, A2uiRendererService, - { provide: A2UI_RENDERER_CONFIG, useValue: { catalogs: [] } }, + {provide: A2UI_RENDERER_CONFIG, useValue: {catalogs: []}}, ], }).compileComponents(); @@ -46,7 +46,7 @@ describe('TextComponent', () => { it('should create', () => { fixture.componentRef.setInput('props', { - text: { value: signal('Hello World'), raw: 'Hello World', onUpdate: () => {} }, + text: {value: signal('Hello World'), raw: 'Hello World', onUpdate: () => {}}, }); fixture.detectChanges(); expect(component).toBeTruthy(); @@ -54,7 +54,7 @@ describe('TextComponent', () => { it('should render the markdown text', async () => { fixture.componentRef.setInput('props', { - text: { value: signal('Hello World'), raw: 'Hello World', onUpdate: () => {} }, + text: {value: signal('Hello World'), raw: 'Hello World', onUpdate: () => {}}, }); fixture.detectChanges(); await fixture.whenStable(); @@ -67,8 +67,8 @@ describe('TextComponent', () => { it('should handle variant h1', async () => { fixture.componentRef.setInput('props', { - text: { value: signal('Heading'), raw: 'Heading', onUpdate: () => {} }, - variant: { value: signal('h1'), raw: 'h1', onUpdate: () => {} }, + text: {value: signal('Heading'), raw: 'Heading', onUpdate: () => {}}, + variant: {value: signal('h1'), raw: 'h1', onUpdate: () => {}}, }); fixture.detectChanges(); await fixture.whenStable(); @@ -79,8 +79,8 @@ describe('TextComponent', () => { it('should handle variant caption', async () => { fixture.componentRef.setInput('props', { - text: { value: signal('Caption'), raw: 'Caption', onUpdate: () => {} }, - variant: { value: signal('caption'), raw: 'caption', onUpdate: () => {} }, + text: {value: signal('Caption'), raw: 'Caption', onUpdate: () => {}}, + variant: {value: signal('caption'), raw: 'caption', onUpdate: () => {}}, }); fixture.detectChanges(); await fixture.whenStable(); @@ -91,8 +91,8 @@ describe('TextComponent', () => { it('should handle variant h2', async () => { fixture.componentRef.setInput('props', { - text: { value: signal('Heading'), raw: 'Heading', onUpdate: () => {} }, - variant: { value: signal('h2'), raw: 'h2', onUpdate: () => {} }, + text: {value: signal('Heading'), raw: 'Heading', onUpdate: () => {}}, + variant: {value: signal('h2'), raw: 'h2', onUpdate: () => {}}, }); fixture.detectChanges(); await fixture.whenStable(); @@ -103,8 +103,8 @@ describe('TextComponent', () => { it('should handle variant h3', async () => { fixture.componentRef.setInput('props', { - text: { value: signal('Heading'), raw: 'Heading', onUpdate: () => {} }, - variant: { value: signal('h3'), raw: 'h3', onUpdate: () => {} }, + text: {value: signal('Heading'), raw: 'Heading', onUpdate: () => {}}, + variant: {value: signal('h3'), raw: 'h3', onUpdate: () => {}}, }); fixture.detectChanges(); await fixture.whenStable(); @@ -115,8 +115,8 @@ describe('TextComponent', () => { it('should handle variant h4', async () => { fixture.componentRef.setInput('props', { - text: { value: signal('Heading'), raw: 'Heading', onUpdate: () => {} }, - variant: { value: signal('h4'), raw: 'h4', onUpdate: () => {} }, + text: {value: signal('Heading'), raw: 'Heading', onUpdate: () => {}}, + variant: {value: signal('h4'), raw: 'h4', onUpdate: () => {}}, }); fixture.detectChanges(); await fixture.whenStable(); @@ -127,8 +127,8 @@ describe('TextComponent', () => { it('should handle variant h5', async () => { fixture.componentRef.setInput('props', { - text: { value: signal('Heading'), raw: 'Heading', onUpdate: () => {} }, - variant: { value: signal('h5'), raw: 'h5', onUpdate: () => {} }, + text: {value: signal('Heading'), raw: 'Heading', onUpdate: () => {}}, + variant: {value: signal('h5'), raw: 'h5', onUpdate: () => {}}, }); fixture.detectChanges(); await fixture.whenStable(); diff --git a/renderers/angular/src/v0_9/catalog/basic/text.component.ts b/renderers/angular/src/v0_9/catalog/basic/text.component.ts index 80292b638..7c7915602 100644 --- a/renderers/angular/src/v0_9/catalog/basic/text.component.ts +++ b/renderers/angular/src/v0_9/catalog/basic/text.component.ts @@ -14,17 +14,10 @@ * limitations under the License. */ -import { - Component, - computed, - ChangeDetectionStrategy, - inject, - signal, - effect, -} from '@angular/core'; -import { MarkdownRenderer } from '../../core/markdown'; -import { BasicCatalogComponent } from './basic-catalog-component'; -import { TextApi } from '@a2ui/web_core/v0_9/basic_catalog'; +import {Component, computed, ChangeDetectionStrategy, inject, signal, effect} from '@angular/core'; +import {MarkdownRenderer} from '../../core/markdown'; +import {BasicCatalogComponent} from './basic-catalog-component'; +import {TextApi} from '@a2ui/web_core/v0_9/basic_catalog'; /** * Angular implementation of the A2UI Text component (v0.9). @@ -149,7 +142,7 @@ export class TextComponent extends BasicCatalogComponent { } const requestId = ++this.renderRequestId; - this.markdownRenderer.render(value).then((rendered) => { + this.markdownRenderer.render(value).then(rendered => { if (requestId === this.renderRequestId) { this.resolvedText.set(rendered); } diff --git a/renderers/angular/src/v0_9/catalog/basic/video.component.ts b/renderers/angular/src/v0_9/catalog/basic/video.component.ts index 39426c096..c65963b80 100644 --- a/renderers/angular/src/v0_9/catalog/basic/video.component.ts +++ b/renderers/angular/src/v0_9/catalog/basic/video.component.ts @@ -14,11 +14,10 @@ * limitations under the License. */ -import { Component, computed, ChangeDetectionStrategy } from '@angular/core'; -import { BasicCatalogComponent } from './basic-catalog-component'; -import { VideoApi } from '@a2ui/web_core/v0_9/basic_catalog'; -import { AnyDuringSchemaAlignment } from '../types'; - +import {Component, computed, ChangeDetectionStrategy} from '@angular/core'; +import {BasicCatalogComponent} from './basic-catalog-component'; +import {VideoApi} from '@a2ui/web_core/v0_9/basic_catalog'; +import {AnyDuringSchemaAlignment} from '../types'; /** * Angular implementation of the A2UI Video component (v0.9). @@ -62,5 +61,7 @@ import { AnyDuringSchemaAlignment } from '../types'; }) export class VideoComponent extends BasicCatalogComponent { readonly url = computed(() => this.props()['url']?.value()); - readonly posterUrl = computed(() => (this.props() as AnyDuringSchemaAlignment)['posterUrl']?.value()); + readonly posterUrl = computed(() => + (this.props() as AnyDuringSchemaAlignment)['posterUrl']?.value(), + ); } diff --git a/renderers/angular/src/v0_9/catalog/types.ts b/renderers/angular/src/v0_9/catalog/types.ts index f6e60257d..c61da4aff 100644 --- a/renderers/angular/src/v0_9/catalog/types.ts +++ b/renderers/angular/src/v0_9/catalog/types.ts @@ -14,20 +14,19 @@ * limitations under the License. */ -import { Type } from '@angular/core'; -import { Catalog, ComponentApi } from '@a2ui/web_core/v0_9'; -import { CatalogComponentInstance } from '../core/catalog_component'; +import {Type} from '@angular/core'; +import {Catalog, ComponentApi} from '@a2ui/web_core/v0_9'; +import {CatalogComponentInstance} from '../core/catalog_component'; /** * Temporary type used during basic catalog schema alignment to bypass strict type checking. - * + * * To be removed once all properties implemented in Angular basic catalog components conform * to the basic catalog schema. * @see https://github.com/google/A2UI/issues/1303 */ export type AnyDuringSchemaAlignment = any; - /** * Extends the generic {@link ComponentApi} to include Angular-specific component metadata. */ diff --git a/renderers/angular/src/v0_9/core/a2ui-renderer.service.spec.ts b/renderers/angular/src/v0_9/core/a2ui-renderer.service.spec.ts index 2960d126e..e2811bbb5 100644 --- a/renderers/angular/src/v0_9/core/a2ui-renderer.service.spec.ts +++ b/renderers/angular/src/v0_9/core/a2ui-renderer.service.spec.ts @@ -14,9 +14,8 @@ * limitations under the License. */ -import { TestBed } from '@angular/core/testing'; -import { A2uiRendererService, A2UI_RENDERER_CONFIG } from './a2ui-renderer.service'; - +import {TestBed} from '@angular/core/testing'; +import {A2uiRendererService, A2UI_RENDERER_CONFIG} from './a2ui-renderer.service'; describe('A2uiRendererService', () => { let service: A2uiRendererService; @@ -41,7 +40,7 @@ describe('A2uiRendererService', () => { A2uiRendererService, { provide: A2UI_RENDERER_CONFIG, - useValue: { catalogs: [mockCatalog] }, + useValue: {catalogs: [mockCatalog]}, }, ], }); diff --git a/renderers/angular/src/v0_9/core/a2ui-renderer.service.ts b/renderers/angular/src/v0_9/core/a2ui-renderer.service.ts index 2e4e00458..7e2f462c7 100644 --- a/renderers/angular/src/v0_9/core/a2ui-renderer.service.ts +++ b/renderers/angular/src/v0_9/core/a2ui-renderer.service.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { Injectable, OnDestroy, InjectionToken, Inject } from '@angular/core'; +import {Injectable, OnDestroy, InjectionToken, Inject} from '@angular/core'; import { MessageProcessor, SurfaceGroupModel, @@ -22,7 +22,7 @@ import { A2uiMessage, A2uiClientAction as Action, } from '@a2ui/web_core/v0_9'; -import { AngularComponentImplementation, AngularCatalog } from '../catalog/types'; +import {AngularComponentImplementation, AngularCatalog} from '../catalog/types'; /** * Configuration for the A2UI renderer. diff --git a/renderers/angular/src/v0_9/core/catalog_component.ts b/renderers/angular/src/v0_9/core/catalog_component.ts index 601a92ad5..0c725bdcf 100644 --- a/renderers/angular/src/v0_9/core/catalog_component.ts +++ b/renderers/angular/src/v0_9/core/catalog_component.ts @@ -14,9 +14,9 @@ * limitations under the License. */ -import { ComponentApi } from "@a2ui/web_core/v0_9"; -import { Directive, input, Signal } from "@angular/core"; -import { ComponentApiToProps } from './types'; +import {ComponentApi} from '@a2ui/web_core/v0_9'; +import {Directive, input, Signal} from '@angular/core'; +import {ComponentApiToProps} from './types'; /** Describes the properties that a Catalog component needs to implement. For ease of use, please extend CatalogComponent. */ export interface CatalogComponentInstance { @@ -28,13 +28,15 @@ export interface CatalogComponentInstance { /** * Base class for A2UI catalog component in Angular. - * + * * All Angular catalog components should extend this base class, * which provides type safe access to props() and other common * fields. */ @Directive() -export abstract class CatalogComponent implements CatalogComponentInstance { +export abstract class CatalogComponent< + Api extends ComponentApi, +> implements CatalogComponentInstance { /** * Reactive properties resolved from the A2UI ComponentModel. */ @@ -42,4 +44,4 @@ export abstract class CatalogComponent implements Cata readonly surfaceId = input.required(); readonly componentId = input.required(); readonly dataContextPath = input('/'); -} \ No newline at end of file +} diff --git a/renderers/angular/src/v0_9/core/component-binder.service.spec.ts b/renderers/angular/src/v0_9/core/component-binder.service.spec.ts index c71e606e7..d2ce41c5f 100644 --- a/renderers/angular/src/v0_9/core/component-binder.service.spec.ts +++ b/renderers/angular/src/v0_9/core/component-binder.service.spec.ts @@ -14,11 +14,11 @@ * limitations under the License. */ -import { TestBed } from '@angular/core/testing'; -import { DestroyRef } from '@angular/core'; -import { signal as preactSignal } from '@preact/signals-core'; -import { ComponentContext } from '@a2ui/web_core/v0_9'; -import { ComponentBinder } from './component-binder.service'; +import {TestBed} from '@angular/core/testing'; +import {DestroyRef} from '@angular/core'; +import {signal as preactSignal} from '@preact/signals-core'; +import {ComponentContext} from '@a2ui/web_core/v0_9'; +import {ComponentBinder} from './component-binder.service'; describe('ComponentBinder', () => { let binder: ComponentBinder; @@ -31,7 +31,7 @@ describe('ComponentBinder', () => { }); TestBed.configureTestingModule({ - providers: [ComponentBinder, { provide: DestroyRef, useValue: mockDestroyRef }], + providers: [ComponentBinder, {provide: DestroyRef, useValue: mockDestroyRef}], }); binder = TestBed.inject(ComponentBinder); @@ -81,7 +81,7 @@ describe('ComponentBinder', () => { it('should add update() method for data bindings (two-way binding)', () => { const mockComponentModel = { properties: { - value: { path: '/data/text' }, + value: {path: '/data/text'}, }, }; @@ -141,7 +141,7 @@ describe('ComponentBinder', () => { it('should expand ChildList object templates', () => { const mockComponentModel = { properties: { - children: { componentId: 'item-comp', path: '/list/data' }, + children: {componentId: 'item-comp', path: '/list/data'}, }, }; @@ -153,7 +153,7 @@ describe('ComponentBinder', () => { }), nested: jasmine.createSpy('nested').and.callFake((path: string) => ({ path, - nested: (sub: string) => ({ path: `${path}/${sub}` }), + nested: (sub: string) => ({path: `${path}/${sub}`}), })), set: jasmine.createSpy('set'), }; @@ -169,7 +169,7 @@ describe('ComponentBinder', () => { const children = bound['children'].value(); expect(Array.isArray(children)).toBe(true); expect(children.length).toBe(2); - expect(children[0]).toEqual({ id: 'item-comp', basePath: '/list/data/0' }); - expect(children[1]).toEqual({ id: 'item-comp', basePath: '/list/data/1' }); + expect(children[0]).toEqual({id: 'item-comp', basePath: '/list/data/0'}); + expect(children[1]).toEqual({id: 'item-comp', basePath: '/list/data/1'}); }); }); diff --git a/renderers/angular/src/v0_9/core/component-binder.service.ts b/renderers/angular/src/v0_9/core/component-binder.service.ts index 201dd3ddd..5b0ce2f20 100644 --- a/renderers/angular/src/v0_9/core/component-binder.service.ts +++ b/renderers/angular/src/v0_9/core/component-binder.service.ts @@ -14,10 +14,10 @@ * limitations under the License. */ -import { DestroyRef, Injectable, inject, NgZone } from '@angular/core'; -import { ComponentContext, computed } from '@a2ui/web_core/v0_9'; -import { toAngularSignal } from './utils'; -import { BoundProperty } from './types'; +import {DestroyRef, Injectable, inject, NgZone} from '@angular/core'; +import {ComponentContext, computed} from '@a2ui/web_core/v0_9'; +import {toAngularSignal} from './utils'; +import {BoundProperty} from './types'; /** Represents a reference to a child component. */ export interface Child { @@ -54,11 +54,13 @@ export class ComponentBinder { const value = props[key]; let preactSig; - const isChildListTemplate = value && typeof value === 'object' && 'componentId' in value && 'path' in value; - const isBoundPath = value && typeof value === 'object' && 'path' in value && !('componentId' in value); + const isChildListTemplate = + value && typeof value === 'object' && 'componentId' in value && 'path' in value; + const isBoundPath = + value && typeof value === 'object' && 'path' in value && !('componentId' in value); if (isChildListTemplate) { - const listSig = context.dataContext.resolveSignal({ path: value.path }); + const listSig = context.dataContext.resolveSignal({path: value.path}); const listContext = context.dataContext.nested(value.path); preactSig = computed(() => { const arr = listSig.value; @@ -80,7 +82,7 @@ export class ComponentBinder { if (typeof val === 'object' && val !== null && 'id' in val) { return val; } - return { id: val, basePath: context.dataContext.path }; + return {id: val, basePath: context.dataContext.path}; }); } else if (key === 'children') { const originalSig = preactSig; @@ -91,7 +93,7 @@ export class ComponentBinder { if (typeof item === 'object' && item !== null && 'id' in item) { return item; } - return { id: item, basePath: context.dataContext.path }; + return {id: item, basePath: context.dataContext.path}; }); }); } @@ -113,7 +115,7 @@ export class ComponentBinder { const condition = rule.condition || rule; const message = rule.message || 'Validation failed'; const conditionSig = context.dataContext.resolveSignal(condition); - return { conditionSig, message }; + return {conditionSig, message}; }); const isValidPreactSig = computed(() => { @@ -121,9 +123,7 @@ export class ComponentBinder { }); const validationErrorsPreactSig = computed(() => { - return ruleResults - .filter((r: any) => !r.conditionSig.value) - .map((r: any) => r.message); + return ruleResults.filter((r: any) => !r.conditionSig.value).map((r: any) => r.message); }); bound['isValid'] = { diff --git a/renderers/angular/src/v0_9/core/component-host.component.spec.ts b/renderers/angular/src/v0_9/core/component-host.component.spec.ts index 6bb2d304c..c9c928fdd 100644 --- a/renderers/angular/src/v0_9/core/component-host.component.spec.ts +++ b/renderers/angular/src/v0_9/core/component-host.component.spec.ts @@ -14,12 +14,12 @@ * limitations under the License. */ -import { TestBed, ComponentFixture } from '@angular/core/testing'; -import { By } from '@angular/platform-browser'; -import { ComponentHostComponent } from './component-host.component'; -import { A2uiRendererService } from './a2ui-renderer.service'; -import { ComponentModel, SurfaceComponentsModel, SurfaceModel } from '@a2ui/web_core/v0_9'; -import { Component, Input } from '@angular/core'; +import {TestBed, ComponentFixture} from '@angular/core/testing'; +import {By} from '@angular/platform-browser'; +import {ComponentHostComponent} from './component-host.component'; +import {A2uiRendererService} from './a2ui-renderer.service'; +import {ComponentModel, SurfaceComponentsModel, SurfaceModel} from '@a2ui/web_core/v0_9'; +import {Component, Input} from '@angular/core'; @Component({ selector: 'test-child', @@ -43,11 +43,13 @@ describe('ComponentHostComponent', () => { beforeEach(async () => { mockCatalog = { id: 'test-catalog', - components: new Map([['TestType', { component: TestChildComponent }]]), + components: new Map([['TestType', {component: TestChildComponent}]]), }; const mockSurfaceComponentsModel = new SurfaceComponentsModel(); - mockSurfaceComponentsModel.addComponent(new ComponentModel('comp1', 'TestType', { text: 'Hello' })); + mockSurfaceComponentsModel.addComponent( + new ComponentModel('comp1', 'TestType', {text: 'Hello'}), + ); mockSurface = { componentsModel: mockSurfaceComponentsModel, @@ -64,14 +66,12 @@ describe('ComponentHostComponent', () => { await TestBed.configureTestingModule({ imports: [ComponentHostComponent], - providers: [ - { provide: A2uiRendererService, useValue: mockRendererService }, - ], + providers: [{provide: A2uiRendererService, useValue: mockRendererService}], }).compileComponents(); fixture = TestBed.createComponent(ComponentHostComponent); component = fixture.componentInstance; - fixture.componentRef.setInput('componentKey', { id: 'comp1', basePath: '/' }); + fixture.componentRef.setInput('componentKey', {id: 'comp1', basePath: '/'}); fixture.componentRef.setInput('surfaceId', 'surf1'); }); @@ -97,7 +97,7 @@ describe('ComponentHostComponent', () => { }); it('should use provided dataContextPath for ComponentContext', () => { - fixture.componentRef.setInput('componentKey', { id: 'comp1', basePath: '/nested/path' }); + fixture.componentRef.setInput('componentKey', {id: 'comp1', basePath: '/nested/path'}); fixture.detectChanges(); const childDebugElement = fixture.debugElement.query(By.directive(TestChildComponent)); @@ -109,15 +109,15 @@ describe('ComponentHostComponent', () => { it('should update props when component model is updated', () => { fixture.detectChanges(); // Trigger change detection - + const childDebugElement = fixture.debugElement.query(By.directive(TestChildComponent)); const childInstance = childDebugElement.componentInstance as TestChildComponent; - + expect(childInstance.props.text.value()).toBe('Hello'); const compModel = mockSurface.componentsModel.get('comp1')!; // This properties assignment triggers the update. - compModel.properties = { text: 'Hello', newProp: 'new value' }; + compModel.properties = {text: 'Hello', newProp: 'new value'}; fixture.detectChanges(); // Propagate changes @@ -143,7 +143,9 @@ describe('ComponentHostComponent', () => { const childDebugElement = fixture.debugElement.query(By.directive(TestChildComponent)); expect(childDebugElement).toBeFalsy(); - expect(consoleWarnSpy).toHaveBeenCalledWith('Component comp1 not found in surface surf1. Waiting for it...'); + expect(consoleWarnSpy).toHaveBeenCalledWith( + 'Component comp1 not found in surface surf1. Waiting for it...', + ); }); it('should error and return if component type not in catalog', () => { @@ -178,7 +180,7 @@ describe('ComponentHostComponent', () => { expect(compiled.innerHTML).toContain('Child Component'); }); it('should pass dataContextPath to the rendered component', () => { - fixture.componentRef.setInput('componentKey', { id: 'comp1', basePath: '/some/path' }); + fixture.componentRef.setInput('componentKey', {id: 'comp1', basePath: '/some/path'}); fixture.detectChanges(); const childDebugElement = fixture.debugElement.query(By.directive(TestChildComponent)); diff --git a/renderers/angular/src/v0_9/core/component-host.component.ts b/renderers/angular/src/v0_9/core/component-host.component.ts index 2a6c5470d..793de9dc0 100644 --- a/renderers/angular/src/v0_9/core/component-host.component.ts +++ b/renderers/angular/src/v0_9/core/component-host.component.ts @@ -45,7 +45,7 @@ import {BoundProperty} from './types'; selector: 'a2ui-v09-component-host', imports: [NgComponentOutlet], host: { - 'style': 'display: contents;' + style: 'display: contents;', }, template: ` @if (componentType) { @@ -127,7 +127,7 @@ export class ComponentHostComponent { if (!componentModel) { console.warn(`Component ${id} not found in surface ${surfaceId}. Waiting for it...`); - const sub = surface.componentsModel.onCreated.subscribe((comp) => { + const sub = surface.componentsModel.onCreated.subscribe(comp => { if (comp.id === id) { this.initializeComponent(surface, comp, id, basePath); sub.unsubscribe(); diff --git a/renderers/angular/src/v0_9/core/function_binding.spec.ts b/renderers/angular/src/v0_9/core/function_binding.spec.ts index b3b570c74..ec0427623 100644 --- a/renderers/angular/src/v0_9/core/function_binding.spec.ts +++ b/renderers/angular/src/v0_9/core/function_binding.spec.ts @@ -14,10 +14,10 @@ * limitations under the License. */ -import { DataContext, SurfaceModel } from '@a2ui/web_core/v0_9'; -import { DestroyRef } from '@angular/core'; -import { BasicCatalogBase } from '../catalog/basic/basic-catalog'; -import { toAngularSignal } from './utils'; +import {DataContext, SurfaceModel} from '@a2ui/web_core/v0_9'; +import {DestroyRef} from '@angular/core'; +import {BasicCatalogBase} from '../catalog/basic/basic-catalog'; +import {toAngularSignal} from './utils'; describe('Function Bindings', () => { let mockDestroyRef: jasmine.SpyObj; diff --git a/renderers/angular/src/v0_9/core/markdown.ts b/renderers/angular/src/v0_9/core/markdown.ts index 724b1057b..76f8d9065 100644 --- a/renderers/angular/src/v0_9/core/markdown.ts +++ b/renderers/angular/src/v0_9/core/markdown.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { Injectable } from '@angular/core'; +import {Injectable} from '@angular/core'; export interface MarkdownRendererOptions { tagClassMap?: Record; @@ -30,17 +30,16 @@ export abstract class MarkdownRenderer { export class DefaultMarkdownRenderer extends MarkdownRenderer { private static warningLogged = false; - override async render( - markdown: string, - options?: MarkdownRendererOptions, - ): Promise { + override async render(markdown: string, options?: MarkdownRendererOptions): Promise { try { // @ts-ignore - optional peer dependency - const { renderMarkdown } = await import('@a2ui/markdown-it'); + const {renderMarkdown} = await import('@a2ui/markdown-it'); return await renderMarkdown(markdown, options as any); } catch (e) { if (!DefaultMarkdownRenderer.warningLogged) { - console.warn("[DefaultMarkdownRenderer] Failed to load optional `@a2ui/markdown-it` renderer. Using fallback."); + console.warn( + '[DefaultMarkdownRenderer] Failed to load optional `@a2ui/markdown-it` renderer. Using fallback.', + ); DefaultMarkdownRenderer.warningLogged = true; } return markdown; @@ -48,7 +47,9 @@ export class DefaultMarkdownRenderer extends MarkdownRenderer { } } -export function provideMarkdownRenderer(renderFn?: (markdown: string, options?: MarkdownRendererOptions) => Promise) { +export function provideMarkdownRenderer( + renderFn?: (markdown: string, options?: MarkdownRendererOptions) => Promise, +) { if (renderFn) { return { provide: MarkdownRenderer, @@ -57,5 +58,5 @@ export function provideMarkdownRenderer(renderFn?: (markdown: string, options?: }, }; } - return { provide: MarkdownRenderer, useClass: DefaultMarkdownRenderer }; + return {provide: MarkdownRenderer, useClass: DefaultMarkdownRenderer}; } diff --git a/renderers/angular/src/v0_9/core/surface.component.spec.ts b/renderers/angular/src/v0_9/core/surface.component.spec.ts index bdf4aac20..911b6d226 100644 --- a/renderers/angular/src/v0_9/core/surface.component.spec.ts +++ b/renderers/angular/src/v0_9/core/surface.component.spec.ts @@ -14,14 +14,14 @@ * limitations under the License. */ -import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { SurfaceComponent } from './surface.component'; -import { ComponentHostComponent } from './component-host.component'; -import { By } from '@angular/platform-browser'; -import { A2uiRendererService } from './a2ui-renderer.service'; -import { ComponentBinder } from './component-binder.service'; -import { ComponentModel } from '@a2ui/web_core/v0_9'; +import {Component, Input, ChangeDetectionStrategy} from '@angular/core'; +import {ComponentFixture, TestBed} from '@angular/core/testing'; +import {SurfaceComponent} from './surface.component'; +import {ComponentHostComponent} from './component-host.component'; +import {By} from '@angular/platform-browser'; +import {A2uiRendererService} from './a2ui-renderer.service'; +import {ComponentBinder} from './component-binder.service'; +import {ComponentModel} from '@a2ui/web_core/v0_9'; @Component({ selector: 'test-text', @@ -47,11 +47,11 @@ describe('SurfaceComponent', () => { surfaceGroup: { getSurface: jasmine.createSpy('getSurface').and.returnValue({ componentsModel: new Map([ - ['root', new ComponentModel('root', 'Text', { text: { value: 'Hello' } })], + ['root', new ComponentModel('root', 'Text', {text: {value: 'Hello'}})], ]), catalog: { id: 'mock-catalog', - components: new Map([['Text', { type: 'Text', component: TestTextComponent }]]), + components: new Map([['Text', {type: 'Text', component: TestTextComponent}]]), }, }), }, @@ -61,8 +61,8 @@ describe('SurfaceComponent', () => { await TestBed.configureTestingModule({ imports: [SurfaceComponent], providers: [ - { provide: A2uiRendererService, useValue: mockRendererService }, - { provide: ComponentBinder, useValue: mockBinder }, + {provide: A2uiRendererService, useValue: mockRendererService}, + {provide: ComponentBinder, useValue: mockBinder}, ], }).compileComponents(); @@ -84,7 +84,7 @@ describe('SurfaceComponent', () => { const host = fixture.debugElement.query(By.directive(ComponentHostComponent)); expect(host).toBeTruthy(); expect(host.componentInstance.surfaceId()).toBe('test-surface'); - expect(host.componentInstance.componentKey()).toEqual({ id: 'root', basePath: '/custom/path' }); + expect(host.componentInstance.componentKey()).toEqual({id: 'root', basePath: '/custom/path'}); }); it('should use default dataContextPath of "/"', () => { @@ -92,6 +92,6 @@ describe('SurfaceComponent', () => { fixture.detectChanges(); const host = fixture.debugElement.query(By.directive(ComponentHostComponent)); - expect(host.componentInstance.componentKey()).toEqual({ id: 'root', basePath: '/' }); + expect(host.componentInstance.componentKey()).toEqual({id: 'root', basePath: '/'}); }); }); diff --git a/renderers/angular/src/v0_9/core/surface.component.ts b/renderers/angular/src/v0_9/core/surface.component.ts index 078dd371e..79c77c4a2 100644 --- a/renderers/angular/src/v0_9/core/surface.component.ts +++ b/renderers/angular/src/v0_9/core/surface.component.ts @@ -14,8 +14,8 @@ * limitations under the License. */ -import { ChangeDetectionStrategy, Component, input } from '@angular/core'; -import { ComponentHostComponent } from './component-host.component'; +import {ChangeDetectionStrategy, Component, input} from '@angular/core'; +import {ComponentHostComponent} from './component-host.component'; /** * High-level component for rendering an entire A2UI surface. @@ -29,11 +29,11 @@ import { ComponentHostComponent } from './component-host.component'; standalone: true, imports: [ComponentHostComponent], host: { - 'style': 'display: contents;' + style: 'display: contents;', }, template: ` diff --git a/renderers/angular/src/v0_9/core/types.ts b/renderers/angular/src/v0_9/core/types.ts index 42238a847..c34a8df19 100644 --- a/renderers/angular/src/v0_9/core/types.ts +++ b/renderers/angular/src/v0_9/core/types.ts @@ -14,10 +14,10 @@ * limitations under the License. */ -import { Signal } from '@angular/core'; -import { z } from 'zod'; -import { ComponentApi, DataBindingSchema, FunctionCallSchema } from '@a2ui/web_core/v0_9'; -import { Child } from './component-binder.service'; +import {Signal} from '@angular/core'; +import {z} from 'zod'; +import {ComponentApi, DataBindingSchema, FunctionCallSchema} from '@a2ui/web_core/v0_9'; +import {Child} from './component-binder.service'; /** * Represents a component property bound to an Angular Signal and update logic. @@ -60,9 +60,9 @@ type DynamicSchemaValueToRaw = Exclude = { [K in keyof InferredSchema]: K extends 'children' | 'child' | 'trigger' | 'content' - ? BoundProperty - : BoundProperty> -} + ? BoundProperty + : BoundProperty>; +}; interface CheckProps { isValid: boolean; @@ -70,24 +70,26 @@ interface CheckProps { } /** The binder can add some properties to the Props object. This util adds them to the type. */ -export type ExtendedProps = - 'checks' extends keyof ComponentProps ? Omit & CheckProps : ComponentProps; +export type ExtendedProps = + 'checks' extends keyof ComponentProps + ? Omit & CheckProps + : ComponentProps; /** -* Utility to convert a component Api Type to the props Type, where the -* values are wrapped in BoundProperty. This is used to correctly type the props() -* in a UI component -* -* @example -* export const TextComponentApi = { -* name: 'Text', -* schema: z.object({ -* text: z.string(), -* }) -* .strict(), -* } satisfies ComponentApi; -* export type TextComponentProps = ComponentApiToProps; // outputs { text: BoundProperty; } -*/ -export type ComponentApiToProps = InferredInterfaceToProps>>; + * Utility to convert a component Api Type to the props Type, where the + * values are wrapped in BoundProperty. This is used to correctly type the props() + * in a UI component + * + * @example + * export const TextComponentApi = { + * name: 'Text', + * schema: z.object({ + * text: z.string(), + * }) + * .strict(), + * } satisfies ComponentApi; + * export type TextComponentProps = ComponentApiToProps; // outputs { text: BoundProperty; } + */ +export type ComponentApiToProps = InferredInterfaceToProps< + ExtendedProps> +>; diff --git a/renderers/angular/src/v0_9/core/utils.spec.ts b/renderers/angular/src/v0_9/core/utils.spec.ts index 7327e5741..e438dbcdc 100644 --- a/renderers/angular/src/v0_9/core/utils.spec.ts +++ b/renderers/angular/src/v0_9/core/utils.spec.ts @@ -14,9 +14,9 @@ * limitations under the License. */ -import { signal as preactSignal } from '@preact/signals-core'; -import { DestroyRef } from '@angular/core'; -import { toAngularSignal, getNormalizedPath } from './utils'; +import {signal as preactSignal} from '@preact/signals-core'; +import {DestroyRef} from '@angular/core'; +import {toAngularSignal, getNormalizedPath} from './utils'; describe('toAngularSignal', () => { let mockDestroyRef: jasmine.SpyObj; diff --git a/renderers/angular/src/v0_9/core/utils.ts b/renderers/angular/src/v0_9/core/utils.ts index a9f97d93e..d8bb6afc9 100644 --- a/renderers/angular/src/v0_9/core/utils.ts +++ b/renderers/angular/src/v0_9/core/utils.ts @@ -14,9 +14,9 @@ * limitations under the License. */ -import { DestroyRef, Signal, signal as angularSignal } from '@angular/core'; -import { Signal as PreactSignal, effect, signal as preactSignal } from '@a2ui/web_core/v0_9'; -export { preactSignal }; +import {DestroyRef, Signal, signal as angularSignal} from '@angular/core'; +import {Signal as PreactSignal, effect, signal as preactSignal} from '@a2ui/web_core/v0_9'; +export {preactSignal}; /** * Bridges a Preact Signal (from A2UI web_core) to a reactive Angular Signal. @@ -32,7 +32,7 @@ export { preactSignal }; * (necessary for correct change detection in OnPush components). * @returns A read-only Angular Signal. */ -import { NgZone } from '@angular/core'; +import {NgZone} from '@angular/core'; export function toAngularSignal( preactSignal: PreactSignal, diff --git a/renderers/angular/src/v0_9/test_data/mocks/contact-card.json b/renderers/angular/src/v0_9/test_data/mocks/contact-card.json index 4cbf17de3..af010d118 100644 --- a/renderers/angular/src/v0_9/test_data/mocks/contact-card.json +++ b/renderers/angular/src/v0_9/test_data/mocks/contact-card.json @@ -19,32 +19,25 @@ { "id": "main-column", "component": "Column", - "children": [ - "avatar-image", - "name", - "title", - "divider", - "contact-info", - "actions" - ], + "children": ["avatar-image", "name", "title", "divider", "contact-info", "actions"], "align": "center" }, { "id": "avatar-image", "component": "Image", - "url": { "path": "/avatar" }, + "url": {"path": "/avatar"}, "fit": "cover" }, { "id": "name", "component": "Text", - "text": { "path": "/name" }, + "text": {"path": "/name"}, "weight": 700 }, { "id": "title", "component": "Text", - "text": { "path": "/title" } + "text": {"path": "/title"} }, { "id": "divider", @@ -69,7 +62,7 @@ { "id": "phone-text", "component": "Text", - "text": { "path": "/phone" } + "text": {"path": "/phone"} }, { "id": "email-row", @@ -85,7 +78,7 @@ { "id": "email-text", "component": "Text", - "text": { "path": "/email" } + "text": {"path": "/email"} }, { "id": "location-row", @@ -101,7 +94,7 @@ { "id": "location-text", "component": "Text", - "text": { "path": "/location" } + "text": {"path": "/location"} }, { "id": "actions", @@ -115,7 +108,7 @@ "action": { "functionCall": { "call": "call", - "args": { "number": { "path": "/phone" } } + "args": {"number": {"path": "/phone"}} } } }, @@ -131,7 +124,7 @@ "action": { "functionCall": { "call": "message", - "args": { "recipient": { "path": "/email" } } + "args": {"recipient": {"path": "/email"}} } } }, diff --git a/renderers/angular/src/v0_9/test_data/mocks/restaurant-card.json b/renderers/angular/src/v0_9/test_data/mocks/restaurant-card.json index faa4f405f..6e9197db5 100644 --- a/renderers/angular/src/v0_9/test_data/mocks/restaurant-card.json +++ b/renderers/angular/src/v0_9/test_data/mocks/restaurant-card.json @@ -24,7 +24,7 @@ { "id": "restaurant-image", "component": "Image", - "url": { "path": "/image" }, + "url": {"path": "/image"}, "fit": "cover" }, { @@ -42,18 +42,18 @@ { "id": "restaurant-name", "component": "Text", - "text": { "path": "/name" }, + "text": {"path": "/name"}, "weight": 700 }, { "id": "price-range", "component": "Text", - "text": { "path": "/priceRange" } + "text": {"path": "/priceRange"} }, { "id": "cuisine", "component": "Text", - "text": { "path": "/cuisine" }, + "text": {"path": "/cuisine"}, "style": "italic" }, { @@ -70,12 +70,12 @@ { "id": "rating", "component": "Text", - "text": { "path": "/rating" } + "text": {"path": "/rating"} }, { "id": "reviews", "component": "Text", - "text": { "path": "/reviewCount" } + "text": {"path": "/reviewCount"} }, { "id": "details-row", @@ -85,12 +85,12 @@ { "id": "distance", "component": "Text", - "text": { "path": "/distance" } + "text": {"path": "/distance"} }, { "id": "delivery-time", "component": "Text", - "text": { "path": "/deliveryTime" } + "text": {"path": "/deliveryTime"} } ] } diff --git a/renderers/angular/src/v0_9/v0_9_integration.spec.ts b/renderers/angular/src/v0_9/v0_9_integration.spec.ts index 231141be7..4c408fbf7 100644 --- a/renderers/angular/src/v0_9/v0_9_integration.spec.ts +++ b/renderers/angular/src/v0_9/v0_9_integration.spec.ts @@ -14,13 +14,13 @@ * limitations under the License. */ -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { Component, ChangeDetectionStrategy } from '@angular/core'; -import { A2uiRendererService, A2UI_RENDERER_CONFIG } from './core/a2ui-renderer.service'; -import { SurfaceComponent } from './core/surface.component'; -import { BasicCatalog } from './catalog/basic/basic-catalog'; -import { A2uiMessage } from '@a2ui/web_core/v0_9'; -import { MarkdownRenderer } from './core/markdown'; +import {ComponentFixture, TestBed} from '@angular/core/testing'; +import {Component, ChangeDetectionStrategy} from '@angular/core'; +import {A2uiRendererService, A2UI_RENDERER_CONFIG} from './core/a2ui-renderer.service'; +import {SurfaceComponent} from './core/surface.component'; +import {BasicCatalog} from './catalog/basic/basic-catalog'; +import {A2uiMessage} from '@a2ui/web_core/v0_9'; +import {MarkdownRenderer} from './core/markdown'; import * as restaurantCardMock from './test_data/mocks/restaurant-card.json'; import * as contactCardMock from './test_data/mocks/contact-card.json'; @@ -147,7 +147,7 @@ describe('v0.9 Angular Renderer Integration', () => { { id: 'root', component: 'Text', - text: { path: '/user/name' }, + text: {path: '/user/name'}, }, ], }, @@ -202,7 +202,7 @@ describe('v0.9 Angular Renderer Integration', () => { action: { event: { name: 'navigate', - context: { url: 'https://example.com' }, + context: {url: 'https://example.com'}, }, }, }, @@ -227,7 +227,7 @@ describe('v0.9 Angular Renderer Integration', () => { const actionArg = actionSpy.calls.mostRecent().args[0]; expect(actionArg.surfaceId).toBe('test-surface'); expect(actionArg.name).toBe('navigate'); - expect(actionArg.context).toEqual({ url: 'https://example.com' }); + expect(actionArg.context).toEqual({url: 'https://example.com'}); expect(actionArg.sourceComponentId).toBe('root'); expect(actionArg.timestamp).toBeDefined(); }); diff --git a/renderers/docs/web_publishing.md b/renderers/docs/web_publishing.md index 480df5560..9c5cd8a82 100644 --- a/renderers/docs/web_publishing.md +++ b/renderers/docs/web_publishing.md @@ -32,6 +32,7 @@ renderers/scripts/increment_version.mjs lit 0.9.2-beta.1 ``` This script will: + - Update the `package.json` of the target package. - Scan the entire mono-repo for internal dependents (via `file:` links). - Run `npm install` in those dependents to update their lockfiles. @@ -46,6 +47,7 @@ Once versions are updated and merged into `main`, use the `publish_npm` script t ``` This script will: + - Run `npx google-artifactregistry-auth` to authenticate. - Sort packages topologically (e.g., publishing `web_core` before `lit`). - Verify that if a renderer is being published, `web_core` is also included (use `--force` to skip). @@ -53,9 +55,10 @@ This script will: - For each package: `npm install` -> `npm test` -> `npm run publish:package`. **Advanced Flags for publish_npm.mjs:** + - `--force`: Skips the `web_core` inclusion warning. - `--yes`: Bypasses the manual user confirmation prompt (useful for CI). -- `--dry-run`: Simulates the process, printing the commands it *would* execute without actually running them. +- `--dry-run`: Simulates the process, printing the commands it _would_ execute without actually running them. - `--skip-tests`: Skips the `npm run test` phase before publishing. - `--test-only`: Runs the full build and test suite in topological order, but skips the final `npm run publish:package` step. Useful for verifying that packages build and tests pass before performing a real release. @@ -74,6 +77,7 @@ This generates a `manifest.json` with the current versions of all renderer packa You can also do this step manually, if you are authenticated with `gcloud` with a corporate Google account in the correct groups: 1. Create a new manifest.json file with these contents: + ```json { "publish_all": true @@ -106,7 +110,7 @@ Because these are scoped packages (`@a2ui/`), they require the `--access public` npm run publish:package ``` -*Note: This command runs the build, prepares the `dist/` directory, and then executes `npm publish dist/ --access public`.* +_Note: This command runs the build, prepares the `dist/` directory, and then executes `npm publish dist/ --access public`._ --- @@ -114,6 +118,7 @@ npm run publish:package **What happens during `npm run publish:package`?** Before publishing, the script runs the necessary `build` command which processes the code. Then, a preparation script (usually `prepare-publish.mjs`) runs, which: + 1. Copies `package.json`, `README.md`, and `LICENSE` to the `dist/` folder. 2. It scans all dependencies and peerDependencies for internal `@a2ui/` packages (those using `file:` links) and updates them to the actual current versions in the mono-repo (e.g., `^0.9.0`). 3. Adjusts exports and paths (removing the `./dist/` prefix) so they are correct when consumed from the package root. @@ -126,4 +131,3 @@ Only the `dist/` directory, `src/` directory (for sourcemaps), `package.json`, ` **What about the License?** The package is automatically published under the `Apache-2.0` open-source license, as defined in `package.json`. - diff --git a/renderers/flutter/README.md b/renderers/flutter/README.md index d39a11941..f918ced8e 100644 --- a/renderers/flutter/README.md +++ b/renderers/flutter/README.md @@ -1,8 +1,8 @@ # Flutter Gen UI SDK -The [Flutter Gen UI SDK](https://github.com/flutter/genui) is the official Flutter renderer for A2UI. +The [Flutter Gen UI SDK](https://github.com/flutter/genui) is the official Flutter renderer for A2UI. Key packages from the Flutter Gen UI SDK include: -* [`genui`](https://pub.dev/packages/genui): The core framework for employing Generative UI in Flutter applications. -* [`genui_a2ui`](https://pub.dev/packages/genui_a2ui): This package specifically allows the Flutter Gen UI SDK to act as a renderer for UIs generated by an A2UI backend agent, integrating with the A2UI protocol. \ No newline at end of file +- [`genui`](https://pub.dev/packages/genui): The core framework for employing Generative UI in Flutter applications. +- [`genui_a2ui`](https://pub.dev/packages/genui_a2ui): This package specifically allows the Flutter Gen UI SDK to act as a renderer for UIs generated by an A2UI backend agent, integrating with the A2UI protocol. diff --git a/renderers/lit/README.md b/renderers/lit/README.md index 8bc471687..de3268174 100644 --- a/renderers/lit/README.md +++ b/renderers/lit/README.md @@ -23,7 +23,7 @@ use the **v0.9** protocol. To use the v0.9 implementation, import from the versioned path: ```typescript -import { A2uiSurface, basicCatalog } from "@a2ui/lit/v0_9"; +import {A2uiSurface, basicCatalog} from '@a2ui/lit/v0_9'; ``` ## Quick Start @@ -31,12 +31,12 @@ import { A2uiSurface, basicCatalog } from "@a2ui/lit/v0_9"; The Lit renderer works alongside the `MessageProcessor` from `@a2ui/web_core`. ```typescript -import { LitElement, html } from "lit"; -import { customElement, state } from "lit/decorators.js"; -import { MessageProcessor } from "@a2ui/web_core/v0_9"; -import { A2uiSurface, basicCatalog } from "@a2ui/lit/v0_9"; +import {LitElement, html} from 'lit'; +import {customElement, state} from 'lit/decorators.js'; +import {MessageProcessor} from '@a2ui/web_core/v0_9'; +import {A2uiSurface, basicCatalog} from '@a2ui/lit/v0_9'; -@customElement("my-app") +@customElement('my-app') export class MyApp extends LitElement { private processor = new MessageProcessor([basicCatalog]); @@ -47,8 +47,8 @@ export class MyApp extends LitElement { super.connectedCallback(); // Listen for surface creation - this.processor.onSurfaceCreated((s) => { - if (s.id === "main-chat") { + this.processor.onSurfaceCreated(s => { + if (s.id === 'main-chat') { this.surface = s; } }); @@ -56,9 +56,9 @@ export class MyApp extends LitElement { // The message objects should come from your agent this.processor.processMessages([ { - version: "v0.9", + version: 'v0.9', createSurface: { - surfaceId: "main-chat", + surfaceId: 'main-chat', catalogId: basicCatalog.id, }, }, @@ -86,11 +86,11 @@ A2UI v0.9 separates a component's API (Schema) from its implementation. Use Zod to define the properties. ```typescript -import { z } from "zod"; -import { CommonSchemas } from "@a2ui/web_core/v0_9"; +import {z} from 'zod'; +import {CommonSchemas} from '@a2ui/web_core/v0_9'; export const MyProfileApi = { - name: "Profile", + name: 'Profile', schema: z.object({ username: CommonSchemas.DynamicString, bio: CommonSchemas.DynamicString, @@ -104,12 +104,12 @@ export const MyProfileApi = { Extend `A2uiLitElement` and implement `createController` and `render`. ```typescript -import { A2uiLitElement, A2uiController } from "@a2ui/lit/v0_9"; -import { customElement } from "lit/decorators.js"; -import { html, nothing } from "lit"; -import { MyProfileApi } from "./my-profile-api"; +import {A2uiLitElement, A2uiController} from '@a2ui/lit/v0_9'; +import {customElement} from 'lit/decorators.js'; +import {html, nothing} from 'lit'; +import {MyProfileApi} from './my-profile-api'; -@customElement("my-profile") +@customElement('my-profile') export class MyProfileElement extends A2uiLitElement { protected createController() { return new A2uiController(this, MyProfileApi); @@ -132,7 +132,7 @@ export class MyProfileElement extends A2uiLitElement { // Export the catalog entry export const MyProfile = { ...MyProfileApi, - tagName: "my-profile", + tagName: 'my-profile', }; ``` @@ -141,13 +141,10 @@ export const MyProfile = { Group your custom components into a `Catalog` from `@a2ui/web_core/v0_9`. ```typescript -import { Catalog } from "@a2ui/web_core/v0_9"; -import { MyProfile } from "./my-profile"; +import {Catalog} from '@a2ui/web_core/v0_9'; +import {MyProfile} from './my-profile'; -export const myCatalog = new Catalog( - "https://example.com/catalogs/my-catalog.json", - [MyProfile], -); +export const myCatalog = new Catalog('https://example.com/catalogs/my-catalog.json', [MyProfile]); ``` ### 4. Use the Custom Catalog @@ -156,9 +153,9 @@ Pass your custom catalog to the `MessageProcessor` alongside the basic catalog in your app setup (as seen in the Quick Start). ```typescript -import { MessageProcessor } from "@a2ui/web_core/v0_9"; -import { basicCatalog } from "@a2ui/lit/v0_9"; -import { myCatalog } from "./my-catalog"; +import {MessageProcessor} from '@a2ui/web_core/v0_9'; +import {basicCatalog} from '@a2ui/lit/v0_9'; +import {myCatalog} from './my-catalog'; // Initialize the MessageProcessor with both catalogs const processor = new MessageProcessor([basicCatalog, myCatalog]); diff --git a/renderers/lit/a2ui_explorer/README.md b/renderers/lit/a2ui_explorer/README.md index cbdaf4ffc..d0b3d0e4e 100644 --- a/renderers/lit/a2ui_explorer/README.md +++ b/renderers/lit/a2ui_explorer/README.md @@ -23,17 +23,20 @@ npm run build ``` For more details on building the renderers, see: + - [Web Core README](../../../../renderers/web_core/README.md) - [Lit Renderer README](../../../../renderers/lit/README.md) ## Getting Started 1. **Navigate to this directory**: + ```bash cd samples/client/lit/local_gallery ``` 2. **Install dependencies**: + ```bash npm install ``` @@ -50,6 +53,6 @@ For more details on building the renderers, see: ## Architecture - **Agentless**: Unlike other samples, this does not require a running Python agent. It simulates agent responses locally for interactive components (like the Login Form). -- **Dynamic Loading**: The app automatically discovers and loads *all* `.json` files present in the v0.8 minimal specification folder at build time. To add a new test case, simply drop a JSON file into that specification folder and restart the dev server. +- **Dynamic Loading**: The app automatically discovers and loads _all_ `.json` files present in the v0.8 minimal specification folder at build time. To add a new test case, simply drop a JSON file into that specification folder and restart the dev server. - **Surface Isolation**: Each example is rendered into its own independent `a2ui-surface` with a unique ID derived from the filename. - **Mock Agent Console**: All user interactions (button clicks, form submissions) are intercepted and logged to a sidebar, demonstrating how the renderer resolves actions and contexts. diff --git a/renderers/lit/a2ui_explorer/index.html b/renderers/lit/a2ui_explorer/index.html index c2ddac1b1..5c752f2ed 100644 --- a/renderers/lit/a2ui_explorer/index.html +++ b/renderers/lit/a2ui_explorer/index.html @@ -1,4 +1,4 @@ - + - - - - A2UI Local Gallery (Minimal v0.9) - - - - - - - - - - + + + + A2UI Local Gallery (Minimal v0.9) + + + + + + + + + + diff --git a/renderers/lit/a2ui_explorer/src/examples.ts b/renderers/lit/a2ui_explorer/src/examples.ts index 0f5563fc4..d8d4a5ea9 100644 --- a/renderers/lit/a2ui_explorer/src/examples.ts +++ b/renderers/lit/a2ui_explorer/src/examples.ts @@ -15,8 +15,8 @@ * limitations under the License. */ -import { basicCatalog } from "@a2ui/lit/v0_9"; -import { A2uiMessage, CreateSurfaceMessage } from "@a2ui/web_core/v0_9"; +import {basicCatalog} from '@a2ui/lit/v0_9'; +import {A2uiMessage, CreateSurfaceMessage} from '@a2ui/web_core/v0_9'; /** * Represents a demo item loaded from an example JSON file. @@ -47,14 +47,10 @@ export function getDemoItems(): DemoItem[] { for (const [path, data] of sortedEntries) { try { - const filename = path.substring(path.lastIndexOf("/") + 1); + const filename = path.substring(path.lastIndexOf('/') + 1); const jsonData = data.default; - const [messages, description] = extractMessagesAndDescription( - jsonData, - filename, - path, - ); + const [messages, description] = extractMessagesAndDescription(jsonData, filename, path); const surfaceId = ensureCreateSurfaceMessage(filename, messages); @@ -71,7 +67,7 @@ export function getDemoItems(): DemoItem[] { } if (items.length === 0) { - console.warn("No demo items were found."); + console.warn('No demo items were found.'); } return items; @@ -89,16 +85,16 @@ export function getDemoItems(): DemoItem[] { * @returns The surfaceId for the createSurface message of this set of messages. */ function ensureCreateSurfaceMessage(filename: string, messages: A2uiMessage[]): string { - let surfaceId = filename.replace(".json", ""); + let surfaceId = filename.replace('.json', ''); const createMsg = messages.find( - (message): message is CreateSurfaceMessage => "createSurface" in message, + (message): message is CreateSurfaceMessage => 'createSurface' in message, ); if (createMsg) { surfaceId = createMsg.createSurface.surfaceId; } else { messages.unshift({ - version: "v0.9", + version: 'v0.9', createSurface: { surfaceId, catalogId: basicCatalog.id, @@ -141,13 +137,11 @@ function getSortedExampleEntries(): [string, ExampleModule][] { // pass it as a parameter because Vite needs to statically analyze it at build // time. const exampleModules = import.meta.glob( - "../../../../specification/v0_9/json/catalogs/basic/examples/*.json", - { eager: true }, + '../../../../specification/v0_9/json/catalogs/basic/examples/*.json', + {eager: true}, ); - return Object.entries(exampleModules).sort((a, b) => - a[0].localeCompare(b[0]), - ); + return Object.entries(exampleModules).sort((a, b) => a[0].localeCompare(b[0])); } /** @@ -195,8 +189,8 @@ function extractMessagesAndDescription( */ function filenameToTitle(filename: string): string { return filename - .split("_") - .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) - .join(" ") - .replace(".json", ""); + .split('_') + .map(word => word.charAt(0).toUpperCase() + word.slice(1)) + .join(' ') + .replace('.json', ''); } diff --git a/renderers/lit/a2ui_explorer/src/local-gallery.css.ts b/renderers/lit/a2ui_explorer/src/local-gallery.css.ts index 45847805a..2a038395b 100644 --- a/renderers/lit/a2ui_explorer/src/local-gallery.css.ts +++ b/renderers/lit/a2ui_explorer/src/local-gallery.css.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { css } from "lit"; +import {css} from 'lit'; /** * Styles for the LocalGallery component. @@ -185,7 +185,7 @@ export const appStyles = css` flex: 1; overflow-y: auto; padding: 16px; - font-family: "JetBrains Mono", monospace; + font-family: 'JetBrains Mono', monospace; font-size: 0.8rem; white-space: pre-wrap; } diff --git a/renderers/lit/a2ui_explorer/src/local-gallery.ts b/renderers/lit/a2ui_explorer/src/local-gallery.ts index 4db532eed..d3781e4d0 100644 --- a/renderers/lit/a2ui_explorer/src/local-gallery.ts +++ b/renderers/lit/a2ui_explorer/src/local-gallery.ts @@ -14,34 +14,31 @@ * limitations under the License. */ -import { LitElement, html, css, nothing } from "lit"; -import { provide } from "@lit/context"; -import { customElement, state } from "lit/decorators.js"; -import { MessageProcessor } from "@a2ui/web_core/v0_9"; -import { basicCatalog, Context } from "@a2ui/lit/v0_9"; -import { renderMarkdown } from "@a2ui/markdown-it"; -import { getDemoItems, DemoItem } from "./examples"; -import { appStyles } from "./local-gallery.css"; - -@customElement("local-gallery") +import {LitElement, html, css, nothing} from 'lit'; +import {provide} from '@lit/context'; +import {customElement, state} from 'lit/decorators.js'; +import {MessageProcessor} from '@a2ui/web_core/v0_9'; +import {basicCatalog, Context} from '@a2ui/lit/v0_9'; +import {renderMarkdown} from '@a2ui/markdown-it'; +import {getDemoItems, DemoItem} from './examples'; +import {appStyles} from './local-gallery.css'; + +@customElement('local-gallery') export class LocalGallery extends LitElement { @state() accessor mockLogs: string[] = []; @state() accessor demoItems: DemoItem[] = []; @state() accessor activeItemIndex = 0; @state() accessor processedMessageCount = 0; - @state() accessor currentDataModelText = "{}"; + @state() accessor currentDataModelText = '{}'; - @provide({ context: Context.markdown }) + @provide({context: Context.markdown}) private accessor markdownRenderer = renderMarkdown; - private processor = new MessageProcessor( - [basicCatalog], - (action: any) => { - this.log(`Action dispatched: ${action.surfaceId}`, action); - }, - ); + private processor = new MessageProcessor([basicCatalog], (action: any) => { + this.log(`Action dispatched: ${action.surfaceId}`, action); + }); - private dataModelSubscription?: { unsubscribe: () => void }; + private dataModelSubscription?: {unsubscribe: () => void}; static styles = [appStyles]; @@ -77,7 +74,7 @@ export class LocalGallery extends LitElement { resetSurface() { this.processedMessageCount = 0; this.mockLogs = []; - this.currentDataModelText = "{}"; + this.currentDataModelText = '{}'; // Clear old surface and subscriptions if (this.dataModelSubscription) { @@ -87,9 +84,7 @@ export class LocalGallery extends LitElement { const item = this.demoItems[this.activeItemIndex]; if (item && this.processor.model.getSurface(item.id)) { - this.processor.processMessages([ - { version: "v0.9", deleteSurface: { surfaceId: item.id } }, - ]); + this.processor.processMessages([{version: 'v0.9', deleteSurface: {surfaceId: item.id}}]); } } @@ -110,7 +105,7 @@ export class LocalGallery extends LitElement { if (!this.dataModelSubscription) { const surface = this.processor.model.getSurface(item.id); if (surface) { - this.dataModelSubscription = surface.dataModel.subscribe("/", (val) => { + this.dataModelSubscription = surface.dataModel.subscribe('/', val => { this.currentDataModelText = JSON.stringify(val || {}, null, 2); }); } @@ -125,11 +120,8 @@ export class LocalGallery extends LitElement { render() { const activeItem = this.demoItems[this.activeItemIndex]; - const surface = activeItem - ? this.processor.model.getSurface(activeItem.id) - : undefined; - const canAdvance = - activeItem && this.processedMessageCount < activeItem.messages.length; + const surface = activeItem ? this.processor.model.getSurface(activeItem.id) : undefined; + const canAdvance = activeItem && this.processedMessageCount < activeItem.messages.length; return html`
@@ -143,7 +135,7 @@ export class LocalGallery extends LitElement { ${this.demoItems.map( (item, i) => html` diff --git a/renderers/lit/a2ui_explorer/src/theme.ts b/renderers/lit/a2ui_explorer/src/theme.ts index c7e91cd1d..6276ee6ce 100644 --- a/renderers/lit/a2ui_explorer/src/theme.ts +++ b/renderers/lit/a2ui_explorer/src/theme.ts @@ -14,137 +14,137 @@ * limitations under the License. */ -import { v0_8 } from "@a2ui/lit"; +import {v0_8} from '@a2ui/lit'; /** Elements */ const a = { - "typography-f-sf": true, - "typography-fs-n": true, - "typography-w-500": true, - "layout-as-n": true, - "layout-dis-iflx": true, - "layout-al-c": true, - "typography-td-none": true, - "color-c-p40": true, + 'typography-f-sf': true, + 'typography-fs-n': true, + 'typography-w-500': true, + 'layout-as-n': true, + 'layout-dis-iflx': true, + 'layout-al-c': true, + 'typography-td-none': true, + 'color-c-p40': true, }; const audio = { - "layout-w-100": true, + 'layout-w-100': true, }; const body = { - "typography-f-s": true, - "typography-fs-n": true, - "typography-w-400": true, - "layout-mt-0": true, - "layout-mb-2": true, - "typography-sz-bm": true, - "color-c-n10": true, + 'typography-f-s': true, + 'typography-fs-n': true, + 'typography-w-400': true, + 'layout-mt-0': true, + 'layout-mb-2': true, + 'typography-sz-bm': true, + 'color-c-n10': true, }; const button = { - "typography-f-sf": true, - "typography-fs-n": true, - "typography-w-500": true, - "layout-pt-3": true, - "layout-pb-3": true, - "layout-pl-5": true, - "layout-pr-5": true, - "layout-mb-1": true, - "border-br-16": true, - "border-bw-0": true, - "border-c-n70": true, - "border-bs-s": true, - "color-bgc-s30": true, - "behavior-ho-80": true, + 'typography-f-sf': true, + 'typography-fs-n': true, + 'typography-w-500': true, + 'layout-pt-3': true, + 'layout-pb-3': true, + 'layout-pl-5': true, + 'layout-pr-5': true, + 'layout-mb-1': true, + 'border-br-16': true, + 'border-bw-0': true, + 'border-c-n70': true, + 'border-bs-s': true, + 'color-bgc-s30': true, + 'behavior-ho-80': true, }; const heading = { - "typography-f-sf": true, - "typography-fs-n": true, - "typography-w-500": true, - "layout-mt-0": true, - "layout-mb-2": true, + 'typography-f-sf': true, + 'typography-fs-n': true, + 'typography-w-500': true, + 'layout-mt-0': true, + 'layout-mb-2': true, }; const iframe = { - "behavior-sw-n": true, + 'behavior-sw-n': true, }; const input = { - "typography-f-sf": true, - "typography-fs-n": true, - "typography-w-400": true, - "layout-pl-4": true, - "layout-pr-4": true, - "layout-pt-2": true, - "layout-pb-2": true, - "border-br-6": true, - "border-bw-1": true, - "color-bc-s70": true, - "border-bs-s": true, - "layout-as-n": true, - "color-c-n10": true, + 'typography-f-sf': true, + 'typography-fs-n': true, + 'typography-w-400': true, + 'layout-pl-4': true, + 'layout-pr-4': true, + 'layout-pt-2': true, + 'layout-pb-2': true, + 'border-br-6': true, + 'border-bw-1': true, + 'color-bc-s70': true, + 'border-bs-s': true, + 'layout-as-n': true, + 'color-c-n10': true, }; const p = { - "typography-f-s": true, - "typography-fs-n": true, - "typography-w-400": true, - "layout-m-0": true, - "typography-sz-bm": true, - "layout-as-n": true, - "color-c-n10": true, + 'typography-f-s': true, + 'typography-fs-n': true, + 'typography-w-400': true, + 'layout-m-0': true, + 'typography-sz-bm': true, + 'layout-as-n': true, + 'color-c-n10': true, }; const orderedList = { - "typography-f-s": true, - "typography-fs-n": true, - "typography-w-400": true, - "layout-m-0": true, - "typography-sz-bm": true, - "layout-as-n": true, - "color-c-n10": true, + 'typography-f-s': true, + 'typography-fs-n': true, + 'typography-w-400': true, + 'layout-m-0': true, + 'typography-sz-bm': true, + 'layout-as-n': true, + 'color-c-n10': true, }; const unorderedList = { - "typography-f-s": true, - "typography-fs-n": true, - "typography-w-400": true, - "layout-m-0": true, - "typography-sz-bm": true, - "layout-as-n": true, - "color-c-n10": true, + 'typography-f-s': true, + 'typography-fs-n': true, + 'typography-w-400': true, + 'layout-m-0': true, + 'typography-sz-bm': true, + 'layout-as-n': true, + 'color-c-n10': true, }; const listItem = { - "typography-f-s": true, - "typography-fs-n": true, - "typography-w-400": true, - "layout-m-0": true, - "typography-sz-bm": true, - "layout-as-n": true, - "color-c-n10": true, + 'typography-f-s': true, + 'typography-fs-n': true, + 'typography-w-400': true, + 'layout-m-0': true, + 'typography-sz-bm': true, + 'layout-as-n': true, + 'color-c-n10': true, }; const pre = { - "typography-f-c": true, - "typography-fs-n": true, - "typography-w-400": true, - "typography-sz-bm": true, - "typography-ws-p": true, - "layout-as-n": true, + 'typography-f-c': true, + 'typography-fs-n': true, + 'typography-w-400': true, + 'typography-sz-bm': true, + 'typography-ws-p': true, + 'layout-as-n': true, }; const textarea = { ...input, - "layout-r-none": true, - "layout-fs-c": true, + 'layout-r-none': true, + 'layout-fs-c': true, }; const video = { - "layout-el-cv": true, + 'layout-el-cv': true, }; const aLight = v0_8.Styles.merge(a, {}); @@ -161,38 +161,38 @@ const listItemLight = v0_8.Styles.merge(listItem, {}); export const theme: v0_8.Types.Theme = { additionalStyles: { Button: { - "--n-35": "var(--n-100)", - "--n-10": "var(--n-0)", + '--n-35': 'var(--n-100)', + '--n-10': 'var(--n-0)', background: - "linear-gradient(135deg, light-dark(#818cf8, #06b6d4) 0%, light-dark(#a78bfa, #3b82f6) 100%)", - boxShadow: "0 4px 15px rgba(102, 126, 234, 0.4)", - padding: "12px 28px", - textTransform: "uppercase", + 'linear-gradient(135deg, light-dark(#818cf8, #06b6d4) 0%, light-dark(#a78bfa, #3b82f6) 100%)', + boxShadow: '0 4px 15px rgba(102, 126, 234, 0.4)', + padding: '12px 28px', + textTransform: 'uppercase', }, Text: { h1: { - color: "transparent", + color: 'transparent', background: - "linear-gradient(135deg, light-dark(#818cf8, #06b6d4) 0%, light-dark(#a78bfa, #3b82f6) 100%)", - "-webkit-background-clip": "text", - "background-clip": "text", - "-webkit-text-fill-color": "transparent", + 'linear-gradient(135deg, light-dark(#818cf8, #06b6d4) 0%, light-dark(#a78bfa, #3b82f6) 100%)', + '-webkit-background-clip': 'text', + 'background-clip': 'text', + '-webkit-text-fill-color': 'transparent', }, h2: { - color: "transparent", + color: 'transparent', background: - "linear-gradient(135deg, light-dark(#818cf8, #06b6d4) 0%, light-dark(#a78bfa, #3b82f6) 100%)", - "-webkit-background-clip": "text", - "background-clip": "text", - "-webkit-text-fill-color": "transparent", + 'linear-gradient(135deg, light-dark(#818cf8, #06b6d4) 0%, light-dark(#a78bfa, #3b82f6) 100%)', + '-webkit-background-clip': 'text', + 'background-clip': 'text', + '-webkit-text-fill-color': 'transparent', }, h3: { - color: "transparent", + color: 'transparent', background: - "linear-gradient(135deg, light-dark(#818cf8, #06b6d4) 0%, light-dark(#a78bfa, #3b82f6) 100%)", - "-webkit-background-clip": "text", - "background-clip": "text", - "-webkit-text-fill-color": "transparent", + 'linear-gradient(135deg, light-dark(#818cf8, #06b6d4) 0%, light-dark(#a78bfa, #3b82f6) 100%)', + '-webkit-background-clip': 'text', + 'background-clip': 'text', + '-webkit-text-fill-color': 'transparent', }, h4: {}, h5: {}, @@ -201,106 +201,106 @@ export const theme: v0_8.Types.Theme = { }, Card: { background: - "radial-gradient(circle at top left, light-dark(transparent, rgba(6, 182, 212, 0.15)), transparent 40%), radial-gradient(circle at bottom right, light-dark(transparent, rgba(139, 92, 246, 0.15)), transparent 40%), linear-gradient(135deg, light-dark(rgba(255, 255, 255, 0.7), rgba(30, 41, 59, 0.7)), light-dark(rgba(255, 255, 255, 0.7), rgba(15, 23, 42, 0.8)))", + 'radial-gradient(circle at top left, light-dark(transparent, rgba(6, 182, 212, 0.15)), transparent 40%), radial-gradient(circle at bottom right, light-dark(transparent, rgba(139, 92, 246, 0.15)), transparent 40%), linear-gradient(135deg, light-dark(rgba(255, 255, 255, 0.7), rgba(30, 41, 59, 0.7)), light-dark(rgba(255, 255, 255, 0.7), rgba(15, 23, 42, 0.8)))', }, TextField: { - "--p-0": "light-dark(var(--n-0), #1e293b)", + '--p-0': 'light-dark(var(--n-0), #1e293b)', }, Modal: { background: - "linear-gradient(135deg, light-dark(rgba(255, 255, 255, 0.9), rgba(30, 41, 59, 1)), light-dark(rgba(255, 255, 255, 0.95), rgba(15, 23, 42, 1)))", - boxShadow: "0 10px 25px -5px rgba(0, 0, 0, 0.3), 0 8px 10px -6px rgba(0, 0, 0, 0.2)", - borderRadius: "8px", - padding: "16px", - minWidth: "300px", - maxWidth: "80vw", - display: "flex", - flexDirection: "column", + 'linear-gradient(135deg, light-dark(rgba(255, 255, 255, 0.9), rgba(30, 41, 59, 1)), light-dark(rgba(255, 255, 255, 0.95), rgba(15, 23, 42, 1)))', + boxShadow: '0 10px 25px -5px rgba(0, 0, 0, 0.3), 0 8px 10px -6px rgba(0, 0, 0, 0.2)', + borderRadius: '8px', + padding: '16px', + minWidth: '300px', + maxWidth: '80vw', + display: 'flex', + flexDirection: 'column', }, MultipleChoice: { - "--md-sys-color-secondary-container-high": "#e8def8", + '--md-sys-color-secondary-container-high': '#e8def8', }, }, components: { AudioPlayer: {}, Button: { - "layout-pt-2": true, - "layout-pb-2": true, - "layout-pl-3": true, - "layout-pr-3": true, - "border-br-12": true, - "border-bw-0": true, - "border-bs-s": true, - "color-bgc-p30": true, - "behavior-ho-70": true, - "typography-w-400": true, + 'layout-pt-2': true, + 'layout-pb-2': true, + 'layout-pl-3': true, + 'layout-pr-3': true, + 'border-br-12': true, + 'border-bw-0': true, + 'border-bs-s': true, + 'color-bgc-p30': true, + 'behavior-ho-70': true, + 'typography-w-400': true, }, - Card: { "border-br-9": true, "layout-p-4": true, "color-bgc-n100": true }, + Card: {'border-br-9': true, 'layout-p-4': true, 'color-bgc-n100': true}, CheckBox: { element: { - "layout-m-0": true, - "layout-mr-2": true, - "layout-p-2": true, - "border-br-12": true, - "border-bw-1": true, - "border-bs-s": true, - "color-bgc-p100": true, - "color-bc-p60": true, - "color-c-n30": true, - "color-c-p30": true, + 'layout-m-0': true, + 'layout-mr-2': true, + 'layout-p-2': true, + 'border-br-12': true, + 'border-bw-1': true, + 'border-bs-s': true, + 'color-bgc-p100': true, + 'color-bc-p60': true, + 'color-c-n30': true, + 'color-c-p30': true, }, label: { - "color-c-p30": true, - "typography-f-sf": true, - "typography-v-r": true, - "typography-w-400": true, - "layout-flx-1": true, - "typography-sz-ll": true, + 'color-c-p30': true, + 'typography-f-sf': true, + 'typography-v-r': true, + 'typography-w-400': true, + 'layout-flx-1': true, + 'typography-sz-ll': true, }, container: { - "layout-dsp-iflex": true, - "layout-al-c": true, + 'layout-dsp-iflex': true, + 'layout-al-c': true, }, }, Column: { - "layout-g-2": true, + 'layout-g-2': true, }, DateTimeInput: { container: { - "typography-sz-bm": true, - "layout-w-100": true, - "layout-g-2": true, - "layout-dsp-flexhor": true, - "layout-al-c": true, - "typography-ws-nw": true, + 'typography-sz-bm': true, + 'layout-w-100': true, + 'layout-g-2': true, + 'layout-dsp-flexhor': true, + 'layout-al-c': true, + 'typography-ws-nw': true, }, label: { - "color-c-p30": true, - "typography-sz-bm": true, + 'color-c-p30': true, + 'typography-sz-bm': true, }, element: { - "layout-pt-2": true, - "layout-pb-2": true, - "layout-pl-3": true, - "layout-pr-3": true, - "border-br-2": true, - "border-bw-1": true, - "border-bs-s": true, - "color-bgc-p100": true, - "color-bc-p60": true, - "color-c-n30": true, - "color-c-p30": true, + 'layout-pt-2': true, + 'layout-pb-2': true, + 'layout-pl-3': true, + 'layout-pr-3': true, + 'border-br-2': true, + 'border-bw-1': true, + 'border-bs-s': true, + 'color-bgc-p100': true, + 'color-bc-p60': true, + 'color-c-n30': true, + 'color-c-p30': true, }, }, Divider: {}, Image: { all: { - "border-br-5": true, - "layout-el-cv": true, - "layout-w-100": true, - "layout-h-100": true, + 'border-br-5': true, + 'layout-el-cv': true, + 'layout-w-100': true, + 'layout-h-100': true, }, - avatar: { "is-avatar": true }, + avatar: {'is-avatar': true}, header: {}, icon: {}, largeFeature: {}, @@ -309,18 +309,18 @@ export const theme: v0_8.Types.Theme = { }, Icon: {}, List: { - "layout-g-4": true, - "layout-p-2": true, + 'layout-g-4': true, + 'layout-p-2': true, }, Modal: { - backdrop: { "color-bbgc-p60_20": true }, + backdrop: {'color-bbgc-p60_20': true}, element: { - "border-br-2": true, - "color-bgc-p100": true, - "layout-p-4": true, - "border-bw-1": true, - "border-bs-s": true, - "color-bc-p80": true, + 'border-br-2': true, + 'color-bgc-p100': true, + 'layout-p-4': true, + 'border-bw-1': true, + 'border-bs-s': true, + 'color-bc-p80': true, }, }, MultipleChoice: { @@ -329,7 +329,7 @@ export const theme: v0_8.Types.Theme = { element: {}, }, Row: { - "layout-g-4": true, + 'layout-g-4': true, }, Slider: { container: {}, @@ -338,88 +338,88 @@ export const theme: v0_8.Types.Theme = { }, Tabs: { container: {}, - controls: { all: {}, selected: {} }, + controls: {all: {}, selected: {}}, element: {}, }, Text: { all: { - "layout-w-100": true, - "layout-g-2": true, + 'layout-w-100': true, + 'layout-g-2': true, }, h1: { - "typography-f-sf": true, - "typography-v-r": true, - "typography-w-400": true, - "layout-m-0": true, - "layout-p-0": true, - "typography-sz-hs": true, + 'typography-f-sf': true, + 'typography-v-r': true, + 'typography-w-400': true, + 'layout-m-0': true, + 'layout-p-0': true, + 'typography-sz-hs': true, }, h2: { - "typography-f-sf": true, - "typography-v-r": true, - "typography-w-400": true, - "layout-m-0": true, - "layout-p-0": true, - "typography-sz-tl": true, + 'typography-f-sf': true, + 'typography-v-r': true, + 'typography-w-400': true, + 'layout-m-0': true, + 'layout-p-0': true, + 'typography-sz-tl': true, }, h3: { - "typography-f-sf": true, - "typography-v-r": true, - "typography-w-400": true, - "layout-m-0": true, - "layout-p-0": true, - "typography-sz-tl": true, + 'typography-f-sf': true, + 'typography-v-r': true, + 'typography-w-400': true, + 'layout-m-0': true, + 'layout-p-0': true, + 'typography-sz-tl': true, }, h4: { - "typography-f-sf": true, - "typography-v-r": true, - "typography-w-400": true, - "layout-m-0": true, - "layout-p-0": true, - "typography-sz-bl": true, + 'typography-f-sf': true, + 'typography-v-r': true, + 'typography-w-400': true, + 'layout-m-0': true, + 'layout-p-0': true, + 'typography-sz-bl': true, }, h5: { - "typography-f-sf": true, - "typography-v-r": true, - "typography-w-400": true, - "layout-m-0": true, - "layout-p-0": true, - "typography-sz-bm": true, + 'typography-f-sf': true, + 'typography-v-r': true, + 'typography-w-400': true, + 'layout-m-0': true, + 'layout-p-0': true, + 'typography-sz-bm': true, }, body: {}, caption: {}, }, TextField: { container: { - "typography-sz-bm": true, - "layout-w-100": true, - "layout-g-2": true, - "layout-dsp-flexhor": true, - "layout-al-c": true, - "typography-ws-nw": true, + 'typography-sz-bm': true, + 'layout-w-100': true, + 'layout-g-2': true, + 'layout-dsp-flexhor': true, + 'layout-al-c': true, + 'typography-ws-nw': true, }, label: { - "layout-flx-0": true, - "color-c-p30": true, + 'layout-flx-0': true, + 'color-c-p30': true, }, element: { - "typography-sz-bm": true, - "layout-pt-2": true, - "layout-pb-2": true, - "layout-pl-3": true, - "layout-pr-3": true, - "border-br-2": true, - "border-bw-1": true, - "border-bs-s": true, - "color-bgc-p100": true, - "color-bc-p60": true, - "color-c-n30": true, - "color-c-p30": true, + 'typography-sz-bm': true, + 'layout-pt-2': true, + 'layout-pb-2': true, + 'layout-pl-3': true, + 'layout-pr-3': true, + 'border-br-2': true, + 'border-bw-1': true, + 'border-bs-s': true, + 'color-bgc-p100': true, + 'color-bc-p60': true, + 'color-c-n30': true, + 'color-c-p30': true, }, }, Video: { - "border-br-5": true, - "layout-el-cv": true, + 'border-br-5': true, + 'layout-el-cv': true, }, }, elements: { diff --git a/renderers/lit/a2ui_explorer/vite.config.ts b/renderers/lit/a2ui_explorer/vite.config.ts index f45902589..c7e1e8547 100644 --- a/renderers/lit/a2ui_explorer/vite.config.ts +++ b/renderers/lit/a2ui_explorer/vite.config.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { defineConfig } from 'vite'; +import {defineConfig} from 'vite'; export default defineConfig({ build: { @@ -25,7 +25,7 @@ export default defineConfig({ }, server: { fs: { - allow: ['../', '../../../specification'] - } - } + allow: ['../', '../../../specification'], + }, + }, }); diff --git a/renderers/lit/src/0.8/core.ts b/renderers/lit/src/0.8/core.ts index 546419de3..79d259619 100644 --- a/renderers/lit/src/0.8/core.ts +++ b/renderers/lit/src/0.8/core.ts @@ -14,16 +14,16 @@ * limitations under the License. */ -export * as Events from "./events/events.js"; -import * as Types from "@a2ui/web_core/types/types"; -import * as Guards from "@a2ui/web_core/data/guards"; -import { Schemas } from "@a2ui/web_core"; -import * as Styles from "@a2ui/web_core/styles/index"; -import { A2uiMessageProcessor } from "@a2ui/web_core/data/model-processor"; -import * as Primitives from "@a2ui/web_core/types/primitives"; -import { create as createSignalA2uiMessageProcessor } from "./data/signal-model-processor.js"; -import { Events as WebEvents } from "@a2ui/web_core"; -export { Types, Guards, Schemas, Styles, A2uiMessageProcessor, Primitives, WebEvents }; +export * as Events from './events/events.js'; +import * as Types from '@a2ui/web_core/types/types'; +import * as Guards from '@a2ui/web_core/data/guards'; +import {Schemas} from '@a2ui/web_core'; +import * as Styles from '@a2ui/web_core/styles/index'; +import {A2uiMessageProcessor} from '@a2ui/web_core/data/model-processor'; +import * as Primitives from '@a2ui/web_core/types/primitives'; +import {create as createSignalA2uiMessageProcessor} from './data/signal-model-processor.js'; +import {Events as WebEvents} from '@a2ui/web_core'; +export {Types, Guards, Schemas, Styles, A2uiMessageProcessor, Primitives, WebEvents}; export const Data = { createSignalA2uiMessageProcessor, diff --git a/renderers/lit/src/0.8/data/signal-model-processor.ts b/renderers/lit/src/0.8/data/signal-model-processor.ts index dc17b2248..c9edeadbc 100644 --- a/renderers/lit/src/0.8/data/signal-model-processor.ts +++ b/renderers/lit/src/0.8/data/signal-model-processor.ts @@ -14,12 +14,12 @@ * limitations under the License. */ -import { A2uiMessageProcessor } from "@a2ui/web_core/data/model-processor"; +import {A2uiMessageProcessor} from '@a2ui/web_core/data/model-processor'; -import { SignalArray } from "signal-utils/array"; -import { SignalMap } from "signal-utils/map"; -import { SignalObject } from "signal-utils/object"; -import { SignalSet } from "signal-utils/set"; +import {SignalArray} from 'signal-utils/array'; +import {SignalMap} from 'signal-utils/map'; +import {SignalObject} from 'signal-utils/object'; +import {SignalSet} from 'signal-utils/set'; export function create() { return new A2uiMessageProcessor({ diff --git a/renderers/lit/src/0.8/events/a2ui.ts b/renderers/lit/src/0.8/events/a2ui.ts index 01abea001..a279e3507 100644 --- a/renderers/lit/src/0.8/events/a2ui.ts +++ b/renderers/lit/src/0.8/events/a2ui.ts @@ -14,10 +14,10 @@ * limitations under the License. */ -import * as Types from "@a2ui/web_core/types/types"; -import { BaseEventDetail } from "./base.js"; +import * as Types from '@a2ui/web_core/types/types'; +import {BaseEventDetail} from './base.js'; -type Namespace = "a2ui"; +type Namespace = 'a2ui'; export interface A2UIAction extends BaseEventDetail<`${Namespace}.action`> { readonly action: Types.Action; diff --git a/renderers/lit/src/0.8/events/events.ts b/renderers/lit/src/0.8/events/events.ts index acdb50fd3..2e9a00068 100644 --- a/renderers/lit/src/0.8/events/events.ts +++ b/renderers/lit/src/0.8/events/events.ts @@ -14,8 +14,8 @@ * limitations under the License. */ -import type * as A2UI from "./a2ui.js"; -import { BaseEventDetail } from "./base.js"; +import type * as A2UI from './a2ui.js'; +import {BaseEventDetail} from './base.js'; const eventInit = { bubbles: true, @@ -23,31 +23,30 @@ const eventInit = { composed: true, }; -type EnforceEventTypeMatch>> = - { - [K in keyof T]: T[K] extends BaseEventDetail - ? EventType extends K - ? T[K] - : never - : never; - }; +type EnforceEventTypeMatch>> = { + [K in keyof T]: T[K] extends BaseEventDetail + ? EventType extends K + ? T[K] + : never + : never; +}; export type StateEventDetailMap = EnforceEventTypeMatch<{ - "a2ui.action": A2UI.A2UIAction; + 'a2ui.action': A2UI.A2UIAction; }>; -export class StateEvent< - T extends keyof StateEventDetailMap -> extends CustomEvent { - static eventName = "a2uiaction"; +export class StateEvent extends CustomEvent< + StateEventDetailMap[T] +> { + static eventName = 'a2uiaction'; constructor(readonly payload: StateEventDetailMap[T]) { - super(StateEvent.eventName, { detail: payload, ...eventInit }); + super(StateEvent.eventName, {detail: payload, ...eventInit}); } } declare global { interface HTMLElementEventMap { - a2uiaction: StateEvent<"a2ui.action">; + a2uiaction: StateEvent<'a2ui.action'>; } } diff --git a/renderers/lit/src/0.8/index.ts b/renderers/lit/src/0.8/index.ts index 427109b4e..9d2d26cbb 100644 --- a/renderers/lit/src/0.8/index.ts +++ b/renderers/lit/src/0.8/index.ts @@ -14,5 +14,5 @@ * limitations under the License. */ -export * from "./core.js"; -export * as UI from "./ui/ui.js"; +export * from './core.js'; +export * as UI from './ui/ui.js'; diff --git a/renderers/lit/src/0.8/model.test.ts b/renderers/lit/src/0.8/model.test.ts index 0ff24d928..c30fc5f03 100644 --- a/renderers/lit/src/0.8/model.test.ts +++ b/renderers/lit/src/0.8/model.test.ts @@ -15,26 +15,21 @@ * limitations under the License. */ -import assert from "node:assert"; -import { describe, it, beforeEach } from "node:test"; -import * as v0_8 from "@a2ui/lit/v0_8"; -import * as Types from "@a2ui/web_core/types/types"; -import { A2uiStateError } from "@a2ui/web_core/v0_8"; +import assert from 'node:assert'; +import {describe, it, beforeEach} from 'node:test'; +import * as v0_8 from '@a2ui/lit/v0_8'; +import * as Types from '@a2ui/web_core/types/types'; +import {A2uiStateError} from '@a2ui/web_core/v0_8'; // Helper function to strip reactivity for clean comparisons. const toPlainObject = (value: unknown): ReturnType => { if (value instanceof Map) { - return Object.fromEntries( - Array.from(value.entries(), ([k, v]) => [k, toPlainObject(v)]) - ); + return Object.fromEntries(Array.from(value.entries(), ([k, v]) => [k, toPlainObject(v)])); } if (Array.isArray(value)) { return value.map(toPlainObject); } - if ( - v0_8.Data.Guards.isObject(value) && - value.constructor.name === "SignalObject" - ) { + if (v0_8.Data.Guards.isObject(value) && value.constructor.name === 'SignalObject') { const obj: Record = {}; for (const key in value) { if (Object.prototype.hasOwnProperty.call(value, key)) { @@ -47,24 +42,24 @@ const toPlainObject = (value: unknown): ReturnType => { return value; }; -describe("A2uiMessageProcessor", () => { +describe('A2uiMessageProcessor', () => { let processor = new v0_8.Data.A2uiMessageProcessor(); beforeEach(() => { processor = new v0_8.Data.A2uiMessageProcessor(); }); - describe("Basic Initialization and State", () => { - it("should start with no surfaces", () => { + describe('Basic Initialization and State', () => { + it('should start with no surfaces', () => { assert.strictEqual(processor.getSurfaces().size, 0); }); - it("should clear surfaces when clearSurfaces is called", () => { + it('should clear surfaces when clearSurfaces is called', () => { processor.processMessages([ { beginRendering: { - root: "root", - surfaceId: "@default", + root: 'root', + surfaceId: '@default', }, }, ]); @@ -74,29 +69,29 @@ describe("A2uiMessageProcessor", () => { }); }); - describe("Message Processing", () => { - it("should handle `beginRendering` by creating a default surface", () => { + describe('Message Processing', () => { + it('should handle `beginRendering` by creating a default surface', () => { processor.processMessages([ { beginRendering: { - root: "comp-a", - styles: { primaryColor: "#0000ff" }, - surfaceId: "@default", + root: 'comp-a', + styles: {primaryColor: '#0000ff'}, + surfaceId: '@default', }, }, ]); const surfaces = processor.getSurfaces(); assert.strictEqual(surfaces.size, 1); - const defaultSurface = surfaces.get("@default"); - assert.ok(defaultSurface, "Default surface should exist"); - assert.strictEqual(defaultSurface!.rootComponentId, "comp-a"); - assert.deepStrictEqual(defaultSurface!.styles, { primaryColor: "#0000ff" }); + const defaultSurface = surfaces.get('@default'); + assert.ok(defaultSurface, 'Default surface should exist'); + assert.strictEqual(defaultSurface!.rootComponentId, 'comp-a'); + assert.deepStrictEqual(defaultSurface!.styles, {primaryColor: '#0000ff'}); }); - it("should handle `surfaceUpdate` by adding components", () => { - const surfaceId = "@default"; - const rootComponentId = "comp-a"; + it('should handle `surfaceUpdate` by adding components', () => { + const surfaceId = '@default'; + const rootComponentId = 'comp-a'; const messages = [ { beginRendering: { @@ -111,7 +106,7 @@ describe("A2uiMessageProcessor", () => { { id: rootComponentId, component: { - Text: { usageHint: "body", text: { literalString: "Hi" } }, + Text: {usageHint: 'body', text: {literalString: 'Hi'}}, }, }, ], @@ -121,111 +116,109 @@ describe("A2uiMessageProcessor", () => { processor.processMessages(messages); const surface = processor.getSurfaces().get(surfaceId); if (!surface) { - assert.fail("No default surface"); + assert.fail('No default surface'); } assert.strictEqual(surface!.components.size, 1); assert.ok(surface!.components.has(rootComponentId)); }); - it("should handle `deleteSurface`", () => { + it('should handle `deleteSurface`', () => { processor.processMessages([ { - beginRendering: { root: "root", surfaceId: "to-delete" }, + beginRendering: {root: 'root', surfaceId: 'to-delete'}, }, - { deleteSurface: { surfaceId: "to-delete" } }, + {deleteSurface: {surfaceId: 'to-delete'}}, ]); - assert.strictEqual(processor.getSurfaces().has("to-delete"), false); + assert.strictEqual(processor.getSurfaces().has('to-delete'), false); }); }); - describe("Data Model Updates", () => { - it("should update data at a specified path", () => { + describe('Data Model Updates', () => { + it('should update data at a specified path', () => { processor.processMessages([ { dataModelUpdate: { - surfaceId: "@default", - path: "/user", - contents: [{ key: "name", valueString: "Alice" }], + surfaceId: '@default', + path: '/user', + contents: [{key: 'name', valueString: 'Alice'}], }, }, ]); const name = processor.getData( - { dataContextPath: "/" } as v0_8.Types.AnyComponentNode, - "/user/name" + {dataContextPath: '/'} as v0_8.Types.AnyComponentNode, + '/user/name', ); - assert.strictEqual(name, "Alice"); + assert.strictEqual(name, 'Alice'); }); - it("should replace the entire data model when path is not provided", () => { + it('should replace the entire data model when path is not provided', () => { processor.processMessages([ { dataModelUpdate: { - surfaceId: "@default", - path: "/", - contents: [ - { key: "user", valueString: JSON.stringify({ name: "Bob" }) }, - ], + surfaceId: '@default', + path: '/', + contents: [{key: 'user', valueString: JSON.stringify({name: 'Bob'})}], }, }, ]); const user = processor.getData( - { dataContextPath: "/" } as v0_8.Types.AnyComponentNode, - "/user" + {dataContextPath: '/'} as v0_8.Types.AnyComponentNode, + '/user', ); - assert.deepStrictEqual(toPlainObject(user), { name: "Bob" }); + assert.deepStrictEqual(toPlainObject(user), {name: 'Bob'}); }); - it("should create nested structures when setting data", () => { - const component = { dataContextPath: "/" } as v0_8.Types.AnyComponentNode; + it('should create nested structures when setting data', () => { + const component = {dataContextPath: '/'} as v0_8.Types.AnyComponentNode; // Note: setData is a public method that does not use the key-value format - processor.setData(component, "/a/b/c", "value"); - const data = processor.getData(component, "/a/b/c"); - assert.strictEqual(data, "value"); + processor.setData(component, '/a/b/c', 'value'); + const data = processor.getData(component, '/a/b/c'); + assert.strictEqual(data, 'value'); }); - it("should handle paths correctly", () => { - const path1 = processor.resolvePath("/a/b/c", "/value"); - const path2 = processor.resolvePath("a/b/c", "/value/"); - const path3 = processor.resolvePath("a/b/c", "/value"); + it('should handle paths correctly', () => { + const path1 = processor.resolvePath('/a/b/c', '/value'); + const path2 = processor.resolvePath('a/b/c', '/value/'); + const path3 = processor.resolvePath('a/b/c', '/value'); - assert.strictEqual(path1, "/a/b/c"); - assert.strictEqual(path2, "/value/a/b/c"); - assert.strictEqual(path3, "/value/a/b/c"); + assert.strictEqual(path1, '/a/b/c'); + assert.strictEqual(path2, '/value/a/b/c'); + assert.strictEqual(path3, '/value/a/b/c'); }); - it.skip("should correctly parse nested valueMap structures", () => { + it.skip('should correctly parse nested valueMap structures', () => { processor.processMessages([ { dataModelUpdate: { - surfaceId: "@default", - path: "/data", + surfaceId: '@default', + path: '/data', contents: [ { - key: "users", // /data/users + key: 'users', // /data/users valueMap: [ { - key: "user1", // /data/users/user1 + key: 'user1', // /data/users/user1 valueMap: [ { - key: "firstName", - valueString: "Alice", + key: 'firstName', + valueString: 'Alice', }, { - key: "lastName", - valueString: "Doe", + key: 'lastName', + valueString: 'Doe', }, ], }, { - key: "user2", // /data/users/user2 + key: 'user2', // /data/users/user2 valueMap: [ { - key: "firstName", - valueString: "John", + key: 'firstName', + valueString: 'John', }, { - key: "lastName", - valueString: "Doe", + key: 'lastName', + valueString: 'Doe', }, ], }, @@ -237,24 +230,24 @@ describe("A2uiMessageProcessor", () => { ]); const info = processor.getData( - { dataContextPath: "/" } as v0_8.Types.AnyComponentNode, - "/data/users" + {dataContextPath: '/'} as v0_8.Types.AnyComponentNode, + '/data/users', ); // The expected result is a Map of Maps. const expected = new Map([ [ - "user1", + 'user1', new Map([ - ["firstName", "Alice"], - ["lastName", "Doe"], + ['firstName', 'Alice'], + ['lastName', 'Doe'], ]), ], [ - "user2", + 'user2', new Map([ - ["firstName", "John"], - ["lastName", "Doe"], + ['firstName', 'John'], + ['lastName', 'Doe'], ]), ], ]); @@ -262,13 +255,13 @@ describe("A2uiMessageProcessor", () => { assert.deepEqual(info, expected); }); - it.skip("should additively update a Map using numeric-string keys (like timestamps)", () => { + it.skip('should additively update a Map using numeric-string keys (like timestamps)', () => { // 1. First, establish the /messages path as a Map. processor.processMessages([ { dataModelUpdate: { - surfaceId: "@default", - path: "/messages", + surfaceId: '@default', + path: '/messages', contents: [ // Sending an empty key-value array creates an empty Map at the path. ], @@ -276,18 +269,18 @@ describe("A2uiMessageProcessor", () => { }, ]); - const key1 = "1700000000001"; - const message1 = "Hello"; + const key1 = '1700000000001'; + const message1 = 'Hello'; // 2. Add the first message. processor.processMessages([ { dataModelUpdate: { - surfaceId: "@default", + surfaceId: '@default', path: `/messages/${key1}`, contents: [ { - key: ".", + key: '.', valueString: message1, }, ], @@ -296,8 +289,8 @@ describe("A2uiMessageProcessor", () => { ]); let messagesData = processor.getData( - { dataContextPath: "/" } as v0_8.Types.AnyComponentNode, - "/messages" + {dataContextPath: '/'} as v0_8.Types.AnyComponentNode, + '/messages', ); // Check that it's a Map and has the first item. @@ -305,18 +298,18 @@ describe("A2uiMessageProcessor", () => { assert.strictEqual((messagesData as Types.DataMap).size, 1); assert.strictEqual((messagesData as Types.DataMap).get(key1), message1); - const key2 = "1700000000002"; - const message2 = "World"; + const key2 = '1700000000002'; + const message2 = 'World'; // 3. Add the second message. This is where the old logic would fail. processor.processMessages([ { dataModelUpdate: { - surfaceId: "@default", + surfaceId: '@default', path: `/messages/${key2}`, contents: [ { - key: ".", + key: '.', valueString: message2, }, ], @@ -325,43 +318,39 @@ describe("A2uiMessageProcessor", () => { ]); messagesData = processor.getData( - { dataContextPath: "/" } as v0_8.Types.AnyComponentNode, - "/messages" + {dataContextPath: '/'} as v0_8.Types.AnyComponentNode, + '/messages', ); // 4. Check that the Map was additively updated and now has both items. assertIsDataMap(messagesData); - assert.strictEqual((messagesData as Types.DataMap).size, 2, "Map should have 2 items total"); - assert.strictEqual( - (messagesData as Types.DataMap).get(key1), - message1, - "First item correct" - ); + assert.strictEqual((messagesData as Types.DataMap).size, 2, 'Map should have 2 items total'); + assert.strictEqual((messagesData as Types.DataMap).get(key1), message1, 'First item correct'); assert.strictEqual( (messagesData as Types.DataMap).get(key2), message2, - "Second item correct" + 'Second item correct', ); }); }); - describe("Component Tree Building", () => { - it("should build a simple parent-child tree", () => { + describe('Component Tree Building', () => { + it('should build a simple parent-child tree', () => { processor.processMessages([ { surfaceUpdate: { - surfaceId: "@default", + surfaceId: '@default', components: [ { - id: "root", + id: 'root', component: { - Column: { children: { explicitList: ["child"] } }, + Column: {children: {explicitList: ['child']}}, }, }, { - id: "child", + id: 'child', component: { - Text: { usageHint: "body", text: { literalString: "Hello" } }, + Text: {usageHint: 'body', text: {literalString: 'Hello'}}, }, }, ], @@ -369,31 +358,31 @@ describe("A2uiMessageProcessor", () => { }, { beginRendering: { - root: "root", - surfaceId: "@default", + root: 'root', + surfaceId: '@default', }, }, ]); - const tree = processor.getSurfaces().get("@default")?.componentTree; + const tree = processor.getSurfaces().get('@default')?.componentTree; const plainTree = toPlainObject(tree); - assert.strictEqual(plainTree.id, "root"); - assert.strictEqual(plainTree.type, "Column"); + assert.strictEqual(plainTree.id, 'root'); + assert.strictEqual(plainTree.type, 'Column'); assert.strictEqual(plainTree.properties.children.length, 1); - assert.strictEqual(plainTree.properties.children[0].id, "child"); - assert.strictEqual(plainTree.properties.children[0].type, "Text"); + assert.strictEqual(plainTree.properties.children[0].id, 'child'); + assert.strictEqual(plainTree.properties.children[0].type, 'Text'); }); - it("should throw an error on circular dependencies", () => { + it('should throw an error on circular dependencies', () => { // First, load the components processor.processMessages([ { surfaceUpdate: { - surfaceId: "@default", + surfaceId: '@default', components: [ - { id: "a", component: { Card: { child: "b" } } }, - { id: "b", component: { Card: { child: "a" } } }, + {id: 'a', component: {Card: {child: 'b'}}}, + {id: 'b', component: {Card: {child: 'a'}}}, ], }, }, @@ -404,178 +393,174 @@ describe("A2uiMessageProcessor", () => { processor.processMessages([ { beginRendering: { - root: "a", - surfaceId: "@default", + root: 'a', + surfaceId: '@default', }, }, ]); }, new A2uiStateError(`Circular dependency for component "a".`)); - const tree = processor.getSurfaces().get("@default")?.componentTree; - assert.strictEqual( - tree, - null, - "Tree should be null due to circular dependency" - ); + const tree = processor.getSurfaces().get('@default')?.componentTree; + assert.strictEqual(tree, null, 'Tree should be null due to circular dependency'); }); - it("should correctly expand a template with `dataBinding`", () => { + it('should correctly expand a template with `dataBinding`', () => { processor.processMessages([ { dataModelUpdate: { - surfaceId: "@default", - path: "/", + surfaceId: '@default', + path: '/', contents: [ { - key: "items", - valueString: JSON.stringify([{ name: "A" }, { name: "B" }]), + key: 'items', + valueString: JSON.stringify([{name: 'A'}, {name: 'B'}]), }, ], }, }, { surfaceUpdate: { - surfaceId: "@default", + surfaceId: '@default', components: [ { - id: "root", + id: 'root', component: { List: { children: { template: { - componentId: "item-template", - dataBinding: "/items", + componentId: 'item-template', + dataBinding: '/items', }, }, }, }, }, { - id: "item-template", - component: { Text: { usageHint: "body", text: { path: "/name" } } }, + id: 'item-template', + component: {Text: {usageHint: 'body', text: {path: '/name'}}}, }, ], }, }, { beginRendering: { - root: "root", - surfaceId: "@default", + root: 'root', + surfaceId: '@default', }, }, ]); - const tree = processor.getSurfaces().get("@default")?.componentTree; + const tree = processor.getSurfaces().get('@default')?.componentTree; const plainTree = toPlainObject(tree); assert.strictEqual(plainTree.properties.children.length, 2); // Check first generated child. const child1 = plainTree.properties.children[0]; - assert.strictEqual(child1.id, "item-template:0"); - assert.strictEqual(child1.type, "Text"); - assert.strictEqual(child1.dataContextPath, "/items/0"); - assert.deepStrictEqual(child1.properties.text, { path: "name" }); + assert.strictEqual(child1.id, 'item-template:0'); + assert.strictEqual(child1.type, 'Text'); + assert.strictEqual(child1.dataContextPath, '/items/0'); + assert.deepStrictEqual(child1.properties.text, {path: 'name'}); // Check second generated child. const child2 = plainTree.properties.children[1]; - assert.strictEqual(child2.id, "item-template:1"); - assert.strictEqual(child2.type, "Text"); - assert.strictEqual(child2.dataContextPath, "/items/1"); - assert.deepStrictEqual(child2.properties.text, { path: "name" }); + assert.strictEqual(child2.id, 'item-template:1'); + assert.strictEqual(child2.type, 'Text'); + assert.strictEqual(child2.dataContextPath, '/items/1'); + assert.deepStrictEqual(child2.properties.text, {path: 'name'}); }); - it("should rebuild the tree when data for a template arrives later", () => { + it('should rebuild the tree when data for a template arrives later', () => { processor.processMessages([ { surfaceUpdate: { - surfaceId: "@default", + surfaceId: '@default', components: [ { - id: "root", + id: 'root', component: { List: { children: { template: { - componentId: "item-template", - dataBinding: "/items", + componentId: 'item-template', + dataBinding: '/items', }, }, }, }, }, { - id: "item-template", - component: { Text: { usageHint: "body", text: { path: "/name" } } }, + id: 'item-template', + component: {Text: {usageHint: 'body', text: {path: '/name'}}}, }, ], }, }, { beginRendering: { - root: "root", - surfaceId: "@default", + root: 'root', + surfaceId: '@default', }, }, ]); - let tree = processor.getSurfaces().get("@default")?.componentTree; + let tree = processor.getSurfaces().get('@default')?.componentTree; assert.strictEqual( toPlainObject(tree).properties.children.length, 0, - "Children should be empty before data arrives" + 'Children should be empty before data arrives', ); // Now, the data arrives. processor.processMessages([ { dataModelUpdate: { - surfaceId: "@default", - path: "/", + surfaceId: '@default', + path: '/', contents: [ { - key: "items", - valueString: JSON.stringify([{ name: "A" }, { name: "B" }]), + key: 'items', + valueString: JSON.stringify([{name: 'A'}, {name: 'B'}]), }, ], }, }, ]); - tree = processor.getSurfaces().get("@default")?.componentTree; + tree = processor.getSurfaces().get('@default')?.componentTree; assert.strictEqual( toPlainObject(tree).properties.children.length, 2, - "Children should be populated after data arrives" + 'Children should be populated after data arrives', ); }); - it("should trim relative paths within a data context (./item)", () => { + it('should trim relative paths within a data context (./item)', () => { processor.processMessages([ { dataModelUpdate: { - surfaceId: "@default", - path: "/", + surfaceId: '@default', + path: '/', contents: [ { - key: "items", - valueString: JSON.stringify([{ name: "A" }, { name: "B" }]), + key: 'items', + valueString: JSON.stringify([{name: 'A'}, {name: 'B'}]), }, ], }, }, { surfaceUpdate: { - surfaceId: "@default", + surfaceId: '@default', components: [ { - id: "root", + id: 'root', component: { List: { children: { template: { - componentId: "item-template", - dataBinding: "/items", + componentId: 'item-template', + dataBinding: '/items', }, }, }, @@ -583,57 +568,57 @@ describe("A2uiMessageProcessor", () => { }, // These paths would are typical when a databinding is used. { - id: "item-template", - component: { Text: { usageHint: "body", text: { path: "./item/name" } } }, + id: 'item-template', + component: {Text: {usageHint: 'body', text: {path: './item/name'}}}, }, ], }, }, { beginRendering: { - root: "root", - surfaceId: "@default", + root: 'root', + surfaceId: '@default', }, }, ]); - const tree = processor.getSurfaces().get("@default")?.componentTree; + const tree = processor.getSurfaces().get('@default')?.componentTree; const plainTree = toPlainObject(tree); const child1 = plainTree.properties.children[0]; const child2 = plainTree.properties.children[1]; // The processor should have trimmed `/item` and `./` from the path // because we are inside a data context. - assert.deepEqual(child1.properties.text, { path: "name" }); - assert.deepEqual(child2.properties.text, { path: "name" }); + assert.deepEqual(child1.properties.text, {path: 'name'}); + assert.deepEqual(child2.properties.text, {path: 'name'}); }); - it("should trim relative paths within a data context (./name)", () => { + it('should trim relative paths within a data context (./name)', () => { processor.processMessages([ { dataModelUpdate: { - surfaceId: "@default", - path: "/", + surfaceId: '@default', + path: '/', contents: [ { - key: "items", - valueString: JSON.stringify([{ name: "A" }, { name: "B" }]), + key: 'items', + valueString: JSON.stringify([{name: 'A'}, {name: 'B'}]), }, ], }, }, { surfaceUpdate: { - surfaceId: "@default", + surfaceId: '@default', components: [ { - id: "root", + id: 'root', component: { List: { children: { template: { - componentId: "item-template", - dataBinding: "/items", + componentId: 'item-template', + dataBinding: '/items', }, }, }, @@ -641,43 +626,43 @@ describe("A2uiMessageProcessor", () => { }, // These paths would are typical when a databinding is used. { - id: "item-template", - component: { Text: { usageHint: "body", text: { path: "./name" } } }, + id: 'item-template', + component: {Text: {usageHint: 'body', text: {path: './name'}}}, }, ], }, }, { beginRendering: { - root: "root", - surfaceId: "@default", + root: 'root', + surfaceId: '@default', }, }, ]); - const tree = processor.getSurfaces().get("@default")?.componentTree; + const tree = processor.getSurfaces().get('@default')?.componentTree; const plainTree = toPlainObject(tree); const child1 = plainTree.properties.children[0]; const child2 = plainTree.properties.children[1]; // The processor should have trimmed `./` from the path // because we are inside a data context. - assert.deepEqual(child1.properties.text, { path: "name" }); - assert.deepEqual(child2.properties.text, { path: "name" }); + assert.deepEqual(child1.properties.text, {path: 'name'}); + assert.deepEqual(child2.properties.text, {path: 'name'}); }); }); - describe("Data Normalization and Parsing", () => { - it("should correctly handle and parse the key-value array data format at the root", () => { + describe('Data Normalization and Parsing', () => { + it('should correctly handle and parse the key-value array data format at the root', () => { const messages = [ { dataModelUpdate: { - surfaceId: "test-surface", - path: "/", + surfaceId: 'test-surface', + path: '/', contents: [ - { key: "title", valueString: "My Title" }, + {key: 'title', valueString: 'My Title'}, { - key: "items", + key: 'items', valueString: '[{"id": 1}, {"id": 2}]', }, ], @@ -687,195 +672,195 @@ describe("A2uiMessageProcessor", () => { processor.processMessages(messages); - const component = { dataContextPath: "/" } as v0_8.Types.AnyComponentNode; - const title = processor.getData(component, "/title", "test-surface"); - const items = processor.getData(component, "/items", "test-surface"); + const component = {dataContextPath: '/'} as v0_8.Types.AnyComponentNode; + const title = processor.getData(component, '/title', 'test-surface'); + const items = processor.getData(component, '/items', 'test-surface'); - assert.strictEqual(title, "My Title"); - assert.deepStrictEqual(toPlainObject(items), [{ id: 1 }, { id: 2 }]); + assert.strictEqual(title, 'My Title'); + assert.deepStrictEqual(toPlainObject(items), [{id: 1}, {id: 2}]); }); - it("should fallback to a string if stringified JSON is invalid", () => { + it('should fallback to a string if stringified JSON is invalid', () => { const invalidJSON = '[{"id": 1}, {"id": 2}'; // Missing closing bracket processor.processMessages([ { dataModelUpdate: { - surfaceId: "@default", - path: "/", - contents: [{ key: "badData", valueString: invalidJSON }], + surfaceId: '@default', + path: '/', + contents: [{key: 'badData', valueString: invalidJSON}], }, }, ]); - const component = { dataContextPath: "/" } as v0_8.Types.AnyComponentNode; - const badData = processor.getData(component, "/badData"); + const component = {dataContextPath: '/'} as v0_8.Types.AnyComponentNode; + const badData = processor.getData(component, '/badData'); assert.strictEqual(badData, invalidJSON); }); }); - describe("Complex Template Scenarios", () => { - it.skip("should correctly expand a template with dataBinding to a Map (from valueMap)", () => { + describe('Complex Template Scenarios', () => { + it.skip('should correctly expand a template with dataBinding to a Map (from valueMap)', () => { const messages = [ { beginRendering: { - surfaceId: "default", - root: "root-column", + surfaceId: 'default', + root: 'root-column', }, }, { surfaceUpdate: { - surfaceId: "default", + surfaceId: 'default', components: [ { - id: "root-column", + id: 'root-column', component: { Column: { children: { - explicitList: ["title-heading", "item-list"], + explicitList: ['title-heading', 'item-list'], }, }, }, }, { - id: "title-heading", + id: 'title-heading', component: { Text: { - usageHint: "body", + usageHint: 'body', text: { - literalString: "Top Restaurants", + literalString: 'Top Restaurants', }, }, }, }, { - id: "item-list", + id: 'item-list', component: { List: { - direction: "vertical", + direction: 'vertical', children: { template: { - componentId: "item-card-template", - dataBinding: "/items", + componentId: 'item-card-template', + dataBinding: '/items', }, }, }, }, }, { - id: "item-card-template", + id: 'item-card-template', component: { Card: { - child: "card-layout", + child: 'card-layout', }, }, }, { - id: "card-layout", + id: 'card-layout', component: { Row: { children: { - explicitList: ["template-image", "card-details"], + explicitList: ['template-image', 'card-details'], }, }, }, }, { - id: "template-image", + id: 'template-image', weight: 1, component: { Image: { - usageHint: "largeFeature", + usageHint: 'largeFeature', url: { - path: "imageUrl", + path: 'imageUrl', }, }, }, }, { - id: "card-details", + id: 'card-details', weight: 2, component: { Column: { children: { explicitList: [ - "template-name", - "template-rating", - "template-detail", - "template-link", - "template-book-button", + 'template-name', + 'template-rating', + 'template-detail', + 'template-link', + 'template-book-button', ], }, }, }, }, { - id: "template-name", + id: 'template-name', component: { Text: { - usageHint: "body", + usageHint: 'body', text: { - path: "name", + path: 'name', }, }, }, }, { - id: "template-rating", + id: 'template-rating', component: { Text: { - usageHint: "body", + usageHint: 'body', text: { - path: "rating", + path: 'rating', }, }, }, }, { - id: "template-detail", + id: 'template-detail', component: { Text: { - usageHint: "body", + usageHint: 'body', text: { - path: "detail", + path: 'detail', }, }, }, }, { - id: "template-link", + id: 'template-link', component: { Text: { - usageHint: "body", + usageHint: 'body', text: { - path: "infoLink", + path: 'infoLink', }, }, }, }, { - id: "template-book-button", + id: 'template-book-button', component: { Button: { - child: "book-now-text", + child: 'book-now-text', action: { - name: "book_restaurant", + name: 'book_restaurant', context: [ { - key: "restaurantName", + key: 'restaurantName', value: { - path: "name", + path: 'name', }, }, { - key: "imageUrl", + key: 'imageUrl', value: { - path: "imageUrl", + path: 'imageUrl', }, }, { - key: "address", + key: 'address', value: { - path: "address", + path: 'address', }, }, ], @@ -884,12 +869,12 @@ describe("A2uiMessageProcessor", () => { }, }, { - id: "book-now-text", + id: 'book-now-text', component: { Text: { - usageHint: "body", + usageHint: 'body', text: { - literalString: "Book Now", + literalString: 'Book Now', }, }, }, @@ -899,160 +884,154 @@ describe("A2uiMessageProcessor", () => { }, { dataModelUpdate: { - surfaceId: "default", - path: "/", + surfaceId: 'default', + path: '/', contents: [ { - key: "items", + key: 'items', valueMap: [ { - key: "item1", + key: 'item1', valueMap: [ { - key: "name", - valueString: "Business 1", + key: 'name', + valueString: 'Business 1', }, { - key: "rating", - valueString: "★★★★☆", + key: 'rating', + valueString: '★★★★☆', }, { - key: "detail", - valueString: "Spicy and savory hand-pulled noodles.", + key: 'detail', + valueString: 'Spicy and savory hand-pulled noodles.', }, { - key: "infoLink", - valueString: "[More Info](https://www.example.com/)", + key: 'infoLink', + valueString: '[More Info](https://www.example.com/)', }, { - key: "imageUrl", - valueString: - "http://www.example.com/static/shrimpchowmein.jpeg", + key: 'imageUrl', + valueString: 'http://www.example.com/static/shrimpchowmein.jpeg', }, { - key: "address", - valueString: "Address 1", + key: 'address', + valueString: 'Address 1', }, ], }, { - key: "item2", + key: 'item2', valueMap: [ { - key: "name", - valueString: "Business 2", + key: 'name', + valueString: 'Business 2', }, { - key: "rating", - valueString: "★★★★☆", + key: 'rating', + valueString: '★★★★☆', }, { - key: "detail", - valueString: "Authentic and real.", + key: 'detail', + valueString: 'Authentic and real.', }, { - key: "infoLink", - valueString: "[More Info](https://www.example.com/)", + key: 'infoLink', + valueString: '[More Info](https://www.example.com/)', }, { - key: "imageUrl", - valueString: - "http://www.example.com/static/mapotofu.jpeg", + key: 'imageUrl', + valueString: 'http://www.example.com/static/mapotofu.jpeg', }, { - key: "address", - valueString: "Address 2", + key: 'address', + valueString: 'Address 2', }, ], }, { - key: "item3", + key: 'item3', valueMap: [ { - key: "name", - valueString: "Business 3", + key: 'name', + valueString: 'Business 3', }, { - key: "rating", - valueString: "★★★★☆", + key: 'rating', + valueString: '★★★★☆', }, { - key: "detail", - valueString: - "Modern food with a farm-to-table approach.", + key: 'detail', + valueString: 'Modern food with a farm-to-table approach.', }, { - key: "infoLink", - valueString: "[More Info](https://www.example.com/)", + key: 'infoLink', + valueString: '[More Info](https://www.example.com/)', }, { - key: "imageUrl", - valueString: - "http://www.example.com/static/beefbroccoli.jpeg", + key: 'imageUrl', + valueString: 'http://www.example.com/static/beefbroccoli.jpeg', }, { - key: "address", - valueString: "Address 3", + key: 'address', + valueString: 'Address 3', }, ], }, { - key: "item4", + key: 'item4', valueMap: [ { - key: "name", - valueString: "Business 4", + key: 'name', + valueString: 'Business 4', }, { - key: "rating", - valueString: "★★★★★", + key: 'rating', + valueString: '★★★★★', }, { - key: "detail", - valueString: "Upscale dining.", + key: 'detail', + valueString: 'Upscale dining.', }, { - key: "infoLink", - valueString: "[More Info](https://www.example.com/)", + key: 'infoLink', + valueString: '[More Info](https://www.example.com/)', }, { - key: "imageUrl", - valueString: - "http://www.example.com/static/springrolls.jpeg", + key: 'imageUrl', + valueString: 'http://www.example.com/static/springrolls.jpeg', }, { - key: "address", - valueString: "Address 4", + key: 'address', + valueString: 'Address 4', }, ], }, { - key: "item5", + key: 'item5', valueMap: [ { - key: "name", - valueString: "Business 5", + key: 'name', + valueString: 'Business 5', }, { - key: "rating", - valueString: "★★★★☆", + key: 'rating', + valueString: '★★★★☆', }, { - key: "detail", - valueString: "Famous for its noodles.", + key: 'detail', + valueString: 'Famous for its noodles.', }, { - key: "infoLink", - valueString: "[More Info](https://www.example.com/)", + key: 'infoLink', + valueString: '[More Info](https://www.example.com/)', }, { - key: "imageUrl", - valueString: - "http://www.example.com/static/kungpao.jpeg", + key: 'imageUrl', + valueString: 'http://www.example.com/static/kungpao.jpeg', }, { - key: "address", - valueString: "Address 5", + key: 'address', + valueString: 'Address 5', }, ], }, @@ -1064,12 +1043,12 @@ describe("A2uiMessageProcessor", () => { ]; processor.processMessages(messages); - const tree = processor.getSurfaces().get("default")?.componentTree; + const tree = processor.getSurfaces().get('default')?.componentTree; const plainTree = toPlainObject(tree); // 1. Find the "item-list" component (the List) const itemList = plainTree.properties.children[1]; - assert.strictEqual(itemList.id, "item-list"); + assert.strictEqual(itemList.id, 'item-list'); // 2. Check that it expanded 5 children from the Map const templateChildren = itemList.properties.children; @@ -1077,43 +1056,43 @@ describe("A2uiMessageProcessor", () => { // 3. Check the first generated child for correct key-based ID and data context const child1 = templateChildren[0]; - assert.strictEqual(child1.id, "item-card-template:item1"); - assert.strictEqual(child1.dataContextPath, "/items/item1"); + assert.strictEqual(child1.id, 'item-card-template:item1'); + assert.strictEqual(child1.dataContextPath, '/items/item1'); // 4. Go deeper to check the data binding on a nested component // Path: Card -> Row -> Column -> Heading const child1NameHeading = child1.properties.child.properties.children[1].properties.children[0]; - assert.strictEqual(child1NameHeading.id, "template-name:item1"); - assert.strictEqual(child1NameHeading.dataContextPath, "/items/item1"); + assert.strictEqual(child1NameHeading.id, 'template-name:item1'); + assert.strictEqual(child1NameHeading.dataContextPath, '/items/item1'); assert.deepStrictEqual(child1NameHeading.properties.text, { - path: "name", + path: 'name', }); // 5. Check the second generated child const child2 = templateChildren[1]; - assert.strictEqual(child2.id, "item-card-template:item2"); - assert.strictEqual(child2.dataContextPath, "/items/item2"); + assert.strictEqual(child2.id, 'item-card-template:item2'); + assert.strictEqual(child2.dataContextPath, '/items/item2'); }); - it("should correctly expand nested templates with layered data contexts", () => { + it('should correctly expand nested templates with layered data contexts', () => { const messages = [ { dataModelUpdate: { - surfaceId: "@default", - path: "/", + surfaceId: '@default', + path: '/', contents: [ { - key: "days", + key: 'days', // The correct way to send an array of objects is as a stringified JSON. valueString: JSON.stringify([ { - title: "Day 1", - activities: ["Morning Walk", "Museum Visit"], + title: 'Day 1', + activities: ['Morning Walk', 'Museum Visit'], }, { - title: "Day 2", - activities: ["Market Trip"], + title: 'Day 2', + activities: ['Market Trip'], }, ]), }, @@ -1122,227 +1101,219 @@ describe("A2uiMessageProcessor", () => { }, { surfaceUpdate: { - surfaceId: "@default", + surfaceId: '@default', components: [ { - id: "root", + id: 'root', component: { List: { children: { template: { - componentId: "day-list", - dataBinding: "/days", + componentId: 'day-list', + dataBinding: '/days', }, }, }, }, }, { - id: "day-list", + id: 'day-list', component: { Column: { - children: { explicitList: ["day-title", "activity-list"] }, + children: {explicitList: ['day-title', 'activity-list']}, }, }, }, { - id: "day-title", + id: 'day-title', component: { - Text: { usageHint: "body", text: { path: "title" } }, + Text: {usageHint: 'body', text: {path: 'title'}}, }, }, { - id: "activity-list", + id: 'activity-list', component: { List: { children: { template: { - componentId: "activity-text", - dataBinding: "activities", + componentId: 'activity-text', + dataBinding: 'activities', }, }, }, }, }, { - id: "activity-text", - component: { Text: { usageHint: "body", text: { path: "." } } }, + id: 'activity-text', + component: {Text: {usageHint: 'body', text: {path: '.'}}}, }, ], }, }, { beginRendering: { - root: "root", - surfaceId: "@default", + root: 'root', + surfaceId: '@default', }, }, ]; processor.processMessages(messages); - const tree = processor.getSurfaces().get("@default")?.componentTree; + const tree = processor.getSurfaces().get('@default')?.componentTree; const plainTree = toPlainObject(tree); // Assert Day 1 structure const day1 = plainTree.properties.children[0]; - assert.strictEqual(day1.dataContextPath, "/days/0"); + assert.strictEqual(day1.dataContextPath, '/days/0'); const day1Activities = day1.properties.children[1].properties.children; assert.strictEqual(day1Activities.length, 2); - assert.strictEqual(day1Activities[0].id, "activity-text:0:0"); - assert.strictEqual( - day1Activities[0].dataContextPath, - "/days/0/activities/0" - ); + assert.strictEqual(day1Activities[0].id, 'activity-text:0:0'); + assert.strictEqual(day1Activities[0].dataContextPath, '/days/0/activities/0'); assert.deepStrictEqual(day1.properties.children[0].properties.text, { - path: "title", + path: 'title', }); - assert.deepStrictEqual(day1Activities[0].properties.text, { path: "." }); + assert.deepStrictEqual(day1Activities[0].properties.text, {path: '.'}); // Assert Day 2 structure const day2 = plainTree.properties.children[1]; - assert.strictEqual(day2.dataContextPath, "/days/1"); + assert.strictEqual(day2.dataContextPath, '/days/1'); const day2Activities = day2.properties.children[1].properties.children; assert.strictEqual(day2Activities.length, 1); - assert.strictEqual(day2Activities[0].id, "activity-text:1:0"); - assert.strictEqual( - day2Activities[0].dataContextPath, - "/days/1/activities/0" - ); + assert.strictEqual(day2Activities[0].id, 'activity-text:1:0'); + assert.strictEqual(day2Activities[0].dataContextPath, '/days/1/activities/0'); assert.deepStrictEqual(day2.properties.children[0].properties.text, { - path: "title", + path: 'title', }); - assert.deepStrictEqual(day2Activities[0].properties.text, { path: "." }); + assert.deepStrictEqual(day2Activities[0].properties.text, {path: '.'}); }); it("should correctly bind to primitive values in an array using path: '.'", () => { processor.processMessages([ { dataModelUpdate: { - surfaceId: "@default", - path: "/", + surfaceId: '@default', + path: '/', contents: [ { - key: "tags", - valueString: JSON.stringify(["travel", "paris", "guide"]), + key: 'tags', + valueString: JSON.stringify(['travel', 'paris', 'guide']), }, ], }, }, { surfaceUpdate: { - surfaceId: "@default", + surfaceId: '@default', components: [ { - id: "root", + id: 'root', component: { Row: { children: { - template: { componentId: "tag", dataBinding: "/tags" }, + template: {componentId: 'tag', dataBinding: '/tags'}, }, }, }, }, - { id: "tag", component: { Text: { usageHint: "body", text: { path: "." } } } }, + {id: 'tag', component: {Text: {usageHint: 'body', text: {path: '.'}}}}, ], }, }, { beginRendering: { - root: "root", - surfaceId: "@default", + root: 'root', + surfaceId: '@default', }, }, ]); - const tree = processor.getSurfaces().get("@default")?.componentTree; + const tree = processor.getSurfaces().get('@default')?.componentTree; const plainTree = toPlainObject(tree); const children = plainTree.properties.children; assert.strictEqual(children.length, 3); - assert.strictEqual(children[0].dataContextPath, "/tags/0"); - assert.deepEqual(children[0].properties.text, { path: "." }); - assert.strictEqual(children[1].dataContextPath, "/tags/1"); - assert.deepEqual(children[1].properties.text, { path: "." }); + assert.strictEqual(children[0].dataContextPath, '/tags/0'); + assert.deepEqual(children[0].properties.text, {path: '.'}); + assert.strictEqual(children[1].dataContextPath, '/tags/1'); + assert.deepEqual(children[1].properties.text, {path: '.'}); }); }); - describe("Multi-Surface Interaction", () => { - it("should keep data and components for different surfaces separate", () => { + describe('Multi-Surface Interaction', () => { + it('should keep data and components for different surfaces separate', () => { processor.processMessages([ // Surface A { dataModelUpdate: { - surfaceId: "A", - path: "/", - contents: [{ key: "name", valueString: "Surface A Data" }], + surfaceId: 'A', + path: '/', + contents: [{key: 'name', valueString: 'Surface A Data'}], }, }, { surfaceUpdate: { - surfaceId: "A", + surfaceId: 'A', components: [ { - id: "comp-a", - component: { Text: { usageHint: "body", text: { path: "/name" } } }, + id: 'comp-a', + component: {Text: {usageHint: 'body', text: {path: '/name'}}}, }, ], }, }, - { beginRendering: { root: "comp-a", surfaceId: "A" } }, + {beginRendering: {root: 'comp-a', surfaceId: 'A'}}, // Surface B { dataModelUpdate: { - surfaceId: "B", - path: "/", - contents: [{ key: "name", valueString: "Surface B Data" }], + surfaceId: 'B', + path: '/', + contents: [{key: 'name', valueString: 'Surface B Data'}], }, }, { surfaceUpdate: { - surfaceId: "B", + surfaceId: 'B', components: [ { - id: "comp-b", - component: { Text: { usageHint: "body", text: { path: "/name" } } }, + id: 'comp-b', + component: {Text: {usageHint: 'body', text: {path: '/name'}}}, }, ], }, }, - { beginRendering: { root: "comp-b", surfaceId: "B" } }, + {beginRendering: {root: 'comp-b', surfaceId: 'B'}}, ]); const surfaces = processor.getSurfaces(); assert.strictEqual(surfaces.size, 2); - const surfaceA = surfaces.get("A"); - const surfaceB = surfaces.get("B"); + const surfaceA = surfaces.get('A'); + const surfaceB = surfaces.get('B'); - assert.ok(surfaceA && surfaceB, "Both surfaces should exist"); + assert.ok(surfaceA && surfaceB, 'Both surfaces should exist'); // Check Surface A - assert.ok(surfaceA, "Surface A exists."); + assert.ok(surfaceA, 'Surface A exists.'); assert.strictEqual(surfaceA!.components.size, 1); - assert.ok(surfaceA!.components.has("comp-a")); + assert.ok(surfaceA!.components.has('comp-a')); assert.deepStrictEqual(toPlainObject(surfaceA!.dataModel), { - name: "Surface A Data", + name: 'Surface A Data', + }); + assert.deepStrictEqual(toPlainObject(surfaceA!.componentTree).properties.text, { + path: '/name', }); - assert.deepStrictEqual( - toPlainObject(surfaceA!.componentTree).properties.text, - { path: "/name" } - ); // Check Surface B - assert.ok(surfaceB, "Surface B exists."); + assert.ok(surfaceB, 'Surface B exists.'); assert.strictEqual(surfaceB!.components.size, 1); - assert.ok(surfaceB!.components.has("comp-b")); + assert.ok(surfaceB!.components.has('comp-b')); assert.deepStrictEqual(toPlainObject(surfaceB!.dataModel), { - name: "Surface B Data", + name: 'Surface B Data', + }); + assert.deepStrictEqual(toPlainObject(surfaceB!.componentTree).properties.text, { + path: '/name', }); - assert.deepStrictEqual( - toPlainObject(surfaceB!.componentTree).properties.text, - { path: "/name" } - ); }); }); }); diff --git a/renderers/lit/src/0.8/ui/audio.ts b/renderers/lit/src/0.8/ui/audio.ts index a835a253c..93bea0738 100644 --- a/renderers/lit/src/0.8/ui/audio.ts +++ b/renderers/lit/src/0.8/ui/audio.ts @@ -14,16 +14,16 @@ * limitations under the License. */ -import { html, css, nothing } from "lit"; -import { customElement, property } from "lit/decorators.js"; -import { Root } from "./root.js"; -import { A2uiMessageProcessor } from "@a2ui/web_core/data/model-processor"; -import * as Primitives from "@a2ui/web_core/types/primitives"; -import { classMap } from "lit/directives/class-map.js"; -import { styleMap } from "lit/directives/style-map.js"; -import { structuralStyles } from "./styles.js"; +import {html, css, nothing} from 'lit'; +import {customElement, property} from 'lit/decorators.js'; +import {Root} from './root.js'; +import {A2uiMessageProcessor} from '@a2ui/web_core/data/model-processor'; +import * as Primitives from '@a2ui/web_core/types/primitives'; +import {classMap} from 'lit/directives/class-map.js'; +import {styleMap} from 'lit/directives/style-map.js'; +import {structuralStyles} from './styles.js'; -@customElement("a2ui-audioplayer") +@customElement('a2ui-audioplayer') export class Audio extends Root { @property() accessor url: Primitives.StringValue | null = null; @@ -54,12 +54,12 @@ export class Audio extends Root { return nothing; } - if (this.url && typeof this.url === "object") { - if ("literalString" in this.url) { + if (this.url && typeof this.url === 'object') { + if ('literalString' in this.url) { return html`
- ); - } -); +import {createComponentImplementation} from '@a2ui/react/v0_9'; + +export const MyProfile = createComponentImplementation(MyProfileApi, ({props, buildChild}) => { + // 'props' is strictly inferred from the Zod schema: + // props.username is 'string' (resolved from DynamicString) + // props.onEdit is '() => void' (resolved from Action) + + return ( +
+ {props.username} +

{props.username}

+

{props.bio}

+ + {props.isEditable && ( + + )} + + {/* Render validation errors if any check fails */} + {props.validationErrors?.map((err, i) => ( +
+ {err} +
+ ))} +
+ ); +}); ``` ## Generic Binder Features @@ -186,23 +188,20 @@ The Generic Binder is a framework-agnostic engine that transforms raw JSON paylo For advanced use cases where you need direct access to the `ComponentContext` or want to manage reactivity manually (e.g., for performance-critical animations), use `createBinderlessComponentImplementation`. ```tsx -import { createBinderlessComponentImplementation } from '@a2ui/react/v0_9'; - -export const RawInspector = createBinderlessComponentImplementation( - InspectorApi, - ({ context }) => { - // Access the raw, unresolved component model and the data model directly - const rawData = context.componentModel.properties; - const componentId = context.componentModel.id; - - return ( -
- Raw Component State (ID: {componentId}) -
{JSON.stringify(rawData, null, 2)}
-
- ); - } -); +import {createBinderlessComponentImplementation} from '@a2ui/react/v0_9'; + +export const RawInspector = createBinderlessComponentImplementation(InspectorApi, ({context}) => { + // Access the raw, unresolved component model and the data model directly + const rawData = context.componentModel.properties; + const componentId = context.componentModel.id; + + return ( +
+ Raw Component State (ID: {componentId}) +
{JSON.stringify(rawData, null, 2)}
+
+ ); +}); ``` ## Defining Catalogs and Functions @@ -210,24 +209,24 @@ export const RawInspector = createBinderlessComponentImplementation( Group your components and logic functions into a `Catalog` to be used by the `MessageProcessor`. ```typescript -import { Catalog, createFunctionImplementation } from '@a2ui/web_core/v0_9'; -import { z } from 'zod'; +import {Catalog, createFunctionImplementation} from '@a2ui/web_core/v0_9'; +import {z} from 'zod'; // 1. Implement a custom logic function const myCheckFunc = createFunctionImplementation( - { - name: 'is_admin', - returnType: 'boolean', - schema: z.object({ role: z.string() }) + { + name: 'is_admin', + returnType: 'boolean', + schema: z.object({role: z.string()}), }, - (args) => args.role === 'admin' + args => args.role === 'admin', ); // 2. Compose the catalog export const myCatalog = new Catalog( 'https://example.com/catalogs/v1.json', [MyProfile, RawInspector], // List of ReactComponentImplementation - [myCheckFunc] // List of FunctionImplementation + [myCheckFunc], // List of FunctionImplementation ); ``` @@ -247,7 +246,6 @@ Some components in this package (like `Text`) use **CSS Modules** for style enca You can also use CSS Modules for styling your custom components or extending the basic catalog: - ```css /* MyComponent.module.css */ .myComponent { @@ -264,12 +262,8 @@ You can also use CSS Modules for styling your custom components or extending the ```tsx import styles from './MyComponent.module.css'; -export const MyComponent = createComponentImplementation(MyComponentApi, ({ props }) => { - return ( -
- {/* ... */} -
- ); +export const MyComponent = createComponentImplementation(MyComponentApi, ({props}) => { + return
{/* ... */}
; }); ``` @@ -284,13 +278,13 @@ A2UI v0.8 is maintained for backward compatibility. While it remains the default To use the v0.8 renderer, you use the `A2UIProvider` and `A2UIRenderer` components: ```tsx -import { A2UIProvider, A2UIRenderer, injectStyles } from '@a2ui/react/v0_8'; +import {A2UIProvider, A2UIRenderer, injectStyles} from '@a2ui/react/v0_8'; // Inject v0.8 styles injectStyles(); function LegacyApp() { - const handleAction = (msg) => console.log('Action:', msg); + const handleAction = msg => console.log('Action:', msg); return ( @@ -303,21 +297,21 @@ function LegacyApp() { ### Key Differences (v0.8 vs v0.9) -| Feature | v0.8 (Legacy) | v0.9 (Current) | -| :--- | :--- | :--- | -| **Protocol** | Uses `BeginRendering`, `SurfaceUpdate`. | Uses `createSurface`, `updateComponents`, `updateDataModel`. | -| **Data Flow** | Unidirectional surface updates. | Bidirectional synchronization (`sendDataModel`). | -| **Type Safety** | Props accessed via `node.properties`. | Strongly typed `props` inferred from Zod schemas. | -| **Logic** | Limited client-side logic. | Extensible `Function` system (e.g., `formatString`, `required`). | -| **Reactivity** | Hooks-based (`useA2UIComponent`). | Automatic via the **Generic Binder** middleware. | -| **Binding** | Manual resolution in component body. | Declarative two-way binding with injected setters. | +| Feature | v0.8 (Legacy) | v0.9 (Current) | +| :-------------- | :-------------------------------------- | :--------------------------------------------------------------- | +| **Protocol** | Uses `BeginRendering`, `SurfaceUpdate`. | Uses `createSurface`, `updateComponents`, `updateDataModel`. | +| **Data Flow** | Unidirectional surface updates. | Bidirectional synchronization (`sendDataModel`). | +| **Type Safety** | Props accessed via `node.properties`. | Strongly typed `props` inferred from Zod schemas. | +| **Logic** | Limited client-side logic. | Extensible `Function` system (e.g., `formatString`, `required`). | +| **Reactivity** | Hooks-based (`useA2UIComponent`). | Automatic via the **Generic Binder** middleware. | +| **Binding** | Manual resolution in component body. | Declarative two-way binding with injected setters. | In v0.8, components are responsible for resolving their own dynamic values using hooks: ```tsx // v0.8 Manual Resolution -function TextField({ node, surfaceId }) { - const { resolveString, setValue } = useA2UIComponent(node, surfaceId); +function TextField({node, surfaceId}) { + const {resolveString, setValue} = useA2UIComponent(node, surfaceId); const label = resolveString(node.properties.label); // ... } @@ -333,4 +327,3 @@ In v0.9, this is handled by the **Generic Binder** before the component even ren All operational data received from an external agent—including its messages and UI definitions—should be handled as untrusted input. Malicious agents could attempt to spoof legitimate interfaces to deceive users (phishing), inject malicious scripts via property values (XSS), or generate excessive layout complexity to degrade client performance (DoS). If your application supports optional embedded content (such as iframes or web views), additional care must be taken to prevent exposure to malicious external sites. **Developer Responsibility**: Failure to properly validate data and strictly sandbox rendered content can introduce severe vulnerabilities. Developers are responsible for implementing appropriate security measures—such as input sanitization, Content Security Policies (CSP), and secure credential handling—to protect their systems and users. - diff --git a/renderers/react/a2ui_explorer/README.md b/renderers/react/a2ui_explorer/README.md index 2b04a3e15..489f7d479 100644 --- a/renderers/react/a2ui_explorer/README.md +++ b/renderers/react/a2ui_explorer/README.md @@ -5,6 +5,7 @@ This is the reference Gallery Application for the A2UI React renderer. It allows ## Prerequisites This application depends on the following local libraries in this repository: + 1. `@a2ui/web_core` (located in `renderers/web_core`) 2. `@a2ui/react` (located in `renderers/react`) @@ -21,7 +22,7 @@ npm install npm run build ``` -*Note: Ensure `@a2ui/web_core` is also built if you have made changes to the core logic.* +_Note: Ensure `@a2ui/web_core` is also built if you have made changes to the core logic._ ## Setup and Development diff --git a/renderers/react/a2ui_explorer/eslint.config.js b/renderers/react/a2ui_explorer/eslint.config.js index dcc74559c..c9d1e1bc1 100644 --- a/renderers/react/a2ui_explorer/eslint.config.js +++ b/renderers/react/a2ui_explorer/eslint.config.js @@ -26,15 +26,17 @@ export default tseslint.config( ...gts.map(config => ({ ...config, // Override the project for a2ui_explorer since it has its own tsconfig - ...(config.languageOptions?.parserOptions?.project ? { - languageOptions: { - ...config.languageOptions, - parserOptions: { - ...config.languageOptions.parserOptions, - project: ['./tsconfig.app.json', './tsconfig.node.json'] + ...(config.languageOptions?.parserOptions?.project + ? { + languageOptions: { + ...config.languageOptions, + parserOptions: { + ...config.languageOptions.parserOptions, + project: ['./tsconfig.app.json', './tsconfig.node.json'], + }, + }, } - } - } : {}) + : {}), })), { @@ -59,10 +61,7 @@ export default tseslint.config( ...reactHooksPlugin.configs.recommended.rules, // React Refresh rules - 'react-refresh/only-export-components': [ - 'warn', - { allowConstantExport: true }, - ], + 'react-refresh/only-export-components': ['warn', {allowConstantExport: true}], // TypeScript rules '@typescript-eslint/no-unused-vars': [ @@ -82,7 +81,7 @@ export default tseslint.config( ], // General rules - 'no-console': ['warn', { allow: ['warn', 'error'] }], + 'no-console': ['warn', {allow: ['warn', 'error']}], 'prefer-arrow-callback': 'off', }, }, @@ -101,5 +100,5 @@ export default tseslint.config( { ignores: ['dist/**', 'node_modules/**', '**/*.d.ts'], - } + }, ); diff --git a/renderers/react/a2ui_explorer/index.html b/renderers/react/a2ui_explorer/index.html index 77fa01208..f1f2264ec 100644 --- a/renderers/react/a2ui_explorer/index.html +++ b/renderers/react/a2ui_explorer/index.html @@ -20,7 +20,10 @@ - + react diff --git a/renderers/react/a2ui_explorer/src/App-smoke.test.tsx b/renderers/react/a2ui_explorer/src/App-smoke.test.tsx index 64588e129..c22cd0a4c 100644 --- a/renderers/react/a2ui_explorer/src/App-smoke.test.tsx +++ b/renderers/react/a2ui_explorer/src/App-smoke.test.tsx @@ -14,8 +14,8 @@ * limitations under the License. */ -import { render, screen } from '@testing-library/react'; -import { describe, it, expect } from 'vitest'; +import {render, screen} from '@testing-library/react'; +import {describe, it, expect} from 'vitest'; import App from './App'; describe('App Smoke Test', () => { diff --git a/renderers/react/a2ui_explorer/src/App.tsx b/renderers/react/a2ui_explorer/src/App.tsx index b6f972746..a30816157 100644 --- a/renderers/react/a2ui_explorer/src/App.tsx +++ b/renderers/react/a2ui_explorer/src/App.tsx @@ -16,7 +16,12 @@ import {useState, useEffect, useSyncExternalStore, useCallback} from 'react'; import {MessageProcessor, SurfaceModel} from '@a2ui/web_core/v0_9'; -import {basicCatalog, A2uiSurface, MarkdownContext, type ReactComponentImplementation} from '@a2ui/react/v0_9'; +import { + basicCatalog, + A2uiSurface, + MarkdownContext, + type ReactComponentImplementation, +} from '@a2ui/react/v0_9'; import {exampleFiles, getMessages} from './examples'; import {renderMarkdown} from '@a2ui/markdown-it'; import styles from './App.module.css'; @@ -27,7 +32,7 @@ const DataModelViewer = ({surface}: {surface: SurfaceModel}) => { const bound = surface.dataModel.subscribe('/', callback); return () => bound.unsubscribe(); }, - [surface] + [surface], ); const getSnapshot = useCallback(() => { @@ -46,23 +51,28 @@ const DataModelViewer = ({surface}: {surface: SurfaceModel}) => { export default function App() { const [selectedExampleKey, setSelectedExampleKey] = useState(exampleFiles[0].key); - const selectedExample = exampleFiles.find((e) => e.key === selectedExampleKey)?.data as any; + const selectedExample = exampleFiles.find(e => e.key === selectedExampleKey)?.data as any; const [logs, setLogs] = useState([]); - const [processor, setProcessor] = useState | null>(null); + const [processor, setProcessor] = useState | null>( + null, + ); const [surfaces, setSurfaces] = useState([]); const [currentMessageIndex, setCurrentMessageIndex] = useState(-1); // Initialize or reset processor const resetProcessor = useCallback( (advanceToEnd: boolean = false) => { - setProcessor((prevProcessor) => { + setProcessor(prevProcessor => { if (prevProcessor) { prevProcessor.model.dispose(); } - const newProcessor = new MessageProcessor([basicCatalog], async (action: any) => { - setLogs((l) => [...l, {time: new Date().toISOString(), action}]); - }); + const newProcessor = new MessageProcessor( + [basicCatalog], + async (action: any) => { + setLogs(l => [...l, {time: new Date().toISOString(), action}]); + }, + ); const msgs = getMessages(selectedExample); if (advanceToEnd && msgs) { @@ -81,7 +91,7 @@ export default function App() { setCurrentMessageIndex(-1); } }, - [selectedExample] + [selectedExample], ); // Effect to handle example selection change @@ -89,7 +99,7 @@ export default function App() { resetProcessor(true); // Cleanup on unmount or when changing examples return () => { - setProcessor((prev) => { + setProcessor(prev => { if (prev) prev.model.dispose(); return null; }); @@ -144,11 +154,10 @@ export default function App() {

Preview and interact with React components

- Message {currentMessageIndex + 1} of {messages.length} -
@@ -157,7 +166,7 @@ export default function App() {
{/* Left Column: Sample List */}
- {exampleFiles.map((ex) => { + {exampleFiles.map(ex => { const isActive = selectedExampleKey === ex.key; return ( - ); + return ; } - const { rerender } = render( + const {rerender} = render( - + , ); rerender( - + , ); const ref0 = getElement(actionRefs, 0); diff --git a/renderers/react/tests/v0_8/integration/messages.test.tsx b/renderers/react/tests/v0_8/integration/messages.test.tsx index 0cb5dbfdd..df97f9b78 100644 --- a/renderers/react/tests/v0_8/integration/messages.test.tsx +++ b/renderers/react/tests/v0_8/integration/messages.test.tsx @@ -14,10 +14,10 @@ * limitations under the License. */ -import { describe, it, expect } from 'vitest'; -import { render, screen, waitFor } from '@testing-library/react'; -import React, { useEffect } from 'react'; -import { A2UIProvider, A2UIRenderer, useA2UI } from '../../../src/v0_8'; +import {describe, it, expect} from 'vitest'; +import {render, screen, waitFor} from '@testing-library/react'; +import React, {useEffect} from 'react'; +import {A2UIProvider, A2UIRenderer, useA2UI} from '../../../src/v0_8'; import type * as Types from '@a2ui/web_core/types/types'; import { TestWrapper, @@ -38,7 +38,7 @@ describe('Message Processing', () => { describe('Basic Processing', () => { it('should not render surface until beginRendering is received', () => { function StagedRenderer() { - const { processMessages } = useA2UI(); + const {processMessages} = useA2UI(); const [stage, setStage] = React.useState<'initial' | 'updated' | 'rendering'>('initial'); useEffect(() => { @@ -46,7 +46,12 @@ describe('Message Processing', () => { // Step 1: Only send surfaceUpdate (no beginRendering) processMessages([ createSurfaceUpdate([ - { id: 'text-1', component: { Text: { text: { literalString: 'Should not appear yet' } , usageHint: 'body' } } }, + { + id: 'text-1', + component: { + Text: {text: {literalString: 'Should not appear yet'}, usageHint: 'body'}, + }, + }, ]), ]); setStage('updated'); @@ -70,7 +75,7 @@ describe('Message Processing', () => { render( - + , ); // After surfaceUpdate only, content should NOT be visible @@ -87,7 +92,10 @@ describe('Message Processing', () => { it('should process surfaceUpdate and beginRendering messages', () => { const messages: Types.ServerToClientMessage[] = [ createSurfaceUpdate([ - { id: 'text-1', component: { Text: { text: { literalString: 'Hello World' } , usageHint: 'body' } } }, + { + id: 'text-1', + component: {Text: {text: {literalString: 'Hello World'}, usageHint: 'body'}}, + }, ]), createBeginRendering('text-1'), ]; @@ -95,7 +103,7 @@ describe('Message Processing', () => { render( - + , ); expect(screen.getByText('Hello World')).toBeInTheDocument(); @@ -103,19 +111,25 @@ describe('Message Processing', () => { it('should process multiple messages in sequence', () => { function SequentialRenderer() { - const { processMessages } = useA2UI(); + const {processMessages} = useA2UI(); useEffect(() => { processMessages([ createSurfaceUpdate([ - { id: 'text-1', component: { Text: { text: { literalString: 'Initial' } , usageHint: 'body' } } }, + { + id: 'text-1', + component: {Text: {text: {literalString: 'Initial'}, usageHint: 'body'}}, + }, ]), createBeginRendering('text-1'), ]); processMessages([ createSurfaceUpdate([ - { id: 'text-1', component: { Text: { text: { literalString: 'Updated' } , usageHint: 'body' } } }, + { + id: 'text-1', + component: {Text: {text: {literalString: 'Updated'}, usageHint: 'body'}}, + }, ]), createBeginRendering('text-1'), ]); @@ -127,7 +141,7 @@ describe('Message Processing', () => { render( - + , ); expect(screen.getByText('Updated')).toBeInTheDocument(); @@ -136,13 +150,16 @@ describe('Message Processing', () => { it('should handle empty message arrays gracefully', () => { function EmptyMessagesRenderer() { - const { processMessages } = useA2UI(); + const {processMessages} = useA2UI(); useEffect(() => { processMessages([]); processMessages([ createSurfaceUpdate([ - { id: 'text-1', component: { Text: { text: { literalString: 'After empty' } , usageHint: 'body' } } }, + { + id: 'text-1', + component: {Text: {text: {literalString: 'After empty'}, usageHint: 'body'}}, + }, ]), createBeginRendering('text-1'), ]); @@ -154,7 +171,7 @@ describe('Message Processing', () => { render( - + , ); expect(screen.getByText('After empty')).toBeInTheDocument(); @@ -164,18 +181,32 @@ describe('Message Processing', () => { describe('Multiple Surfaces', () => { it('should render different content on different surfaces', () => { function MultiSurfaceRenderer() { - const { processMessages } = useA2UI(); + const {processMessages} = useA2UI(); useEffect(() => { processMessages([ createSurfaceUpdate( - [{ id: 'text-a', component: { Text: { text: { literalString: 'Surface A Content' } , usageHint: 'body' } } }], - 'surface-a' + [ + { + id: 'text-a', + component: { + Text: {text: {literalString: 'Surface A Content'}, usageHint: 'body'}, + }, + }, + ], + 'surface-a', ), createBeginRendering('text-a', 'surface-a'), createSurfaceUpdate( - [{ id: 'text-b', component: { Text: { text: { literalString: 'Surface B Content' } , usageHint: 'body' } } }], - 'surface-b' + [ + { + id: 'text-b', + component: { + Text: {text: {literalString: 'Surface B Content'}, usageHint: 'body'}, + }, + }, + ], + 'surface-b', ), createBeginRendering('text-b', 'surface-b'), ]); @@ -196,7 +227,7 @@ describe('Message Processing', () => { render( - + , ); expect(screen.getByText('Surface A Content')).toBeInTheDocument(); @@ -210,20 +241,34 @@ describe('Message Processing', () => { it('should update surfaces independently', () => { function IndependentSurfaceRenderer() { - const { processMessages } = useA2UI(); + const {processMessages} = useA2UI(); const [step, setStep] = React.useState(0); useEffect(() => { if (step === 0) { processMessages([ createSurfaceUpdate( - [{ id: 'text-a', component: { Text: { text: { literalString: 'A: Initial' } , usageHint: 'body' } } }], - 'surface-a' + [ + { + id: 'text-a', + component: { + Text: {text: {literalString: 'A: Initial'}, usageHint: 'body'}, + }, + }, + ], + 'surface-a', ), createBeginRendering('text-a', 'surface-a'), createSurfaceUpdate( - [{ id: 'text-b', component: { Text: { text: { literalString: 'B: Initial' } , usageHint: 'body' } } }], - 'surface-b' + [ + { + id: 'text-b', + component: { + Text: {text: {literalString: 'B: Initial'}, usageHint: 'body'}, + }, + }, + ], + 'surface-b', ), createBeginRendering('text-b', 'surface-b'), ]); @@ -231,8 +276,15 @@ describe('Message Processing', () => { } else if (step === 1) { processMessages([ createSurfaceUpdate( - [{ id: 'text-a', component: { Text: { text: { literalString: 'A: Updated' } , usageHint: 'body' } } }], - 'surface-a' + [ + { + id: 'text-a', + component: { + Text: {text: {literalString: 'A: Updated'}, usageHint: 'body'}, + }, + }, + ], + 'surface-a', ), createBeginRendering('text-a', 'surface-a'), ]); @@ -250,7 +302,7 @@ describe('Message Processing', () => { render( - + , ); expect(screen.getByText('A: Updated')).toBeInTheDocument(); @@ -261,7 +313,7 @@ describe('Message Processing', () => { render( - + , ); // Should render without error but with no content }); @@ -270,14 +322,21 @@ describe('Message Processing', () => { describe('Delete Surface', () => { it('should remove surface content when deleteSurface is received', async () => { function DeleteSurfaceRenderer() { - const { processMessages } = useA2UI(); + const {processMessages} = useA2UI(); const [deleted, setDeleted] = React.useState(false); useEffect(() => { processMessages([ createSurfaceUpdate( - [{ id: 'text-1', component: { Text: { text: { literalString: 'Surface content' } , usageHint: 'body' } } }], - 'deletable-surface' + [ + { + id: 'text-1', + component: { + Text: {text: {literalString: 'Surface content'}, usageHint: 'body'}, + }, + }, + ], + 'deletable-surface', ), createBeginRendering('text-1', 'deletable-surface'), ]); @@ -299,7 +358,7 @@ describe('Message Processing', () => { render( - + , ); expect(screen.getByText('Surface content')).toBeInTheDocument(); @@ -312,7 +371,7 @@ describe('Message Processing', () => { it('should handle deleting a non-existent surface gracefully', () => { function DeleteNonExistentRenderer() { - const { processMessages } = useA2UI(); + const {processMessages} = useA2UI(); const [attempted, setAttempted] = React.useState(false); useEffect(() => { @@ -328,7 +387,7 @@ describe('Message Processing', () => { render( - + , ); expect(screen.getByTestId('status')).toHaveTextContent('completed'); @@ -336,20 +395,34 @@ describe('Message Processing', () => { it('should only delete the specified surface, leaving others intact', async () => { function MultiSurfaceDeleteRenderer() { - const { processMessages } = useA2UI(); + const {processMessages} = useA2UI(); const [deleted, setDeleted] = React.useState(false); useEffect(() => { // Create two surfaces processMessages([ createSurfaceUpdate( - [{ id: 'text-a', component: { Text: { text: { literalString: 'Surface A content' } , usageHint: 'body' } } }], - 'surface-a' + [ + { + id: 'text-a', + component: { + Text: {text: {literalString: 'Surface A content'}, usageHint: 'body'}, + }, + }, + ], + 'surface-a', ), createBeginRendering('text-a', 'surface-a'), createSurfaceUpdate( - [{ id: 'text-b', component: { Text: { text: { literalString: 'Surface B content' } , usageHint: 'body' } } }], - 'surface-b' + [ + { + id: 'text-b', + component: { + Text: {text: {literalString: 'Surface B content'}, usageHint: 'body'}, + }, + }, + ], + 'surface-b', ), createBeginRendering('text-b', 'surface-b'), ]); @@ -373,7 +446,7 @@ describe('Message Processing', () => { render( - + , ); // Both surfaces should be visible initially @@ -390,7 +463,7 @@ describe('Message Processing', () => { it('should allow re-creating a surface after deletion with the same ID', async () => { function RecreateAfterDeleteRenderer() { - const { processMessages } = useA2UI(); + const {processMessages} = useA2UI(); const [stage, setStage] = React.useState<'initial' | 'deleted' | 'recreated'>('initial'); useEffect(() => { @@ -398,8 +471,15 @@ describe('Message Processing', () => { // Create surface processMessages([ createSurfaceUpdate( - [{ id: 'text-1', component: { Text: { text: { literalString: 'Original content' } , usageHint: 'body' } } }], - 'recyclable-surface' + [ + { + id: 'text-1', + component: { + Text: {text: {literalString: 'Original content'}, usageHint: 'body'}, + }, + }, + ], + 'recyclable-surface', ), createBeginRendering('text-1', 'recyclable-surface'), ]); @@ -412,8 +492,18 @@ describe('Message Processing', () => { // Re-create surface with same ID but different content processMessages([ createSurfaceUpdate( - [{ id: 'text-2', component: { Text: { text: { literalString: 'New content after recreation' } , usageHint: 'body' } } }], - 'recyclable-surface' + [ + { + id: 'text-2', + component: { + Text: { + text: {literalString: 'New content after recreation'}, + usageHint: 'body', + }, + }, + }, + ], + 'recyclable-surface', ), createBeginRendering('text-2', 'recyclable-surface'), ]); @@ -431,7 +521,7 @@ describe('Message Processing', () => { render( - + , ); // Initial content should be visible @@ -449,13 +539,18 @@ describe('Message Processing', () => { describe('Clear Surfaces', () => { it('should clear all surfaces when clearSurfaces is called', () => { function ClearRenderer() { - const { processMessages, clearSurfaces } = useA2UI(); + const {processMessages, clearSurfaces} = useA2UI(); const [cleared, setCleared] = React.useState(false); useEffect(() => { processMessages([ createSurfaceUpdate([ - { id: 'text-1', component: { Text: { text: { literalString: 'Will be cleared' } , usageHint: 'body' } } }, + { + id: 'text-1', + component: { + Text: {text: {literalString: 'Will be cleared'}, usageHint: 'body'}, + }, + }, ]), createBeginRendering('text-1'), ]); @@ -477,7 +572,7 @@ describe('Message Processing', () => { render( - + , ); expect(screen.getByText('Will be cleared')).toBeInTheDocument(); @@ -490,14 +585,17 @@ describe('Message Processing', () => { it('should allow new content after clearing', () => { function ClearAndRefillRenderer() { - const { processMessages, clearSurfaces } = useA2UI(); + const {processMessages, clearSurfaces} = useA2UI(); const [step, setStep] = React.useState(0); useEffect(() => { if (step === 0) { processMessages([ createSurfaceUpdate([ - { id: 'text-1', component: { Text: { text: { literalString: 'Original' } , usageHint: 'body' } } }, + { + id: 'text-1', + component: {Text: {text: {literalString: 'Original'}, usageHint: 'body'}}, + }, ]), createBeginRendering('text-1'), ]); @@ -508,7 +606,12 @@ describe('Message Processing', () => { } else if (step === 2) { processMessages([ createSurfaceUpdate([ - { id: 'text-2', component: { Text: { text: { literalString: 'New Content' } , usageHint: 'body' } } }, + { + id: 'text-2', + component: { + Text: {text: {literalString: 'New Content'}, usageHint: 'body'}, + }, + }, ]), createBeginRendering('text-2'), ]); @@ -521,7 +624,7 @@ describe('Message Processing', () => { render( - + , ); return waitFor(() => { diff --git a/renderers/react/tests/v0_8/integration/property-updates.test.tsx b/renderers/react/tests/v0_8/integration/property-updates.test.tsx index 27b33ac59..25ab8a40e 100644 --- a/renderers/react/tests/v0_8/integration/property-updates.test.tsx +++ b/renderers/react/tests/v0_8/integration/property-updates.test.tsx @@ -14,11 +14,11 @@ * limitations under the License. */ -import { describe, it, expect } from 'vitest'; -import { render, screen, waitFor } from '@testing-library/react'; -import React, { useEffect } from 'react'; -import { A2UIProvider, A2UIRenderer, useA2UI } from '../../../src/v0_8'; -import { createSurfaceUpdate, createBeginRendering } from '../utils'; +import {describe, it, expect} from 'vitest'; +import {render, screen, waitFor} from '@testing-library/react'; +import React, {useEffect} from 'react'; +import {A2UIProvider, A2UIRenderer, useA2UI} from '../../../src/v0_8'; +import {createSurfaceUpdate, createBeginRendering} from '../utils'; /** * Property Updates Integration Tests @@ -32,14 +32,19 @@ describe('Property Updates via surfaceUpdate', () => { describe('Content Components', () => { it('should update Text content', async () => { function TextUpdateRenderer() { - const { processMessages } = useA2UI(); + const {processMessages} = useA2UI(); const [stage, setStage] = React.useState<'initial' | 'updated'>('initial'); useEffect(() => { if (stage === 'initial') { processMessages([ createSurfaceUpdate([ - { id: 'text-1', component: { Text: { text: { literalString: 'Original text' } , usageHint: 'body' } } }, + { + id: 'text-1', + component: { + Text: {text: {literalString: 'Original text'}, usageHint: 'body'}, + }, + }, ]), createBeginRendering('text-1'), ]); @@ -47,7 +52,12 @@ describe('Property Updates via surfaceUpdate', () => { } else if (stage === 'updated') { processMessages([ createSurfaceUpdate([ - { id: 'text-1', component: { Text: { text: { literalString: 'Updated text' } , usageHint: 'body' } } }, + { + id: 'text-1', + component: { + Text: {text: {literalString: 'Updated text'}, usageHint: 'body'}, + }, + }, ]), ]); } @@ -64,7 +74,7 @@ describe('Property Updates via surfaceUpdate', () => { render( - + , ); expect(screen.getByText('Original text')).toBeInTheDocument(); @@ -78,14 +88,17 @@ describe('Property Updates via surfaceUpdate', () => { it('should update Text usageHint', async () => { function TextUsageRenderer() { - const { processMessages } = useA2UI(); + const {processMessages} = useA2UI(); const [stage, setStage] = React.useState<'initial' | 'updated'>('initial'); useEffect(() => { if (stage === 'initial') { processMessages([ createSurfaceUpdate([ - { id: 'text-1', component: { Text: { text: { literalString: 'Heading' }, usageHint: 'h1' } } }, + { + id: 'text-1', + component: {Text: {text: {literalString: 'Heading'}, usageHint: 'h1'}}, + }, ]), createBeginRendering('text-1'), ]); @@ -93,7 +106,10 @@ describe('Property Updates via surfaceUpdate', () => { } else if (stage === 'updated') { processMessages([ createSurfaceUpdate([ - { id: 'text-1', component: { Text: { text: { literalString: 'Heading' }, usageHint: 'caption' } } }, + { + id: 'text-1', + component: {Text: {text: {literalString: 'Heading'}, usageHint: 'caption'}}, + }, ]), ]); } @@ -107,10 +123,10 @@ describe('Property Updates via surfaceUpdate', () => { ); } - const { container } = render( + const {container} = render( - + , ); // usageHint affects CSS classes, not the element tag @@ -126,14 +142,22 @@ describe('Property Updates via surfaceUpdate', () => { it('should update Image url', async () => { function ImageUpdateRenderer() { - const { processMessages } = useA2UI(); + const {processMessages} = useA2UI(); const [stage, setStage] = React.useState<'initial' | 'updated'>('initial'); useEffect(() => { if (stage === 'initial') { processMessages([ createSurfaceUpdate([ - { id: 'img-1', component: { Image: { url: { literalString: 'https://example.com/old.jpg' }, usageHint: 'mediumFeature' } } }, + { + id: 'img-1', + component: { + Image: { + url: {literalString: 'https://example.com/old.jpg'}, + usageHint: 'mediumFeature', + }, + }, + }, ]), createBeginRendering('img-1'), ]); @@ -141,7 +165,15 @@ describe('Property Updates via surfaceUpdate', () => { } else if (stage === 'updated') { processMessages([ createSurfaceUpdate([ - { id: 'img-1', component: { Image: { url: { literalString: 'https://example.com/new.jpg' }, usageHint: 'mediumFeature' } } }, + { + id: 'img-1', + component: { + Image: { + url: {literalString: 'https://example.com/new.jpg'}, + usageHint: 'mediumFeature', + }, + }, + }, ]), ]); } @@ -155,30 +187,41 @@ describe('Property Updates via surfaceUpdate', () => { ); } - const { container } = render( + const {container} = render( - + , ); expect(container.querySelector('img')).toHaveAttribute('src', 'https://example.com/old.jpg'); await waitFor(() => { expect(screen.getByTestId('stage')).toHaveTextContent('updated'); - expect(container.querySelector('img')).toHaveAttribute('src', 'https://example.com/new.jpg'); + expect(container.querySelector('img')).toHaveAttribute( + 'src', + 'https://example.com/new.jpg', + ); }); }); it('should update Image usageHint', async () => { function ImageUsageRenderer() { - const { processMessages } = useA2UI(); + const {processMessages} = useA2UI(); const [stage, setStage] = React.useState<'initial' | 'updated'>('initial'); useEffect(() => { if (stage === 'initial') { processMessages([ createSurfaceUpdate([ - { id: 'img-1', component: { Image: { url: { literalString: 'https://example.com/img.jpg' }, usageHint: 'icon' } } }, + { + id: 'img-1', + component: { + Image: { + url: {literalString: 'https://example.com/img.jpg'}, + usageHint: 'icon', + }, + }, + }, ]), createBeginRendering('img-1'), ]); @@ -186,7 +229,15 @@ describe('Property Updates via surfaceUpdate', () => { } else if (stage === 'updated') { processMessages([ createSurfaceUpdate([ - { id: 'img-1', component: { Image: { url: { literalString: 'https://example.com/img.jpg' }, usageHint: 'avatar' } } }, + { + id: 'img-1', + component: { + Image: { + url: {literalString: 'https://example.com/img.jpg'}, + usageHint: 'avatar', + }, + }, + }, ]), ]); } @@ -200,10 +251,10 @@ describe('Property Updates via surfaceUpdate', () => { ); } - const { container } = render( + const {container} = render( - + , ); // usageHint affects CSS classes via theme, not a data attribute @@ -219,14 +270,14 @@ describe('Property Updates via surfaceUpdate', () => { it('should update Icon name', async () => { function IconUpdateRenderer() { - const { processMessages } = useA2UI(); + const {processMessages} = useA2UI(); const [stage, setStage] = React.useState<'initial' | 'updated'>('initial'); useEffect(() => { if (stage === 'initial') { processMessages([ createSurfaceUpdate([ - { id: 'icon-1', component: { Icon: { name: { literalString: 'home' } } } }, + {id: 'icon-1', component: {Icon: {name: {literalString: 'home'}}}}, ]), createBeginRendering('icon-1'), ]); @@ -234,7 +285,7 @@ describe('Property Updates via surfaceUpdate', () => { } else if (stage === 'updated') { processMessages([ createSurfaceUpdate([ - { id: 'icon-1', component: { Icon: { name: { literalString: 'settings' } } } }, + {id: 'icon-1', component: {Icon: {name: {literalString: 'settings'}}}}, ]), ]); } @@ -248,10 +299,10 @@ describe('Property Updates via surfaceUpdate', () => { ); } - const { container } = render( + const {container} = render( - + , ); expect(container.querySelector('.g-icon')).toHaveTextContent('home'); @@ -266,15 +317,21 @@ describe('Property Updates via surfaceUpdate', () => { describe('Interactive Components', () => { it('should update Button child text', async () => { function ButtonUpdateRenderer() { - const { processMessages } = useA2UI(); + const {processMessages} = useA2UI(); const [stage, setStage] = React.useState<'initial' | 'updated'>('initial'); useEffect(() => { if (stage === 'initial') { processMessages([ createSurfaceUpdate([ - { id: 'btn-text', component: { Text: { text: { literalString: 'Click me' } , usageHint: 'body' } } }, - { id: 'btn-1', component: { Button: { child: 'btn-text', action: { name: 'submit' } } } }, + { + id: 'btn-text', + component: {Text: {text: {literalString: 'Click me'}, usageHint: 'body'}}, + }, + { + id: 'btn-1', + component: {Button: {child: 'btn-text', action: {name: 'submit'}}}, + }, ]), createBeginRendering('btn-1'), ]); @@ -282,8 +339,14 @@ describe('Property Updates via surfaceUpdate', () => { } else if (stage === 'updated') { processMessages([ createSurfaceUpdate([ - { id: 'btn-text', component: { Text: { text: { literalString: 'Submit now' } , usageHint: 'body' } } }, - { id: 'btn-1', component: { Button: { child: 'btn-text', action: { name: 'submit' } } } }, + { + id: 'btn-text', + component: {Text: {text: {literalString: 'Submit now'}, usageHint: 'body'}}, + }, + { + id: 'btn-1', + component: {Button: {child: 'btn-text', action: {name: 'submit'}}}, + }, ]), ]); } @@ -300,28 +363,31 @@ describe('Property Updates via surfaceUpdate', () => { render( - + , ); - expect(screen.getByRole('button', { name: 'Click me' })).toBeInTheDocument(); + expect(screen.getByRole('button', {name: 'Click me'})).toBeInTheDocument(); await waitFor(() => { expect(screen.getByTestId('stage')).toHaveTextContent('updated'); - expect(screen.getByRole('button', { name: 'Submit now' })).toBeInTheDocument(); - expect(screen.queryByRole('button', { name: 'Click me' })).not.toBeInTheDocument(); + expect(screen.getByRole('button', {name: 'Submit now'})).toBeInTheDocument(); + expect(screen.queryByRole('button', {name: 'Click me'})).not.toBeInTheDocument(); }); }); it('should update TextField label', async () => { function TextFieldLabelRenderer() { - const { processMessages } = useA2UI(); + const {processMessages} = useA2UI(); const [stage, setStage] = React.useState<'initial' | 'updated'>('initial'); useEffect(() => { if (stage === 'initial') { processMessages([ createSurfaceUpdate([ - { id: 'field-1', component: { TextField: { label: { literalString: 'Username' } } } }, + { + id: 'field-1', + component: {TextField: {label: {literalString: 'Username'}}}, + }, ]), createBeginRendering('field-1'), ]); @@ -329,7 +395,10 @@ describe('Property Updates via surfaceUpdate', () => { } else if (stage === 'updated') { processMessages([ createSurfaceUpdate([ - { id: 'field-1', component: { TextField: { label: { literalString: 'Email address' } } } }, + { + id: 'field-1', + component: {TextField: {label: {literalString: 'Email address'}}}, + }, ]), ]); } @@ -346,7 +415,7 @@ describe('Property Updates via surfaceUpdate', () => { render( - + , ); expect(screen.getByText('Username')).toBeInTheDocument(); @@ -360,14 +429,22 @@ describe('Property Updates via surfaceUpdate', () => { it('should update CheckBox label', async () => { function CheckBoxLabelRenderer() { - const { processMessages } = useA2UI(); + const {processMessages} = useA2UI(); const [stage, setStage] = React.useState<'initial' | 'updated'>('initial'); useEffect(() => { if (stage === 'initial') { processMessages([ createSurfaceUpdate([ - { id: 'cb-1', component: { CheckBox: { label: { literalString: 'Accept terms' }, value: { literalBoolean: false } } } }, + { + id: 'cb-1', + component: { + CheckBox: { + label: {literalString: 'Accept terms'}, + value: {literalBoolean: false}, + }, + }, + }, ]), createBeginRendering('cb-1'), ]); @@ -375,7 +452,15 @@ describe('Property Updates via surfaceUpdate', () => { } else if (stage === 'updated') { processMessages([ createSurfaceUpdate([ - { id: 'cb-1', component: { CheckBox: { label: { literalString: 'I agree to the terms and conditions' }, value: { literalBoolean: false } } } }, + { + id: 'cb-1', + component: { + CheckBox: { + label: {literalString: 'I agree to the terms and conditions'}, + value: {literalBoolean: false}, + }, + }, + }, ]), ]); } @@ -392,7 +477,7 @@ describe('Property Updates via surfaceUpdate', () => { render( - + , ); expect(screen.getByText('Accept terms')).toBeInTheDocument(); @@ -406,14 +491,22 @@ describe('Property Updates via surfaceUpdate', () => { it('should update CheckBox checked state', async () => { function CheckBoxStateRenderer() { - const { processMessages } = useA2UI(); + const {processMessages} = useA2UI(); const [stage, setStage] = React.useState<'initial' | 'updated'>('initial'); useEffect(() => { if (stage === 'initial') { processMessages([ createSurfaceUpdate([ - { id: 'cb-1', component: { CheckBox: { label: { literalString: 'Option' }, value: { literalBoolean: false } } } }, + { + id: 'cb-1', + component: { + CheckBox: { + label: {literalString: 'Option'}, + value: {literalBoolean: false}, + }, + }, + }, ]), createBeginRendering('cb-1'), ]); @@ -421,7 +514,15 @@ describe('Property Updates via surfaceUpdate', () => { } else if (stage === 'updated') { processMessages([ createSurfaceUpdate([ - { id: 'cb-1', component: { CheckBox: { label: { literalString: 'Option' }, value: { literalBoolean: true } } } }, + { + id: 'cb-1', + component: { + CheckBox: { + label: {literalString: 'Option'}, + value: {literalBoolean: true}, + }, + }, + }, ]), ]); } @@ -438,7 +539,7 @@ describe('Property Updates via surfaceUpdate', () => { render( - + , ); expect(screen.getByRole('checkbox')).not.toBeChecked(); @@ -451,7 +552,7 @@ describe('Property Updates via surfaceUpdate', () => { it('should update Slider value', async () => { function SliderValueRenderer() { - const { processMessages } = useA2UI(); + const {processMessages} = useA2UI(); const [stage, setStage] = React.useState<'initial' | 'updated'>('initial'); useEffect(() => { @@ -462,7 +563,7 @@ describe('Property Updates via surfaceUpdate', () => { id: 'slider-1', component: { Slider: { - value: { literalNumber: 25 }, + value: {literalNumber: 25}, minValue: 0, maxValue: 100, }, @@ -479,7 +580,7 @@ describe('Property Updates via surfaceUpdate', () => { id: 'slider-1', component: { Slider: { - value: { literalNumber: 75 }, + value: {literalNumber: 75}, minValue: 0, maxValue: 100, }, @@ -501,7 +602,7 @@ describe('Property Updates via surfaceUpdate', () => { render( - + , ); expect(screen.getByRole('slider')).toHaveValue('25'); @@ -514,7 +615,7 @@ describe('Property Updates via surfaceUpdate', () => { it('should update Slider min and max values', async () => { function SliderRangeRenderer() { - const { processMessages } = useA2UI(); + const {processMessages} = useA2UI(); const [stage, setStage] = React.useState<'initial' | 'updated'>('initial'); useEffect(() => { @@ -525,7 +626,7 @@ describe('Property Updates via surfaceUpdate', () => { id: 'slider-1', component: { Slider: { - value: { literalNumber: 50 }, + value: {literalNumber: 50}, minValue: 0, maxValue: 100, }, @@ -542,7 +643,7 @@ describe('Property Updates via surfaceUpdate', () => { id: 'slider-1', component: { Slider: { - value: { literalNumber: 50 }, + value: {literalNumber: 50}, minValue: 10, maxValue: 200, }, @@ -564,7 +665,7 @@ describe('Property Updates via surfaceUpdate', () => { render( - + , ); const slider = screen.getByRole('slider'); @@ -582,15 +683,23 @@ describe('Property Updates via surfaceUpdate', () => { describe('Layout Components', () => { it('should update Column alignment', async () => { function ColumnAlignmentRenderer() { - const { processMessages } = useA2UI(); + const {processMessages} = useA2UI(); const [stage, setStage] = React.useState<'initial' | 'updated'>('initial'); useEffect(() => { if (stage === 'initial') { processMessages([ createSurfaceUpdate([ - { id: 'text-1', component: { Text: { text: { literalString: 'Content' } , usageHint: 'body' } } }, - { id: 'col-1', component: { Column: { children: { explicitList: ['text-1'] }, alignment: 'start' } } }, + { + id: 'text-1', + component: {Text: {text: {literalString: 'Content'}, usageHint: 'body'}}, + }, + { + id: 'col-1', + component: { + Column: {children: {explicitList: ['text-1']}, alignment: 'start'}, + }, + }, ]), createBeginRendering('col-1'), ]); @@ -598,8 +707,16 @@ describe('Property Updates via surfaceUpdate', () => { } else if (stage === 'updated') { processMessages([ createSurfaceUpdate([ - { id: 'text-1', component: { Text: { text: { literalString: 'Content' } , usageHint: 'body' } } }, - { id: 'col-1', component: { Column: { children: { explicitList: ['text-1'] }, alignment: 'center' } } }, + { + id: 'text-1', + component: {Text: {text: {literalString: 'Content'}, usageHint: 'body'}}, + }, + { + id: 'col-1', + component: { + Column: {children: {explicitList: ['text-1']}, alignment: 'center'}, + }, + }, ]), ]); } @@ -613,10 +730,10 @@ describe('Property Updates via surfaceUpdate', () => { ); } - const { container } = render( + const {container} = render( - + , ); expect(container.querySelector('.a2ui-column')).toHaveAttribute('data-alignment', 'start'); @@ -629,15 +746,23 @@ describe('Property Updates via surfaceUpdate', () => { it('should update Column distribution', async () => { function ColumnDistributionRenderer() { - const { processMessages } = useA2UI(); + const {processMessages} = useA2UI(); const [stage, setStage] = React.useState<'initial' | 'updated'>('initial'); useEffect(() => { if (stage === 'initial') { processMessages([ createSurfaceUpdate([ - { id: 'text-1', component: { Text: { text: { literalString: 'Content' } , usageHint: 'body' } } }, - { id: 'col-1', component: { Column: { children: { explicitList: ['text-1'] }, distribution: 'start' } } }, + { + id: 'text-1', + component: {Text: {text: {literalString: 'Content'}, usageHint: 'body'}}, + }, + { + id: 'col-1', + component: { + Column: {children: {explicitList: ['text-1']}, distribution: 'start'}, + }, + }, ]), createBeginRendering('col-1'), ]); @@ -645,8 +770,19 @@ describe('Property Updates via surfaceUpdate', () => { } else if (stage === 'updated') { processMessages([ createSurfaceUpdate([ - { id: 'text-1', component: { Text: { text: { literalString: 'Content' } , usageHint: 'body' } } }, - { id: 'col-1', component: { Column: { children: { explicitList: ['text-1'] }, distribution: 'spaceBetween' } } }, + { + id: 'text-1', + component: {Text: {text: {literalString: 'Content'}, usageHint: 'body'}}, + }, + { + id: 'col-1', + component: { + Column: { + children: {explicitList: ['text-1']}, + distribution: 'spaceBetween', + }, + }, + }, ]), ]); } @@ -660,31 +796,42 @@ describe('Property Updates via surfaceUpdate', () => { ); } - const { container } = render( + const {container} = render( - + , ); expect(container.querySelector('.a2ui-column')).toHaveAttribute('data-distribution', 'start'); await waitFor(() => { expect(screen.getByTestId('stage')).toHaveTextContent('updated'); - expect(container.querySelector('.a2ui-column')).toHaveAttribute('data-distribution', 'spaceBetween'); + expect(container.querySelector('.a2ui-column')).toHaveAttribute( + 'data-distribution', + 'spaceBetween', + ); }); }); it('should update Row alignment', async () => { function RowAlignmentRenderer() { - const { processMessages } = useA2UI(); + const {processMessages} = useA2UI(); const [stage, setStage] = React.useState<'initial' | 'updated'>('initial'); useEffect(() => { if (stage === 'initial') { processMessages([ createSurfaceUpdate([ - { id: 'text-1', component: { Text: { text: { literalString: 'Content' } , usageHint: 'body' } } }, - { id: 'row-1', component: { Row: { children: { explicitList: ['text-1'] }, alignment: 'start' } } }, + { + id: 'text-1', + component: {Text: {text: {literalString: 'Content'}, usageHint: 'body'}}, + }, + { + id: 'row-1', + component: { + Row: {children: {explicitList: ['text-1']}, alignment: 'start'}, + }, + }, ]), createBeginRendering('row-1'), ]); @@ -692,8 +839,14 @@ describe('Property Updates via surfaceUpdate', () => { } else if (stage === 'updated') { processMessages([ createSurfaceUpdate([ - { id: 'text-1', component: { Text: { text: { literalString: 'Content' } , usageHint: 'body' } } }, - { id: 'row-1', component: { Row: { children: { explicitList: ['text-1'] }, alignment: 'end' } } }, + { + id: 'text-1', + component: {Text: {text: {literalString: 'Content'}, usageHint: 'body'}}, + }, + { + id: 'row-1', + component: {Row: {children: {explicitList: ['text-1']}, alignment: 'end'}}, + }, ]), ]); } @@ -707,10 +860,10 @@ describe('Property Updates via surfaceUpdate', () => { ); } - const { container } = render( + const {container} = render( - + , ); expect(container.querySelector('.a2ui-row')).toHaveAttribute('data-alignment', 'start'); @@ -723,15 +876,23 @@ describe('Property Updates via surfaceUpdate', () => { it('should update List direction', async () => { function ListDirectionRenderer() { - const { processMessages } = useA2UI(); + const {processMessages} = useA2UI(); const [stage, setStage] = React.useState<'initial' | 'updated'>('initial'); useEffect(() => { if (stage === 'initial') { processMessages([ createSurfaceUpdate([ - { id: 'item-1', component: { Text: { text: { literalString: 'Item' } , usageHint: 'body' } } }, - { id: 'list-1', component: { List: { children: { explicitList: ['item-1'] }, direction: 'vertical' } } }, + { + id: 'item-1', + component: {Text: {text: {literalString: 'Item'}, usageHint: 'body'}}, + }, + { + id: 'list-1', + component: { + List: {children: {explicitList: ['item-1']}, direction: 'vertical'}, + }, + }, ]), createBeginRendering('list-1'), ]); @@ -739,8 +900,16 @@ describe('Property Updates via surfaceUpdate', () => { } else if (stage === 'updated') { processMessages([ createSurfaceUpdate([ - { id: 'item-1', component: { Text: { text: { literalString: 'Item' } , usageHint: 'body' } } }, - { id: 'list-1', component: { List: { children: { explicitList: ['item-1'] }, direction: 'horizontal' } } }, + { + id: 'item-1', + component: {Text: {text: {literalString: 'Item'}, usageHint: 'body'}}, + }, + { + id: 'list-1', + component: { + List: {children: {explicitList: ['item-1']}, direction: 'horizontal'}, + }, + }, ]), ]); } @@ -754,17 +923,20 @@ describe('Property Updates via surfaceUpdate', () => { ); } - const { container } = render( + const {container} = render( - + , ); expect(container.querySelector('.a2ui-list')).toHaveAttribute('data-direction', 'vertical'); await waitFor(() => { expect(screen.getByTestId('stage')).toHaveTextContent('updated'); - expect(container.querySelector('.a2ui-list')).toHaveAttribute('data-direction', 'horizontal'); + expect(container.querySelector('.a2ui-list')).toHaveAttribute( + 'data-direction', + 'horizontal', + ); }); }); }); @@ -772,19 +944,24 @@ describe('Property Updates via surfaceUpdate', () => { describe('Complex Components', () => { it('should update Tabs titles', async () => { function TabsTitleRenderer() { - const { processMessages } = useA2UI(); + const {processMessages} = useA2UI(); const [stage, setStage] = React.useState<'initial' | 'updated'>('initial'); useEffect(() => { if (stage === 'initial') { processMessages([ createSurfaceUpdate([ - { id: 'content-1', component: { Text: { text: { literalString: 'Tab content' } , usageHint: 'body' } } }, + { + id: 'content-1', + component: { + Text: {text: {literalString: 'Tab content'}, usageHint: 'body'}, + }, + }, { id: 'tabs-1', component: { Tabs: { - tabItems: [{ title: { literalString: 'Tab A' }, child: 'content-1' }], + tabItems: [{title: {literalString: 'Tab A'}, child: 'content-1'}], }, }, }, @@ -795,12 +972,17 @@ describe('Property Updates via surfaceUpdate', () => { } else if (stage === 'updated') { processMessages([ createSurfaceUpdate([ - { id: 'content-1', component: { Text: { text: { literalString: 'Tab content' } , usageHint: 'body' } } }, + { + id: 'content-1', + component: { + Text: {text: {literalString: 'Tab content'}, usageHint: 'body'}, + }, + }, { id: 'tabs-1', component: { Tabs: { - tabItems: [{ title: { literalString: 'Renamed Tab' }, child: 'content-1' }], + tabItems: [{title: {literalString: 'Renamed Tab'}, child: 'content-1'}], }, }, }, @@ -820,33 +1002,36 @@ describe('Property Updates via surfaceUpdate', () => { render( - + , ); - expect(screen.getByRole('button', { name: 'Tab A' })).toBeInTheDocument(); + expect(screen.getByRole('button', {name: 'Tab A'})).toBeInTheDocument(); await waitFor(() => { expect(screen.getByTestId('stage')).toHaveTextContent('updated'); - expect(screen.getByRole('button', { name: 'Renamed Tab' })).toBeInTheDocument(); - expect(screen.queryByRole('button', { name: 'Tab A' })).not.toBeInTheDocument(); + expect(screen.getByRole('button', {name: 'Renamed Tab'})).toBeInTheDocument(); + expect(screen.queryByRole('button', {name: 'Tab A'})).not.toBeInTheDocument(); }); }); it('should add new tabs via surfaceUpdate', async () => { function TabsAddRenderer() { - const { processMessages } = useA2UI(); + const {processMessages} = useA2UI(); const [stage, setStage] = React.useState<'initial' | 'updated'>('initial'); useEffect(() => { if (stage === 'initial') { processMessages([ createSurfaceUpdate([ - { id: 'content-1', component: { Text: { text: { literalString: 'Content 1' } , usageHint: 'body' } } }, + { + id: 'content-1', + component: {Text: {text: {literalString: 'Content 1'}, usageHint: 'body'}}, + }, { id: 'tabs-1', component: { Tabs: { - tabItems: [{ title: { literalString: 'Tab 1' }, child: 'content-1' }], + tabItems: [{title: {literalString: 'Tab 1'}, child: 'content-1'}], }, }, }, @@ -857,15 +1042,21 @@ describe('Property Updates via surfaceUpdate', () => { } else if (stage === 'updated') { processMessages([ createSurfaceUpdate([ - { id: 'content-1', component: { Text: { text: { literalString: 'Content 1' } , usageHint: 'body' } } }, - { id: 'content-2', component: { Text: { text: { literalString: 'Content 2' } , usageHint: 'body' } } }, + { + id: 'content-1', + component: {Text: {text: {literalString: 'Content 1'}, usageHint: 'body'}}, + }, + { + id: 'content-2', + component: {Text: {text: {literalString: 'Content 2'}, usageHint: 'body'}}, + }, { id: 'tabs-1', component: { Tabs: { tabItems: [ - { title: { literalString: 'Tab 1' }, child: 'content-1' }, - { title: { literalString: 'Tab 2' }, child: 'content-2' }, + {title: {literalString: 'Tab 1'}, child: 'content-1'}, + {title: {literalString: 'Tab 2'}, child: 'content-2'}, ], }, }, @@ -886,16 +1077,16 @@ describe('Property Updates via surfaceUpdate', () => { render( - + , ); - expect(screen.getByRole('button', { name: 'Tab 1' })).toBeInTheDocument(); - expect(screen.queryByRole('button', { name: 'Tab 2' })).not.toBeInTheDocument(); + expect(screen.getByRole('button', {name: 'Tab 1'})).toBeInTheDocument(); + expect(screen.queryByRole('button', {name: 'Tab 2'})).not.toBeInTheDocument(); await waitFor(() => { expect(screen.getByTestId('stage')).toHaveTextContent('updated'); - expect(screen.getByRole('button', { name: 'Tab 1' })).toBeInTheDocument(); - expect(screen.getByRole('button', { name: 'Tab 2' })).toBeInTheDocument(); + expect(screen.getByRole('button', {name: 'Tab 1'})).toBeInTheDocument(); + expect(screen.getByRole('button', {name: 'Tab 2'})).toBeInTheDocument(); }); }); }); diff --git a/renderers/react/tests/v0_8/integration/templates.test.tsx b/renderers/react/tests/v0_8/integration/templates.test.tsx index f85b92fa9..c738b7275 100644 --- a/renderers/react/tests/v0_8/integration/templates.test.tsx +++ b/renderers/react/tests/v0_8/integration/templates.test.tsx @@ -14,10 +14,10 @@ * limitations under the License. */ -import { describe, it, expect, vi } from 'vitest'; -import { render, screen, fireEvent, waitFor } from '@testing-library/react'; -import React, { useEffect } from 'react'; -import { A2UIProvider, A2UIRenderer, useA2UI } from '../../../src/v0_8'; +import {describe, it, expect, vi} from 'vitest'; +import {render, screen, fireEvent, waitFor} from '@testing-library/react'; +import React, {useEffect} from 'react'; +import {A2UIProvider, A2UIRenderer, useA2UI} from '../../../src/v0_8'; import type * as Types from '@a2ui/web_core/types/types'; import { createSurfaceUpdate, @@ -37,18 +37,14 @@ import { describe('Template Integration', () => { it('should expand template with dataBinding to array', () => { function TemplateRenderer() { - const { processMessages } = useA2UI(); + const {processMessages} = useA2UI(); useEffect(() => { processMessages([ createDataModelUpdateSpec([ { key: 'items', - valueString: JSON.stringify([ - { name: 'Item A' }, - { name: 'Item B' }, - { name: 'Item C' }, - ]), + valueString: JSON.stringify([{name: 'Item A'}, {name: 'Item B'}, {name: 'Item C'}]), }, ]), createSurfaceUpdate([ @@ -68,7 +64,7 @@ describe('Template Integration', () => { { id: 'item-template', component: { - Text: { text: { path: 'name' }, usageHint: 'body' }, + Text: {text: {path: 'name'}, usageHint: 'body'}, }, }, ]), @@ -82,7 +78,7 @@ describe('Template Integration', () => { render( - + , ); expect(screen.getByText('Item A')).toBeInTheDocument(); @@ -92,7 +88,7 @@ describe('Template Integration', () => { it('should handle template with primitive array values using path "."', () => { function PrimitiveTemplateRenderer() { - const { processMessages } = useA2UI(); + const {processMessages} = useA2UI(); useEffect(() => { processMessages([ @@ -119,7 +115,7 @@ describe('Template Integration', () => { { id: 'tag-template', component: { - Text: { text: { path: '.' }, usageHint: 'body' }, + Text: {text: {path: '.'}, usageHint: 'body'}, }, }, ]), @@ -133,7 +129,7 @@ describe('Template Integration', () => { render( - + , ); expect(screen.getByText('travel')).toBeInTheDocument(); @@ -143,7 +139,7 @@ describe('Template Integration', () => { it('should expand nested templates with layered data contexts', () => { function NestedTemplateRenderer() { - const { processMessages } = useA2UI(); + const {processMessages} = useA2UI(); useEffect(() => { processMessages([ @@ -151,8 +147,8 @@ describe('Template Integration', () => { { key: 'days', valueString: JSON.stringify([ - { title: 'Day 1', activities: ['Morning Walk', 'Museum Visit'] }, - { title: 'Day 2', activities: ['Market Trip'] }, + {title: 'Day 1', activities: ['Morning Walk', 'Museum Visit']}, + {title: 'Day 2', activities: ['Market Trip']}, ]), }, ]), @@ -174,14 +170,14 @@ describe('Template Integration', () => { id: 'day-template', component: { Column: { - children: { explicitList: ['day-title', 'activity-list'] }, + children: {explicitList: ['day-title', 'activity-list']}, }, }, }, { id: 'day-title', component: { - Text: { text: { path: 'title' }, usageHint: 'body' }, + Text: {text: {path: 'title'}, usageHint: 'body'}, }, }, { @@ -200,7 +196,7 @@ describe('Template Integration', () => { { id: 'activity-template', component: { - Text: { text: { path: '.' }, usageHint: 'body' }, + Text: {text: {path: '.'}, usageHint: 'body'}, }, }, ]), @@ -214,7 +210,7 @@ describe('Template Integration', () => { render( - + , ); expect(screen.getByText('Day 1')).toBeInTheDocument(); @@ -226,7 +222,7 @@ describe('Template Integration', () => { it('should rebuild template when data arrives after components', async () => { function LateDataRenderer() { - const { processMessages } = useA2UI(); + const {processMessages} = useA2UI(); const [step, setStep] = React.useState(0); useEffect(() => { @@ -249,7 +245,7 @@ describe('Template Integration', () => { { id: 'item-template', component: { - Text: { text: { path: 'name' }, usageHint: 'body' }, + Text: {text: {path: 'name'}, usageHint: 'body'}, }, }, ]), @@ -262,10 +258,7 @@ describe('Template Integration', () => { createDataModelUpdateSpec([ { key: 'items', - valueString: JSON.stringify([ - { name: 'Late Item 1' }, - { name: 'Late Item 2' }, - ]), + valueString: JSON.stringify([{name: 'Late Item 1'}, {name: 'Late Item 2'}]), }, ]), ]); @@ -279,7 +272,7 @@ describe('Template Integration', () => { render( - + , ); await waitFor(() => { @@ -294,7 +287,7 @@ describe('Template Integration', () => { // Use a top-level valueMap with flat string values that the processor converts to a Map, // combined with a JSON-encoded array for the iterable data. function MapTemplateRenderer() { - const { processMessages } = useA2UI(); + const {processMessages} = useA2UI(); useEffect(() => { processMessages([ @@ -302,8 +295,8 @@ describe('Template Integration', () => { { key: 'users', valueString: JSON.stringify([ - { name: 'Alice', role: 'Admin' }, - { name: 'Bob', role: 'User' }, + {name: 'Alice', role: 'Admin'}, + {name: 'Bob', role: 'User'}, ]), }, ]), @@ -324,27 +317,27 @@ describe('Template Integration', () => { { id: 'user-card', component: { - Card: { child: 'user-info' }, + Card: {child: 'user-info'}, }, }, { id: 'user-info', component: { Column: { - children: { explicitList: ['user-name', 'user-role'] }, + children: {explicitList: ['user-name', 'user-role']}, }, }, }, { id: 'user-name', component: { - Text: { text: { path: 'name' }, usageHint: 'body' }, + Text: {text: {path: 'name'}, usageHint: 'body'}, }, }, { id: 'user-role', component: { - Text: { text: { path: 'role' }, usageHint: 'body' }, + Text: {text: {path: 'role'}, usageHint: 'body'}, }, }, ]), @@ -358,7 +351,7 @@ describe('Template Integration', () => { render( - + , ); expect(screen.getByText('Alice')).toBeInTheDocument(); @@ -371,7 +364,7 @@ describe('Template Integration', () => { const mockOnAction = vi.fn(); function ComplexTemplateRenderer() { - const { processMessages } = useA2UI(); + const {processMessages} = useA2UI(); useEffect(() => { processMessages([ @@ -379,8 +372,8 @@ describe('Template Integration', () => { { key: 'products', valueString: JSON.stringify([ - { id: 'prod-1', name: 'Widget', price: '$10' }, - { id: 'prod-2', name: 'Gadget', price: '$20' }, + {id: 'prod-1', name: 'Widget', price: '$10'}, + {id: 'prod-2', name: 'Gadget', price: '$20'}, ]), }, ]), @@ -402,20 +395,20 @@ describe('Template Integration', () => { id: 'product-row', component: { Row: { - children: { explicitList: ['product-name', 'product-price', 'buy-button'] }, + children: {explicitList: ['product-name', 'product-price', 'buy-button']}, }, }, }, { id: 'product-name', component: { - Text: { text: { path: 'name' }, usageHint: 'body' }, + Text: {text: {path: 'name'}, usageHint: 'body'}, }, }, { id: 'product-price', component: { - Text: { text: { path: 'price' }, usageHint: 'body' }, + Text: {text: {path: 'price'}, usageHint: 'body'}, }, }, { @@ -425,9 +418,7 @@ describe('Template Integration', () => { child: 'buy-text', action: { name: 'buy', - context: [ - { key: 'productId', value: { path: 'id' } }, - ], + context: [{key: 'productId', value: {path: 'id'}}], }, }, }, @@ -435,7 +426,7 @@ describe('Template Integration', () => { { id: 'buy-text', component: { - Text: { text: { literalString: 'Buy' }, usageHint: 'body' }, + Text: {text: {literalString: 'Buy'}, usageHint: 'body'}, }, }, ]), @@ -449,7 +440,7 @@ describe('Template Integration', () => { render( - + , ); expect(screen.getByText('Widget')).toBeInTheDocument(); @@ -457,7 +448,7 @@ describe('Template Integration', () => { expect(screen.getByText('Gadget')).toBeInTheDocument(); expect(screen.getByText('$20')).toBeInTheDocument(); - const buyButtons = screen.getAllByRole('button', { name: 'Buy' }); + const buyButtons = screen.getAllByRole('button', {name: 'Buy'}); expect(buyButtons).toHaveLength(2); fireEvent.click(getElement(buyButtons, 0)); @@ -469,7 +460,7 @@ describe('Template Integration', () => { it('should handle empty data array gracefully', () => { function EmptyDataRenderer() { - const { processMessages } = useA2UI(); + const {processMessages} = useA2UI(); useEffect(() => { processMessages([ @@ -484,14 +475,14 @@ describe('Template Integration', () => { id: 'root', component: { Column: { - children: { explicitList: ['header', 'item-list'] }, + children: {explicitList: ['header', 'item-list']}, }, }, }, { id: 'header', component: { - Text: { text: { literalString: 'Items:' }, usageHint: 'body' }, + Text: {text: {literalString: 'Items:'}, usageHint: 'body'}, }, }, { @@ -510,7 +501,7 @@ describe('Template Integration', () => { { id: 'item-template', component: { - Text: { text: { path: 'name' }, usageHint: 'body' }, + Text: {text: {path: 'name'}, usageHint: 'body'}, }, }, ]), @@ -524,7 +515,7 @@ describe('Template Integration', () => { render( - + , ); expect(screen.getByText('Items:')).toBeInTheDocument(); @@ -532,7 +523,7 @@ describe('Template Integration', () => { it('should update template when data changes', async () => { function DataChangeRenderer() { - const { processMessages } = useA2UI(); + const {processMessages} = useA2UI(); const [step, setStep] = React.useState(0); useEffect(() => { @@ -541,7 +532,7 @@ describe('Template Integration', () => { createDataModelUpdateSpec([ { key: 'items', - valueString: JSON.stringify([{ name: 'Original' }]), + valueString: JSON.stringify([{name: 'Original'}]), }, ]), createSurfaceUpdate([ @@ -561,7 +552,7 @@ describe('Template Integration', () => { { id: 'item-template', component: { - Text: { text: { path: 'name' }, usageHint: 'body' }, + Text: {text: {path: 'name'}, usageHint: 'body'}, }, }, ]), @@ -574,10 +565,7 @@ describe('Template Integration', () => { createDataModelUpdateSpec([ { key: 'items', - valueString: JSON.stringify([ - { name: 'Updated 1' }, - { name: 'Updated 2' }, - ]), + valueString: JSON.stringify([{name: 'Updated 1'}, {name: 'Updated 2'}]), }, ]), ]); @@ -591,7 +579,7 @@ describe('Template Integration', () => { render( - + , ); expect(screen.getByText('Original')).toBeInTheDocument(); diff --git a/renderers/react/tests/v0_8/unit/components/Button.test.tsx b/renderers/react/tests/v0_8/unit/components/Button.test.tsx index 0edda1d28..af376cb45 100644 --- a/renderers/react/tests/v0_8/unit/components/Button.test.tsx +++ b/renderers/react/tests/v0_8/unit/components/Button.test.tsx @@ -14,10 +14,16 @@ * limitations under the License. */ -import { describe, it, expect, vi } from 'vitest'; -import { render, screen, fireEvent } from '@testing-library/react'; +import {describe, it, expect, vi} from 'vitest'; +import {render, screen, fireEvent} from '@testing-library/react'; import React from 'react'; -import { TestWrapper, TestRenderer, createSurfaceUpdate, createBeginRendering, getMockCallArg } from '../../utils'; +import { + TestWrapper, + TestRenderer, + createSurfaceUpdate, + createBeginRendering, + getMockCallArg, +} from '../../utils'; import type * as Types from '@a2ui/web_core/types/types'; /** @@ -37,11 +43,19 @@ function createButtonMessages( id: string, props: { actionName: string; - actionContext?: Array<{ key: string; value: { literalString?: string; literalNumber?: number; literalBoolean?: boolean; path?: string } }>; + actionContext?: Array<{ + key: string; + value: { + literalString?: string; + literalNumber?: number; + literalBoolean?: boolean; + path?: string; + }; + }>; childText?: string; primary?: boolean; }, - surfaceId = '@default' + surfaceId = '@default', ): Types.ServerToClientMessage[] { const textId = `${id}-text`; @@ -53,8 +67,8 @@ function createButtonMessages( id: textId, component: { Text: { - text: { literalString: props.childText ?? 'Click me' }, - usageHint: 'body', + text: {literalString: props.childText ?? 'Click me'}, + usageHint: 'body', }, }, }, @@ -73,7 +87,7 @@ function createButtonMessages( }, }, ], - surfaceId + surfaceId, ), createBeginRendering(id, surfaceId), ]; @@ -82,12 +96,12 @@ function createButtonMessages( describe('Button Component', () => { describe('Basic Rendering', () => { it('should render a button element', () => { - const messages = createButtonMessages('btn-1', { actionName: 'submit' }); + const messages = createButtonMessages('btn-1', {actionName: 'submit'}); - const { container } = render( + const {container} = render( - + , ); const button = container.querySelector('button'); @@ -96,12 +110,12 @@ describe('Button Component', () => { }); it('should render with wrapper div having correct class', () => { - const messages = createButtonMessages('btn-1', { actionName: 'submit' }); + const messages = createButtonMessages('btn-1', {actionName: 'submit'}); - const { container } = render( + const {container} = render( - + , ); const wrapper = container.querySelector('.a2ui-button'); @@ -115,10 +129,10 @@ describe('Button Component', () => { childText: 'Submit Form', }); - const { container } = render( + const {container} = render( - + , ); const button = container.querySelector('button'); @@ -137,22 +151,22 @@ describe('Button Component', () => { childText: 'Cancel', }); - const { container: container1 } = render( + const {container: container1} = render( - + , ); - const { container: container2 } = render( + const {container: container2} = render( - + , ); expect(container1.querySelector('button')?.textContent).toContain('Save'); expect(container2.querySelector('button')?.textContent).toContain('Cancel'); // Verify they're different expect(container1.querySelector('button')?.textContent).not.toBe( - container2.querySelector('button')?.textContent + container2.querySelector('button')?.textContent, ); }); }); @@ -160,12 +174,12 @@ describe('Button Component', () => { describe('Action Handling', () => { it('should call onAction with correct action name when clicked', () => { const mockOnAction = vi.fn(); - const messages = createButtonMessages('btn-1', { actionName: 'submit-form' }); + const messages = createButtonMessages('btn-1', {actionName: 'submit-form'}); render( - + , ); const button = screen.getByRole('button'); @@ -186,15 +200,15 @@ describe('Button Component', () => { const messages = createButtonMessages('btn-1', { actionName: 'delete-item', actionContext: [ - { key: 'itemId', value: { literalString: '123' } }, - { key: 'confirmed', value: { literalBoolean: true } }, + {key: 'itemId', value: {literalString: '123'}}, + {key: 'confirmed', value: {literalBoolean: true}}, ], }); render( - + , ); fireEvent.click(screen.getByRole('button')); @@ -211,12 +225,12 @@ describe('Button Component', () => { it('should not call onAction before click', () => { const mockOnAction = vi.fn(); - const messages = createButtonMessages('btn-1', { actionName: 'test' }); + const messages = createButtonMessages('btn-1', {actionName: 'test'}); render( - + , ); // No click - should not have been called @@ -225,12 +239,12 @@ describe('Button Component', () => { it('should call onAction multiple times for multiple clicks', () => { const mockOnAction = vi.fn(); - const messages = createButtonMessages('btn-1', { actionName: 'increment' }); + const messages = createButtonMessages('btn-1', {actionName: 'increment'}); render( - + , ); const button = screen.getByRole('button'); @@ -244,12 +258,12 @@ describe('Button Component', () => { describe('Accessibility', () => { it('should be focusable', () => { - const messages = createButtonMessages('btn-1', { actionName: 'action' }); + const messages = createButtonMessages('btn-1', {actionName: 'action'}); render( - + , ); const button = screen.getByRole('button'); @@ -259,12 +273,12 @@ describe('Button Component', () => { it('should be clickable via role', () => { const mockOnAction = vi.fn(); - const messages = createButtonMessages('btn-1', { actionName: 'action' }); + const messages = createButtonMessages('btn-1', {actionName: 'action'}); render( - + , ); // Using getByRole ensures the button has proper ARIA role diff --git a/renderers/react/tests/v0_8/unit/components/Card.test.tsx b/renderers/react/tests/v0_8/unit/components/Card.test.tsx index d62f05e53..a29e390a3 100644 --- a/renderers/react/tests/v0_8/unit/components/Card.test.tsx +++ b/renderers/react/tests/v0_8/unit/components/Card.test.tsx @@ -14,10 +14,10 @@ * limitations under the License. */ -import { describe, it, expect } from 'vitest'; -import { render, screen } from '@testing-library/react'; +import {describe, it, expect} from 'vitest'; +import {render, screen} from '@testing-library/react'; import React from 'react'; -import { TestWrapper, TestRenderer, createSurfaceUpdate, createBeginRendering } from '../../utils'; +import {TestWrapper, TestRenderer, createSurfaceUpdate, createBeginRendering} from '../../utils'; import type * as Types from '@a2ui/web_core/types/types'; /** @@ -29,16 +29,19 @@ describe('Card Component', () => { it('should render a section element', () => { const messages: Types.ServerToClientMessage[] = [ createSurfaceUpdate([ - { id: 'text-1', component: { Text: { text: { literalString: 'Card content' } , usageHint: 'body' } } }, - { id: 'card-1', component: { Card: { child: 'text-1' } } }, + { + id: 'text-1', + component: {Text: {text: {literalString: 'Card content'}, usageHint: 'body'}}, + }, + {id: 'card-1', component: {Card: {child: 'text-1'}}}, ]), createBeginRendering('card-1'), ]; - const { container } = render( + const {container} = render( - + , ); const section = container.querySelector('section'); @@ -48,16 +51,19 @@ describe('Card Component', () => { it('should render with wrapper div', () => { const messages: Types.ServerToClientMessage[] = [ createSurfaceUpdate([ - { id: 'text-1', component: { Text: { text: { literalString: 'Content' } , usageHint: 'body' } } }, - { id: 'card-1', component: { Card: { child: 'text-1' } } }, + { + id: 'text-1', + component: {Text: {text: {literalString: 'Content'}, usageHint: 'body'}}, + }, + {id: 'card-1', component: {Card: {child: 'text-1'}}}, ]), createBeginRendering('card-1'), ]; - const { container } = render( + const {container} = render( - + , ); const wrapper = container.querySelector('.a2ui-card'); @@ -69,8 +75,11 @@ describe('Card Component', () => { it('should render child Text component', () => { const messages: Types.ServerToClientMessage[] = [ createSurfaceUpdate([ - { id: 'text-1', component: { Text: { text: { literalString: 'Card content' } , usageHint: 'body' } } }, - { id: 'card-1', component: { Card: { child: 'text-1' } } }, + { + id: 'text-1', + component: {Text: {text: {literalString: 'Card content'}, usageHint: 'body'}}, + }, + {id: 'card-1', component: {Card: {child: 'text-1'}}}, ]), createBeginRendering('card-1'), ]; @@ -78,7 +87,7 @@ describe('Card Component', () => { render( - + , ); expect(screen.getByText('Card content')).toBeInTheDocument(); @@ -87,9 +96,12 @@ describe('Card Component', () => { it('should render nested Button in Card', () => { const messages: Types.ServerToClientMessage[] = [ createSurfaceUpdate([ - { id: 'btn-text', component: { Text: { text: { literalString: 'Click me' } , usageHint: 'body' } } }, - { id: 'btn-1', component: { Button: { child: 'btn-text', action: { name: 'click' } } } }, - { id: 'card-1', component: { Card: { child: 'btn-1' } } }, + { + id: 'btn-text', + component: {Text: {text: {literalString: 'Click me'}, usageHint: 'body'}}, + }, + {id: 'btn-1', component: {Button: {child: 'btn-text', action: {name: 'click'}}}}, + {id: 'card-1', component: {Card: {child: 'btn-1'}}}, ]), createBeginRendering('card-1'), ]; @@ -97,7 +109,7 @@ describe('Card Component', () => { render( - + , ); expect(screen.getByRole('button')).toBeInTheDocument(); @@ -109,16 +121,19 @@ describe('Card Component', () => { it('should apply theme classes to section', () => { const messages: Types.ServerToClientMessage[] = [ createSurfaceUpdate([ - { id: 'text-1', component: { Text: { text: { literalString: 'Content' } , usageHint: 'body' } } }, - { id: 'card-1', component: { Card: { child: 'text-1' } } }, + { + id: 'text-1', + component: {Text: {text: {literalString: 'Content'}, usageHint: 'body'}}, + }, + {id: 'card-1', component: {Card: {child: 'text-1'}}}, ]), createBeginRendering('card-1'), ]; - const { container } = render( + const {container} = render( - + , ); const section = container.querySelector('section'); @@ -133,16 +148,19 @@ describe('Card Component', () => { it('should have correct DOM structure', () => { const messages: Types.ServerToClientMessage[] = [ createSurfaceUpdate([ - { id: 'text-1', component: { Text: { text: { literalString: 'Content' } , usageHint: 'body' } } }, - { id: 'card-1', component: { Card: { child: 'text-1' } } }, + { + id: 'text-1', + component: {Text: {text: {literalString: 'Content'}, usageHint: 'body'}}, + }, + {id: 'card-1', component: {Card: {child: 'text-1'}}}, ]), createBeginRendering('card-1'), ]; - const { container } = render( + const {container} = render( - + , ); const wrapper = container.querySelector('.a2ui-card'); diff --git a/renderers/react/tests/v0_8/unit/components/CheckBox.test.tsx b/renderers/react/tests/v0_8/unit/components/CheckBox.test.tsx index 42a91f129..55edc7d21 100644 --- a/renderers/react/tests/v0_8/unit/components/CheckBox.test.tsx +++ b/renderers/react/tests/v0_8/unit/components/CheckBox.test.tsx @@ -14,10 +14,10 @@ * limitations under the License. */ -import { describe, it, expect } from 'vitest'; -import { render, screen, fireEvent } from '@testing-library/react'; +import {describe, it, expect} from 'vitest'; +import {render, screen, fireEvent} from '@testing-library/react'; import React from 'react'; -import { TestWrapper, TestRenderer, createSimpleMessages } from '../../utils'; +import {TestWrapper, TestRenderer, createSimpleMessages} from '../../utils'; /** * CheckBox tests following A2UI specification. @@ -27,14 +27,14 @@ describe('CheckBox Component', () => { describe('Basic Rendering', () => { it('should render a checkbox input', () => { const messages = createSimpleMessages('cb-1', 'CheckBox', { - label: { literalString: 'Accept terms' }, - value: { literalBoolean: false }, + label: {literalString: 'Accept terms'}, + value: {literalBoolean: false}, }); - const { container } = render( + const {container} = render( - + , ); const checkbox = container.querySelector('input[type="checkbox"]'); @@ -43,14 +43,14 @@ describe('CheckBox Component', () => { it('should render with wrapper div', () => { const messages = createSimpleMessages('cb-1', 'CheckBox', { - label: { literalString: 'Subscribe' }, - value: { literalBoolean: false }, + label: {literalString: 'Subscribe'}, + value: {literalBoolean: false}, }); - const { container } = render( + const {container} = render( - + , ); const wrapper = container.querySelector('.a2ui-checkbox'); @@ -59,14 +59,14 @@ describe('CheckBox Component', () => { it('should render unchecked when value is false', () => { const messages = createSimpleMessages('cb-1', 'CheckBox', { - label: { literalString: 'Option' }, - value: { literalBoolean: false }, + label: {literalString: 'Option'}, + value: {literalBoolean: false}, }); - const { container } = render( + const {container} = render( - + , ); const checkbox = container.querySelector('input[type="checkbox"]') as HTMLInputElement; @@ -75,14 +75,14 @@ describe('CheckBox Component', () => { it('should render checked when value is true', () => { const messages = createSimpleMessages('cb-1', 'CheckBox', { - label: { literalString: 'Option' }, - value: { literalBoolean: true }, + label: {literalString: 'Option'}, + value: {literalBoolean: true}, }); - const { container } = render( + const {container} = render( - + , ); const checkbox = container.querySelector('input[type="checkbox"]') as HTMLInputElement; @@ -91,27 +91,31 @@ describe('CheckBox Component', () => { it('should render different states for different value inputs', () => { const messagesTrue = createSimpleMessages('cb-1', 'CheckBox', { - label: { literalString: 'Checked option' }, - value: { literalBoolean: true }, + label: {literalString: 'Checked option'}, + value: {literalBoolean: true}, }); const messagesFalse = createSimpleMessages('cb-2', 'CheckBox', { - label: { literalString: 'Unchecked option' }, - value: { literalBoolean: false }, + label: {literalString: 'Unchecked option'}, + value: {literalBoolean: false}, }); - const { container: containerTrue } = render( + const {container: containerTrue} = render( - + , ); - const { container: containerFalse } = render( + const {container: containerFalse} = render( - + , ); - const checkboxTrue = containerTrue.querySelector('input[type="checkbox"]') as HTMLInputElement; - const checkboxFalse = containerFalse.querySelector('input[type="checkbox"]') as HTMLInputElement; + const checkboxTrue = containerTrue.querySelector( + 'input[type="checkbox"]', + ) as HTMLInputElement; + const checkboxFalse = containerFalse.querySelector( + 'input[type="checkbox"]', + ) as HTMLInputElement; expect(checkboxTrue.checked).toBe(true); expect(checkboxFalse.checked).toBe(false); @@ -121,23 +125,23 @@ describe('CheckBox Component', () => { it('should render different labels for different inputs', () => { const messages1 = createSimpleMessages('cb-1', 'CheckBox', { - label: { literalString: 'First Option' }, - value: { literalBoolean: false }, + label: {literalString: 'First Option'}, + value: {literalBoolean: false}, }); const messages2 = createSimpleMessages('cb-2', 'CheckBox', { - label: { literalString: 'Second Option' }, - value: { literalBoolean: false }, + label: {literalString: 'Second Option'}, + value: {literalBoolean: false}, }); - const { container: container1 } = render( + const {container: container1} = render( - + , ); - const { container: container2 } = render( + const {container: container2} = render( - + , ); const label1 = container1.querySelector('label'); @@ -152,14 +156,14 @@ describe('CheckBox Component', () => { describe('Label Rendering', () => { it('should render label (required field)', () => { const messages = createSimpleMessages('cb-1', 'CheckBox', { - label: { literalString: 'Accept terms and conditions' }, - value: { literalBoolean: false }, + label: {literalString: 'Accept terms and conditions'}, + value: {literalBoolean: false}, }); render( - + , ); expect(screen.getByText('Accept terms and conditions')).toBeInTheDocument(); @@ -167,14 +171,14 @@ describe('CheckBox Component', () => { it('should associate label with checkbox via htmlFor', () => { const messages = createSimpleMessages('cb-1', 'CheckBox', { - label: { literalString: 'Remember me' }, - value: { literalBoolean: false }, + label: {literalString: 'Remember me'}, + value: {literalBoolean: false}, }); - const { container } = render( + const {container} = render( - + , ); const label = container.querySelector('label'); @@ -186,14 +190,14 @@ describe('CheckBox Component', () => { describe('User Interaction', () => { it('should toggle checked state on click', () => { const messages = createSimpleMessages('cb-1', 'CheckBox', { - label: { literalString: 'Toggle me' }, - value: { literalBoolean: false }, + label: {literalString: 'Toggle me'}, + value: {literalBoolean: false}, }); - const { container } = render( + const {container} = render( - + , ); const checkbox = container.querySelector('input[type="checkbox"]') as HTMLInputElement; @@ -208,18 +212,18 @@ describe('CheckBox Component', () => { it('should toggle via change event', () => { const messages = createSimpleMessages('cb-1', 'CheckBox', { - label: { literalString: 'Option' }, - value: { literalBoolean: false }, + label: {literalString: 'Option'}, + value: {literalBoolean: false}, }); - const { container } = render( + const {container} = render( - + , ); const checkbox = container.querySelector('input[type="checkbox"]') as HTMLInputElement; - fireEvent.change(checkbox, { target: { checked: true } }); + fireEvent.change(checkbox, {target: {checked: true}}); expect(checkbox.checked).toBe(true); }); @@ -228,14 +232,14 @@ describe('CheckBox Component', () => { describe('Theme Support', () => { it('should render within section container', () => { const messages = createSimpleMessages('cb-1', 'CheckBox', { - label: { literalString: 'Option' }, - value: { literalBoolean: false }, + label: {literalString: 'Option'}, + value: {literalBoolean: false}, }); - const { container } = render( + const {container} = render( - + , ); const section = container.querySelector('section'); @@ -247,14 +251,14 @@ describe('CheckBox Component', () => { describe('Structure', () => { it('should render checkbox before label (Lit structure)', () => { const messages = createSimpleMessages('cb-1', 'CheckBox', { - label: { literalString: 'Option label' }, - value: { literalBoolean: false }, + label: {literalString: 'Option label'}, + value: {literalBoolean: false}, }); - const { container } = render( + const {container} = render( - + , ); const section = container.querySelector('section'); diff --git a/renderers/react/tests/v0_8/unit/components/Column.test.tsx b/renderers/react/tests/v0_8/unit/components/Column.test.tsx index 92f6cc1fb..8b1d41018 100644 --- a/renderers/react/tests/v0_8/unit/components/Column.test.tsx +++ b/renderers/react/tests/v0_8/unit/components/Column.test.tsx @@ -14,10 +14,10 @@ * limitations under the License. */ -import { describe, it, expect } from 'vitest'; -import { render, screen } from '@testing-library/react'; +import {describe, it, expect} from 'vitest'; +import {render, screen} from '@testing-library/react'; import React from 'react'; -import { TestWrapper, TestRenderer, createSurfaceUpdate, createBeginRendering } from '../../utils'; +import {TestWrapper, TestRenderer, createSurfaceUpdate, createBeginRendering} from '../../utils'; import type * as Types from '@a2ui/web_core/types/types'; /** @@ -33,16 +33,19 @@ describe('Column Component', () => { it('should render a section element', () => { const messages: Types.ServerToClientMessage[] = [ createSurfaceUpdate([ - { id: 'text-1', component: { Text: { text: { literalString: 'Item' } , usageHint: 'body' } } }, - { id: 'col-1', component: { Column: { children: { explicitList: ['text-1'] } } } }, + { + id: 'text-1', + component: {Text: {text: {literalString: 'Item'}, usageHint: 'body'}}, + }, + {id: 'col-1', component: {Column: {children: {explicitList: ['text-1']}}}}, ]), createBeginRendering('col-1'), ]; - const { container } = render( + const {container} = render( - + , ); const section = container.querySelector('section'); @@ -52,16 +55,19 @@ describe('Column Component', () => { it('should render with wrapper div', () => { const messages: Types.ServerToClientMessage[] = [ createSurfaceUpdate([ - { id: 'text-1', component: { Text: { text: { literalString: 'Item' } , usageHint: 'body' } } }, - { id: 'col-1', component: { Column: { children: { explicitList: ['text-1'] } } } }, + { + id: 'text-1', + component: {Text: {text: {literalString: 'Item'}, usageHint: 'body'}}, + }, + {id: 'col-1', component: {Column: {children: {explicitList: ['text-1']}}}}, ]), createBeginRendering('col-1'), ]; - const { container } = render( + const {container} = render( - + , ); const wrapper = container.querySelector('.a2ui-column'); @@ -73,9 +79,18 @@ describe('Column Component', () => { it('should render child Text components', () => { const messages: Types.ServerToClientMessage[] = [ createSurfaceUpdate([ - { id: 'text-1', component: { Text: { text: { literalString: 'Row 1' } , usageHint: 'body' } } }, - { id: 'text-2', component: { Text: { text: { literalString: 'Row 2' } , usageHint: 'body' } } }, - { id: 'col-1', component: { Column: { children: { explicitList: ['text-1', 'text-2'] } } } }, + { + id: 'text-1', + component: {Text: {text: {literalString: 'Row 1'}, usageHint: 'body'}}, + }, + { + id: 'text-2', + component: {Text: {text: {literalString: 'Row 2'}, usageHint: 'body'}}, + }, + { + id: 'col-1', + component: {Column: {children: {explicitList: ['text-1', 'text-2']}}}, + }, ]), createBeginRendering('col-1'), ]; @@ -83,7 +98,7 @@ describe('Column Component', () => { render( - + , ); expect(screen.getByText('Row 1')).toBeInTheDocument(); @@ -92,16 +107,14 @@ describe('Column Component', () => { it('should render empty column with empty explicitList', () => { const messages: Types.ServerToClientMessage[] = [ - createSurfaceUpdate([ - { id: 'col-1', component: { Column: { children: { explicitList: [] } } } }, - ]), + createSurfaceUpdate([{id: 'col-1', component: {Column: {children: {explicitList: []}}}}]), createBeginRendering('col-1'), ]; - const { container } = render( + const {container} = render( - + , ); const col = container.querySelector('.a2ui-column'); @@ -113,16 +126,19 @@ describe('Column Component', () => { it('should default to stretch alignment', () => { const messages: Types.ServerToClientMessage[] = [ createSurfaceUpdate([ - { id: 'text-1', component: { Text: { text: { literalString: 'Item' } , usageHint: 'body' } } }, - { id: 'col-1', component: { Column: { children: { explicitList: ['text-1'] } } } }, + { + id: 'text-1', + component: {Text: {text: {literalString: 'Item'}, usageHint: 'body'}}, + }, + {id: 'col-1', component: {Column: {children: {explicitList: ['text-1']}}}}, ]), createBeginRendering('col-1'), ]; - const { container } = render( + const {container} = render( - + , ); const wrapper = container.querySelector('.a2ui-column'); @@ -131,20 +147,26 @@ describe('Column Component', () => { const alignments = ['start', 'center', 'end', 'stretch'] as const; - alignments.forEach((alignment) => { + alignments.forEach(alignment => { it(`should set data-alignment="${alignment}"`, () => { const messages: Types.ServerToClientMessage[] = [ createSurfaceUpdate([ - { id: 'text-1', component: { Text: { text: { literalString: 'Item' } , usageHint: 'body' } } }, - { id: 'col-1', component: { Column: { children: { explicitList: ['text-1'] }, alignment } } }, + { + id: 'text-1', + component: {Text: {text: {literalString: 'Item'}, usageHint: 'body'}}, + }, + { + id: 'col-1', + component: {Column: {children: {explicitList: ['text-1']}, alignment}}, + }, ]), createBeginRendering('col-1'), ]; - const { container } = render( + const {container} = render( - + , ); const wrapper = container.querySelector('.a2ui-column'); @@ -157,38 +179,54 @@ describe('Column Component', () => { it('should default to start distribution', () => { const messages: Types.ServerToClientMessage[] = [ createSurfaceUpdate([ - { id: 'text-1', component: { Text: { text: { literalString: 'Item' } , usageHint: 'body' } } }, - { id: 'col-1', component: { Column: { children: { explicitList: ['text-1'] } } } }, + { + id: 'text-1', + component: {Text: {text: {literalString: 'Item'}, usageHint: 'body'}}, + }, + {id: 'col-1', component: {Column: {children: {explicitList: ['text-1']}}}}, ]), createBeginRendering('col-1'), ]; - const { container } = render( + const {container} = render( - + , ); const wrapper = container.querySelector('.a2ui-column'); expect(wrapper?.getAttribute('data-distribution')).toBe('start'); }); - const distributions = ['start', 'center', 'end', 'spaceBetween', 'spaceAround', 'spaceEvenly'] as const; + const distributions = [ + 'start', + 'center', + 'end', + 'spaceBetween', + 'spaceAround', + 'spaceEvenly', + ] as const; - distributions.forEach((distribution) => { + distributions.forEach(distribution => { it(`should set data-distribution="${distribution}"`, () => { const messages: Types.ServerToClientMessage[] = [ createSurfaceUpdate([ - { id: 'text-1', component: { Text: { text: { literalString: 'Item' } , usageHint: 'body' } } }, - { id: 'col-1', component: { Column: { children: { explicitList: ['text-1'] }, distribution } } }, + { + id: 'text-1', + component: {Text: {text: {literalString: 'Item'}, usageHint: 'body'}}, + }, + { + id: 'col-1', + component: {Column: {children: {explicitList: ['text-1']}, distribution}}, + }, ]), createBeginRendering('col-1'), ]; - const { container } = render( + const {container} = render( - + , ); const wrapper = container.querySelector('.a2ui-column'); @@ -201,17 +239,20 @@ describe('Column Component', () => { it('should render Row inside Column', () => { const messages: Types.ServerToClientMessage[] = [ createSurfaceUpdate([ - { id: 'text-1', component: { Text: { text: { literalString: 'Nested text' } , usageHint: 'body' } } }, - { id: 'row-1', component: { Row: { children: { explicitList: ['text-1'] } } } }, - { id: 'col-1', component: { Column: { children: { explicitList: ['row-1'] } } } }, + { + id: 'text-1', + component: {Text: {text: {literalString: 'Nested text'}, usageHint: 'body'}}, + }, + {id: 'row-1', component: {Row: {children: {explicitList: ['text-1']}}}}, + {id: 'col-1', component: {Column: {children: {explicitList: ['row-1']}}}}, ]), createBeginRendering('col-1'), ]; - const { container } = render( + const {container} = render( - + , ); expect(container.querySelector('.a2ui-column')).toBeInTheDocument(); @@ -224,16 +265,19 @@ describe('Column Component', () => { it('should apply theme classes to section', () => { const messages: Types.ServerToClientMessage[] = [ createSurfaceUpdate([ - { id: 'text-1', component: { Text: { text: { literalString: 'Item' } , usageHint: 'body' } } }, - { id: 'col-1', component: { Column: { children: { explicitList: ['text-1'] } } } }, + { + id: 'text-1', + component: {Text: {text: {literalString: 'Item'}, usageHint: 'body'}}, + }, + {id: 'col-1', component: {Column: {children: {explicitList: ['text-1']}}}}, ]), createBeginRendering('col-1'), ]; - const { container } = render( + const {container} = render( - + , ); const section = container.querySelector('section'); @@ -245,16 +289,19 @@ describe('Column Component', () => { it('should have correct DOM structure', () => { const messages: Types.ServerToClientMessage[] = [ createSurfaceUpdate([ - { id: 'text-1', component: { Text: { text: { literalString: 'Item' } , usageHint: 'body' } } }, - { id: 'col-1', component: { Column: { children: { explicitList: ['text-1'] } } } }, + { + id: 'text-1', + component: {Text: {text: {literalString: 'Item'}, usageHint: 'body'}}, + }, + {id: 'col-1', component: {Column: {children: {explicitList: ['text-1']}}}}, ]), createBeginRendering('col-1'), ]; - const { container } = render( + const {container} = render( - + , ); const wrapper = container.querySelector('.a2ui-column'); diff --git a/renderers/react/tests/v0_8/unit/components/DateTimeInput.test.tsx b/renderers/react/tests/v0_8/unit/components/DateTimeInput.test.tsx index 93ca9850d..b3a1430ad 100644 --- a/renderers/react/tests/v0_8/unit/components/DateTimeInput.test.tsx +++ b/renderers/react/tests/v0_8/unit/components/DateTimeInput.test.tsx @@ -14,10 +14,10 @@ * limitations under the License. */ -import { describe, it, expect } from 'vitest'; -import { render, fireEvent } from '@testing-library/react'; +import {describe, it, expect} from 'vitest'; +import {render, fireEvent} from '@testing-library/react'; import React from 'react'; -import { TestWrapper, TestRenderer, createSimpleMessages } from '../../utils'; +import {TestWrapper, TestRenderer, createSimpleMessages} from '../../utils'; /** * DateTimeInput tests following A2UI specification. @@ -28,13 +28,13 @@ describe('DateTimeInput Component', () => { describe('Basic Rendering', () => { it('should render an input element', () => { const messages = createSimpleMessages('dt-1', 'DateTimeInput', { - value: { literalString: '2024-01-15' }, + value: {literalString: '2024-01-15'}, }); - const { container } = render( + const {container} = render( - + , ); const input = container.querySelector('input'); @@ -43,13 +43,13 @@ describe('DateTimeInput Component', () => { it('should render with wrapper div', () => { const messages = createSimpleMessages('dt-1', 'DateTimeInput', { - value: { literalString: '2024-01-15' }, + value: {literalString: '2024-01-15'}, }); - const { container } = render( + const {container} = render( - + , ); const wrapper = container.querySelector('.a2ui-datetime-input'); @@ -58,13 +58,13 @@ describe('DateTimeInput Component', () => { it('should render with initial value', () => { const messages = createSimpleMessages('dt-1', 'DateTimeInput', { - value: { literalString: '2024-06-20' }, + value: {literalString: '2024-06-20'}, }); - const { container } = render( + const {container} = render( - + , ); const input = container.querySelector('input') as HTMLInputElement; @@ -73,21 +73,21 @@ describe('DateTimeInput Component', () => { it('should render different values for different inputs', () => { const messages1 = createSimpleMessages('dt-1', 'DateTimeInput', { - value: { literalString: '2024-01-01' }, + value: {literalString: '2024-01-01'}, }); const messages2 = createSimpleMessages('dt-2', 'DateTimeInput', { - value: { literalString: '2024-12-31' }, + value: {literalString: '2024-12-31'}, }); - const { container: container1 } = render( + const {container: container1} = render( - + , ); - const { container: container2 } = render( + const {container: container2} = render( - + , ); const input1 = container1.querySelector('input') as HTMLInputElement; @@ -102,13 +102,13 @@ describe('DateTimeInput Component', () => { describe('Input Type', () => { it('should render date input by default (enableDate=true, enableTime=false)', () => { const messages = createSimpleMessages('dt-1', 'DateTimeInput', { - value: { literalString: '2024-01-15' }, + value: {literalString: '2024-01-15'}, }); - const { container } = render( + const {container} = render( - + , ); const input = container.querySelector('input'); @@ -119,15 +119,15 @@ describe('DateTimeInput Component', () => { it('should render date input when enableDate=true', () => { const messages = createSimpleMessages('dt-1', 'DateTimeInput', { - value: { literalString: '2024-01-15' }, + value: {literalString: '2024-01-15'}, enableDate: true, enableTime: false, }); - const { container } = render( + const {container} = render( - + , ); const input = container.querySelector('input'); @@ -136,15 +136,15 @@ describe('DateTimeInput Component', () => { it('should render time input when enableTime=true and enableDate=false', () => { const messages = createSimpleMessages('dt-1', 'DateTimeInput', { - value: { literalString: '14:30' }, + value: {literalString: '14:30'}, enableDate: false, enableTime: true, }); - const { container } = render( + const {container} = render( - + , ); const input = container.querySelector('input'); @@ -154,15 +154,15 @@ describe('DateTimeInput Component', () => { it('should render datetime-local input when both enableDate and enableTime are true', () => { const messages = createSimpleMessages('dt-1', 'DateTimeInput', { - value: { literalString: '2024-01-15T14:30' }, + value: {literalString: '2024-01-15T14:30'}, enableDate: true, enableTime: true, }); - const { container } = render( + const {container} = render( - + , ); const input = container.querySelector('input'); @@ -173,25 +173,25 @@ describe('DateTimeInput Component', () => { it('should render different input types for different configurations', () => { const messagesDate = createSimpleMessages('dt-1', 'DateTimeInput', { - value: { literalString: '2024-01-15' }, + value: {literalString: '2024-01-15'}, enableDate: true, enableTime: false, }); const messagesTime = createSimpleMessages('dt-2', 'DateTimeInput', { - value: { literalString: '14:30' }, + value: {literalString: '14:30'}, enableDate: false, enableTime: true, }); - const { container: containerDate } = render( + const {container: containerDate} = render( - + , ); - const { container: containerTime } = render( + const {container: containerTime} = render( - + , ); const inputDate = containerDate.querySelector('input'); @@ -206,15 +206,15 @@ describe('DateTimeInput Component', () => { describe('Label', () => { it('should render "Date" label for date input', () => { const messages = createSimpleMessages('dt-1', 'DateTimeInput', { - value: { literalString: '2024-01-15' }, + value: {literalString: '2024-01-15'}, enableDate: true, enableTime: false, }); - const { container } = render( + const {container} = render( - + , ); const label = container.querySelector('label'); @@ -223,15 +223,15 @@ describe('DateTimeInput Component', () => { it('should render "Time" label for time input', () => { const messages = createSimpleMessages('dt-1', 'DateTimeInput', { - value: { literalString: '14:30' }, + value: {literalString: '14:30'}, enableDate: false, enableTime: true, }); - const { container } = render( + const {container} = render( - + , ); const label = container.querySelector('label'); @@ -240,15 +240,15 @@ describe('DateTimeInput Component', () => { it('should render "Date & Time" label for datetime input', () => { const messages = createSimpleMessages('dt-1', 'DateTimeInput', { - value: { literalString: '2024-01-15T14:30' }, + value: {literalString: '2024-01-15T14:30'}, enableDate: true, enableTime: true, }); - const { container } = render( + const {container} = render( - + , ); const label = container.querySelector('label'); @@ -257,13 +257,13 @@ describe('DateTimeInput Component', () => { it('should associate label with input via htmlFor', () => { const messages = createSimpleMessages('dt-1', 'DateTimeInput', { - value: { literalString: '2024-01-15' }, + value: {literalString: '2024-01-15'}, }); - const { container } = render( + const {container} = render( - + , ); const label = container.querySelector('label'); @@ -275,19 +275,19 @@ describe('DateTimeInput Component', () => { describe('User Interaction', () => { it('should update value on change', () => { const messages = createSimpleMessages('dt-1', 'DateTimeInput', { - value: { literalString: '2024-01-15' }, + value: {literalString: '2024-01-15'}, }); - const { container } = render( + const {container} = render( - + , ); const input = container.querySelector('input') as HTMLInputElement; expect(input.value).toBe('2024-01-15'); - fireEvent.change(input, { target: { value: '2024-06-20' } }); + fireEvent.change(input, {target: {value: '2024-06-20'}}); expect(input.value).toBe('2024-06-20'); expect(input.value).not.toBe('2024-01-15'); @@ -295,21 +295,21 @@ describe('DateTimeInput Component', () => { it('should update time value on change', () => { const messages = createSimpleMessages('dt-1', 'DateTimeInput', { - value: { literalString: '10:00' }, + value: {literalString: '10:00'}, enableDate: false, enableTime: true, }); - const { container } = render( + const {container} = render( - + , ); const input = container.querySelector('input') as HTMLInputElement; expect(input.value).toBe('10:00'); - fireEvent.change(input, { target: { value: '18:30' } }); + fireEvent.change(input, {target: {value: '18:30'}}); expect(input.value).toBe('18:30'); expect(input.value).not.toBe('10:00'); @@ -319,13 +319,13 @@ describe('DateTimeInput Component', () => { describe('Structure', () => { it('should have correct DOM structure: div > section > label + input', () => { const messages = createSimpleMessages('dt-1', 'DateTimeInput', { - value: { literalString: '2024-01-15' }, + value: {literalString: '2024-01-15'}, }); - const { container } = render( + const {container} = render( - + , ); const wrapper = container.querySelector('.a2ui-datetime-input'); diff --git a/renderers/react/tests/v0_8/unit/components/Divider.test.tsx b/renderers/react/tests/v0_8/unit/components/Divider.test.tsx index 58900b01b..29b956015 100644 --- a/renderers/react/tests/v0_8/unit/components/Divider.test.tsx +++ b/renderers/react/tests/v0_8/unit/components/Divider.test.tsx @@ -14,10 +14,16 @@ * limitations under the License. */ -import { describe, it, expect } from 'vitest'; -import { render } from '@testing-library/react'; +import {describe, it, expect} from 'vitest'; +import {render} from '@testing-library/react'; import React from 'react'; -import { TestWrapper, TestRenderer, createSimpleMessages, createSurfaceUpdate, createBeginRendering } from '../../utils'; +import { + TestWrapper, + TestRenderer, + createSimpleMessages, + createSurfaceUpdate, + createBeginRendering, +} from '../../utils'; import type * as Types from '@a2ui/web_core/types/types'; /** @@ -30,10 +36,10 @@ describe('Divider Component', () => { it('should render an hr element', () => { const messages = createSimpleMessages('div-1', 'Divider', {}); - const { container } = render( + const {container} = render( - + , ); const hr = container.querySelector('hr'); @@ -43,10 +49,10 @@ describe('Divider Component', () => { it('should render with wrapper div', () => { const messages = createSimpleMessages('div-1', 'Divider', {}); - const { container } = render( + const {container} = render( - + , ); const wrapper = container.querySelector('.a2ui-divider'); @@ -56,10 +62,10 @@ describe('Divider Component', () => { it('should render as self-closing element', () => { const messages = createSimpleMessages('div-1', 'Divider', {}); - const { container } = render( + const {container} = render( - + , ); const hr = container.querySelector('hr'); @@ -73,10 +79,10 @@ describe('Divider Component', () => { axis: 'horizontal', }); - const { container } = render( + const {container} = render( - + , ); const hr = container.querySelector('hr'); @@ -88,10 +94,10 @@ describe('Divider Component', () => { axis: 'vertical', }); - const { container } = render( + const {container} = render( - + , ); const hr = container.querySelector('hr'); @@ -103,10 +109,10 @@ describe('Divider Component', () => { it('should render hr element with theme classes applied', () => { const messages = createSimpleMessages('div-1', 'Divider', {}); - const { container } = render( + const {container} = render( - + , ); const hr = container.querySelector('hr'); @@ -119,10 +125,10 @@ describe('Divider Component', () => { it('should have correct DOM structure', () => { const messages = createSimpleMessages('div-1', 'Divider', {}); - const { container } = render( + const {container} = render( - + , ); const wrapper = container.querySelector('.a2ui-divider'); @@ -135,18 +141,21 @@ describe('Divider Component', () => { it('should render multiple dividers in a column', () => { const messages: Types.ServerToClientMessage[] = [ createSurfaceUpdate([ - { id: 'div-1', component: { Divider: {} } }, - { id: 'div-2', component: { Divider: {} } }, - { id: 'div-3', component: { Divider: {} } }, - { id: 'col-1', component: { Column: { children: { explicitList: ['div-1', 'div-2', 'div-3'] } } } }, + {id: 'div-1', component: {Divider: {}}}, + {id: 'div-2', component: {Divider: {}}}, + {id: 'div-3', component: {Divider: {}}}, + { + id: 'col-1', + component: {Column: {children: {explicitList: ['div-1', 'div-2', 'div-3']}}}, + }, ]), createBeginRendering('col-1'), ]; - const { container } = render( + const {container} = render( - + , ); const dividers = container.querySelectorAll('.a2ui-divider'); diff --git a/renderers/react/tests/v0_8/unit/components/Icon.test.tsx b/renderers/react/tests/v0_8/unit/components/Icon.test.tsx index 7b77918ae..0e7b20adc 100644 --- a/renderers/react/tests/v0_8/unit/components/Icon.test.tsx +++ b/renderers/react/tests/v0_8/unit/components/Icon.test.tsx @@ -14,23 +14,23 @@ * limitations under the License. */ -import { describe, it, expect, beforeEach } from 'vitest'; -import { render, screen } from '@testing-library/react'; +import {describe, it, expect, beforeEach} from 'vitest'; +import {render, screen} from '@testing-library/react'; import React from 'react'; -import { TestWrapper, TestRenderer, createSimpleMessages } from '../../utils'; -import { litTheme, defaultTheme } from '../../../../src/v0_8'; +import {TestWrapper, TestRenderer, createSimpleMessages} from '../../utils'; +import {litTheme, defaultTheme} from '../../../../src/v0_8'; describe('Icon Component', () => { describe('Basic Rendering', () => { it('should render an icon with literal name', () => { const messages = createSimpleMessages('icon-1', 'Icon', { - name: { literalString: 'home' }, + name: {literalString: 'home'}, }); - const { container } = render( + const {container} = render( - + , ); // Should render something (Material Symbols span) @@ -41,13 +41,13 @@ describe('Icon Component', () => { it('should render icon with empty string name gracefully', () => { const messages = createSimpleMessages('icon-1', 'Icon', { - name: { literalString: '' }, + name: {literalString: ''}, }); - const { container } = render( + const {container} = render( - + , ); // Should have the surface - empty name returns null (no icon rendered) @@ -57,13 +57,13 @@ describe('Icon Component', () => { it('should render with search icon', () => { const messages = createSimpleMessages('icon-1', 'Icon', { - name: { literalString: 'search' }, + name: {literalString: 'search'}, }); - const { container } = render( + const {container} = render( - + , ); // Check that content was rendered @@ -72,13 +72,13 @@ describe('Icon Component', () => { it('should render with settings icon', () => { const messages = createSimpleMessages('icon-1', 'Icon', { - name: { literalString: 'settings' }, + name: {literalString: 'settings'}, }); - const { container } = render( + const {container} = render( - + , ); expect(container.querySelector('.a2ui-surface')).toBeInTheDocument(); @@ -88,28 +88,28 @@ describe('Icon Component', () => { describe('Icon Name Mapping', () => { // A2UI names are converted to snake_case for Material Symbols const iconMappings = [ - { a2ui: 'home', expected: 'home' }, - { a2ui: 'search', expected: 'search' }, - { a2ui: 'settings', expected: 'settings' }, - { a2ui: 'favorite', expected: 'favorite' }, - { a2ui: 'delete', expected: 'delete' }, - { a2ui: 'shoppingCart', expected: 'shopping_cart' }, - { a2ui: 'accountCircle', expected: 'account_circle' }, - { a2ui: 'notifications', expected: 'notifications' }, - { a2ui: 'mail', expected: 'mail' }, - { a2ui: 'lock', expected: 'lock' }, + {a2ui: 'home', expected: 'home'}, + {a2ui: 'search', expected: 'search'}, + {a2ui: 'settings', expected: 'settings'}, + {a2ui: 'favorite', expected: 'favorite'}, + {a2ui: 'delete', expected: 'delete'}, + {a2ui: 'shoppingCart', expected: 'shopping_cart'}, + {a2ui: 'accountCircle', expected: 'account_circle'}, + {a2ui: 'notifications', expected: 'notifications'}, + {a2ui: 'mail', expected: 'mail'}, + {a2ui: 'lock', expected: 'lock'}, ]; - iconMappings.forEach(({ a2ui, expected }) => { + iconMappings.forEach(({a2ui, expected}) => { it(`should render "${a2ui}" icon as "${expected}"`, () => { const messages = createSimpleMessages('icon-1', 'Icon', { - name: { literalString: a2ui }, + name: {literalString: a2ui}, }); - const { container } = render( + const {container} = render( - + , ); // Should render with snake_case name for Material Symbols @@ -123,13 +123,13 @@ describe('Icon Component', () => { describe('Theme Support', () => { it('should apply default theme classes', () => { const messages = createSimpleMessages('icon-1', 'Icon', { - name: { literalString: 'home' }, + name: {literalString: 'home'}, }); - const { container } = render( + const {container} = render( - + , ); // Default theme (litTheme) has empty Icon classes, so check section is rendered @@ -141,13 +141,13 @@ describe('Icon Component', () => { it('should apply lit theme classes with container/element structure', () => { const messages = createSimpleMessages('icon-1', 'Icon', { - name: { literalString: 'home' }, + name: {literalString: 'home'}, }); - const { container } = render( + const {container} = render( - + , ); // Lit theme uses layout/typography classes for icon styling @@ -159,13 +159,13 @@ describe('Icon Component', () => { describe('Material Symbols Integration', () => { it('should render icon using Material Symbols font', () => { const messages = createSimpleMessages('icon-1', 'Icon', { - name: { literalString: 'home' }, + name: {literalString: 'home'}, }); - const { container } = render( + const {container} = render( - + , ); // Material Symbols uses a span with g-icon class @@ -176,13 +176,13 @@ describe('Icon Component', () => { it('should convert camelCase icon names to snake_case', () => { const messages = createSimpleMessages('icon-1', 'Icon', { - name: { literalString: 'shoppingCart' }, + name: {literalString: 'shoppingCart'}, }); - const { container } = render( + const {container} = render( - + , ); // camelCase should be converted to snake_case for Material Symbols @@ -195,13 +195,13 @@ describe('Icon Component', () => { describe('Unknown Icons', () => { it('should render unknown icon names as-is for Material Symbols', () => { const messages = createSimpleMessages('icon-1', 'Icon', { - name: { literalString: 'unknownIconName12345' }, + name: {literalString: 'unknownIconName12345'}, }); - const { container } = render( + const {container} = render( - + , ); // Material Symbols renders the icon name as text (font handles display) diff --git a/renderers/react/tests/v0_8/unit/components/Image.test.tsx b/renderers/react/tests/v0_8/unit/components/Image.test.tsx index bfa520918..fee86fa3b 100644 --- a/renderers/react/tests/v0_8/unit/components/Image.test.tsx +++ b/renderers/react/tests/v0_8/unit/components/Image.test.tsx @@ -14,23 +14,23 @@ * limitations under the License. */ -import { describe, it, expect } from 'vitest'; -import { render } from '@testing-library/react'; +import {describe, it, expect} from 'vitest'; +import {render} from '@testing-library/react'; import React from 'react'; -import { TestWrapper, TestRenderer, createSimpleMessages } from '../../utils'; +import {TestWrapper, TestRenderer, createSimpleMessages} from '../../utils'; describe('Image Component', () => { describe('Basic Rendering', () => { it('should render an img element', () => { const messages = createSimpleMessages('img-1', 'Image', { - url: { literalString: 'https://example.com/image.jpg' }, + url: {literalString: 'https://example.com/image.jpg'}, usageHint: 'mediumFeature', }); - const { container } = render( + const {container} = render( - + , ); const img = container.querySelector('img'); @@ -39,14 +39,14 @@ describe('Image Component', () => { it('should render with wrapper div', () => { const messages = createSimpleMessages('img-1', 'Image', { - url: { literalString: 'https://example.com/image.jpg' }, + url: {literalString: 'https://example.com/image.jpg'}, usageHint: 'mediumFeature', }); - const { container } = render( + const {container} = render( - + , ); const wrapper = container.querySelector('.a2ui-image'); @@ -55,14 +55,14 @@ describe('Image Component', () => { it('should set src attribute from url', () => { const messages = createSimpleMessages('img-1', 'Image', { - url: { literalString: 'https://example.com/photo.png' }, + url: {literalString: 'https://example.com/photo.png'}, usageHint: 'mediumFeature', }); - const { container } = render( + const {container} = render( - + , ); const img = container.querySelector('img'); @@ -71,14 +71,14 @@ describe('Image Component', () => { it('should have empty alt attribute', () => { const messages = createSimpleMessages('img-1', 'Image', { - url: { literalString: 'https://example.com/image.jpg' }, + url: {literalString: 'https://example.com/image.jpg'}, usageHint: 'mediumFeature', }); - const { container } = render( + const {container} = render( - + , ); const img = container.querySelector('img'); @@ -87,23 +87,23 @@ describe('Image Component', () => { it('should render different src for different url inputs', () => { const messages1 = createSimpleMessages('img-1', 'Image', { - url: { literalString: 'https://example.com/first.jpg' }, + url: {literalString: 'https://example.com/first.jpg'}, usageHint: 'mediumFeature', }); const messages2 = createSimpleMessages('img-2', 'Image', { - url: { literalString: 'https://example.com/second.jpg' }, + url: {literalString: 'https://example.com/second.jpg'}, usageHint: 'mediumFeature', }); - const { container: container1 } = render( + const {container: container1} = render( - + , ); - const { container: container2 } = render( + const {container: container2} = render( - + , ); const img1 = container1.querySelector('img'); @@ -116,14 +116,14 @@ describe('Image Component', () => { it('should return null for empty url', () => { const messages = createSimpleMessages('img-1', 'Image', { - url: { literalString: '' }, + url: {literalString: ''}, usageHint: 'mediumFeature', }); - const { container } = render( + const {container} = render( - + , ); const img = container.querySelector('img'); @@ -132,19 +132,26 @@ describe('Image Component', () => { }); describe('Usage Hints', () => { - const usageHints = ['icon', 'avatar', 'smallFeature', 'mediumFeature', 'largeFeature', 'header']; - - usageHints.forEach((hint) => { + const usageHints = [ + 'icon', + 'avatar', + 'smallFeature', + 'mediumFeature', + 'largeFeature', + 'header', + ]; + + usageHints.forEach(hint => { it(`should render with usageHint="${hint}"`, () => { const messages = createSimpleMessages('img-1', 'Image', { - url: { literalString: 'https://example.com/image.jpg' }, + url: {literalString: 'https://example.com/image.jpg'}, usageHint: hint, }); - const { container } = render( + const {container} = render( - + , ); const img = container.querySelector('img'); @@ -155,23 +162,23 @@ describe('Image Component', () => { it('should apply different theme classes for different usageHints', () => { // usageHint affects which theme classes are merged onto the section element const messagesNoHint = createSimpleMessages('img-1', 'Image', { - url: { literalString: 'https://example.com/image.jpg' }, + url: {literalString: 'https://example.com/image.jpg'}, usageHint: 'mediumFeature', }); const messagesWithHint = createSimpleMessages('img-2', 'Image', { - url: { literalString: 'https://example.com/image.jpg' }, + url: {literalString: 'https://example.com/image.jpg'}, usageHint: 'avatar', }); - const { container: containerNoHint } = render( + const {container: containerNoHint} = render( - + , ); - const { container: containerWithHint } = render( + const {container: containerWithHint} = render( - + , ); const sectionNoHint = containerNoHint.querySelector('section'); @@ -191,14 +198,14 @@ describe('Image Component', () => { describe('Fit Mode', () => { it('should default to fill fit mode', () => { const messages = createSimpleMessages('img-1', 'Image', { - url: { literalString: 'https://example.com/image.jpg' }, + url: {literalString: 'https://example.com/image.jpg'}, usageHint: 'mediumFeature', }); - const { container } = render( + const {container} = render( - + , ); const section = container.querySelector('section'); @@ -207,18 +214,18 @@ describe('Image Component', () => { const fitModes = ['contain', 'cover', 'fill', 'none', 'scale-down']; - fitModes.forEach((fit) => { + fitModes.forEach(fit => { it(`should set --object-fit CSS variable for fit="${fit}"`, () => { const messages = createSimpleMessages('img-1', 'Image', { - url: { literalString: 'https://example.com/image.jpg' }, + url: {literalString: 'https://example.com/image.jpg'}, usageHint: 'mediumFeature', fit, }); - const { container } = render( + const {container} = render( - + , ); const section = container.querySelector('section'); @@ -228,25 +235,25 @@ describe('Image Component', () => { it('should set different --object-fit for different fit inputs', () => { const messagesCover = createSimpleMessages('img-1', 'Image', { - url: { literalString: 'https://example.com/image.jpg' }, + url: {literalString: 'https://example.com/image.jpg'}, usageHint: 'mediumFeature', fit: 'cover', }); const messagesContain = createSimpleMessages('img-2', 'Image', { - url: { literalString: 'https://example.com/image.jpg' }, + url: {literalString: 'https://example.com/image.jpg'}, usageHint: 'mediumFeature', fit: 'contain', }); - const { container: containerCover } = render( + const {container: containerCover} = render( - + , ); - const { container: containerContain } = render( + const {container: containerContain} = render( - + , ); const sectionCover = containerCover.querySelector('section'); @@ -255,7 +262,7 @@ describe('Image Component', () => { expect(sectionCover?.style.getPropertyValue('--object-fit')).toBe('cover'); expect(sectionContain?.style.getPropertyValue('--object-fit')).toBe('contain'); expect(sectionCover?.style.getPropertyValue('--object-fit')).not.toBe( - sectionContain?.style.getPropertyValue('--object-fit') + sectionContain?.style.getPropertyValue('--object-fit'), ); }); }); @@ -263,14 +270,14 @@ describe('Image Component', () => { describe('Theme Support', () => { it('should render within section container', () => { const messages = createSimpleMessages('img-1', 'Image', { - url: { literalString: 'https://example.com/image.jpg' }, + url: {literalString: 'https://example.com/image.jpg'}, usageHint: 'mediumFeature', }); - const { container } = render( + const {container} = render( - + , ); const section = container.querySelector('section'); @@ -280,14 +287,14 @@ describe('Image Component', () => { it('should apply theme classes to section', () => { const messages = createSimpleMessages('img-1', 'Image', { - url: { literalString: 'https://example.com/image.jpg' }, + url: {literalString: 'https://example.com/image.jpg'}, usageHint: 'mediumFeature', }); - const { container } = render( + const {container} = render( - + , ); const section = container.querySelector('section'); @@ -302,14 +309,14 @@ describe('Image Component', () => { describe('Structure', () => { it('should have correct DOM structure', () => { const messages = createSimpleMessages('img-1', 'Image', { - url: { literalString: 'https://example.com/image.jpg' }, + url: {literalString: 'https://example.com/image.jpg'}, usageHint: 'mediumFeature', }); - const { container } = render( + const {container} = render( - + , ); const wrapper = container.querySelector('.a2ui-image'); diff --git a/renderers/react/tests/v0_8/unit/components/List.test.tsx b/renderers/react/tests/v0_8/unit/components/List.test.tsx index 4f04896b2..af91753fa 100644 --- a/renderers/react/tests/v0_8/unit/components/List.test.tsx +++ b/renderers/react/tests/v0_8/unit/components/List.test.tsx @@ -14,10 +14,10 @@ * limitations under the License. */ -import { describe, it, expect } from 'vitest'; -import { render } from '@testing-library/react'; +import {describe, it, expect} from 'vitest'; +import {render} from '@testing-library/react'; import React from 'react'; -import { TestWrapper, TestRenderer, createSurfaceUpdate, createBeginRendering } from '../../utils'; +import {TestWrapper, TestRenderer, createSurfaceUpdate, createBeginRendering} from '../../utils'; import type * as Types from '@a2ui/web_core/types/types'; /** @@ -37,13 +37,13 @@ function createListMessages( direction?: 'vertical' | 'horizontal'; alignment?: 'start' | 'center' | 'end' | 'stretch'; }, - surfaceId = '@default' + surfaceId = '@default', ): Types.ServerToClientMessage[] { const children = props.childIds.map((childId, index) => ({ id: childId, component: { Text: { - text: { literalString: props.childTexts?.[index] ?? `Item ${index + 1}` }, + text: {literalString: props.childTexts?.[index] ?? `Item ${index + 1}`}, usageHint: 'body', }, }, @@ -57,14 +57,14 @@ function createListMessages( id, component: { List: { - children: { explicitList: props.childIds }, + children: {explicitList: props.childIds}, direction: props.direction, alignment: props.alignment, }, }, }, ], - surfaceId + surfaceId, ), createBeginRendering(id, surfaceId), ]; @@ -77,10 +77,10 @@ describe('List Component', () => { childIds: ['item-1', 'item-2'], }); - const { container } = render( + const {container} = render( - + , ); const list = container.querySelector('.a2ui-list'); @@ -92,10 +92,10 @@ describe('List Component', () => { childIds: ['item-1', 'item-2'], }); - const { container } = render( + const {container} = render( - + , ); const section = container.querySelector('.a2ui-list section'); @@ -108,10 +108,10 @@ describe('List Component', () => { childTexts: ['First', 'Second', 'Third'], }); - const { container } = render( + const {container} = render( - + , ); expect(container.textContent).toContain('First'); @@ -124,10 +124,10 @@ describe('List Component', () => { childIds: ['item-1', 'item-2', 'item-3', 'item-4'], }); - const { container } = render( + const {container} = render( - + , ); // Each Text component is wrapped in a2ui-text div @@ -143,15 +143,15 @@ describe('List Component', () => { childIds: ['item-1', 'item-2', 'item-3', 'item-4'], }); - const { container: container2 } = render( + const {container: container2} = render( - + , ); - const { container: container4 } = render( + const {container: container4} = render( - + , ); const items2 = container2.querySelectorAll('.a2ui-text'); @@ -169,10 +169,10 @@ describe('List Component', () => { childIds: ['item-1'], }); - const { container } = render( + const {container} = render( - + , ); const list = container.querySelector('.a2ui-list'); @@ -185,10 +185,10 @@ describe('List Component', () => { direction: 'horizontal', }); - const { container } = render( + const {container} = render( - + , ); const list = container.querySelector('.a2ui-list'); @@ -202,10 +202,10 @@ describe('List Component', () => { direction: 'vertical', }); - const { container } = render( + const {container} = render( - + , ); const list = container.querySelector('.a2ui-list'); @@ -222,15 +222,15 @@ describe('List Component', () => { direction: 'vertical', }); - const { container: containerH } = render( + const {container: containerH} = render( - + , ); - const { container: containerV } = render( + const {container: containerV} = render( - + , ); const listH = containerH.querySelector('.a2ui-list'); @@ -250,7 +250,7 @@ describe('List Component', () => { id: 'list-1', component: { List: { - children: { explicitList: [] }, + children: {explicitList: []}, }, }, }, @@ -258,10 +258,10 @@ describe('List Component', () => { createBeginRendering('list-1'), ]; - const { container } = render( + const {container} = render( - + , ); const list = container.querySelector('.a2ui-list'); @@ -277,10 +277,10 @@ describe('List Component', () => { childIds: ['item-1', 'item-2'], }); - const { container } = render( + const {container} = render( - + , ); const list = container.querySelector('.a2ui-list'); diff --git a/renderers/react/tests/v0_8/unit/components/Modal.test.tsx b/renderers/react/tests/v0_8/unit/components/Modal.test.tsx index ee876ec54..d4d84bc44 100644 --- a/renderers/react/tests/v0_8/unit/components/Modal.test.tsx +++ b/renderers/react/tests/v0_8/unit/components/Modal.test.tsx @@ -14,10 +14,16 @@ * limitations under the License. */ -import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; -import { render, screen, fireEvent, waitFor } from '@testing-library/react'; +import {describe, it, expect, vi, beforeEach, afterEach} from 'vitest'; +import {render, screen, fireEvent, waitFor} from '@testing-library/react'; import React from 'react'; -import { TestWrapper, TestRenderer, createSurfaceUpdate, createBeginRendering, getMockCallArg } from '../../utils'; +import { + TestWrapper, + TestRenderer, + createSurfaceUpdate, + createBeginRendering, + getMockCallArg, +} from '../../utils'; import type * as Types from '@a2ui/web_core/types/types'; /** @@ -37,7 +43,7 @@ function createModalMessages( triggerText: string; contentText: string; }, - surfaceId = '@default' + surfaceId = '@default', ): Types.ServerToClientMessage[] { const triggerId = `${id}-trigger`; const contentId = `${id}-content`; @@ -49,14 +55,14 @@ function createModalMessages( { id: triggerId, component: { - Text: { text: { literalString: props.triggerText }, usageHint: 'body' }, + Text: {text: {literalString: props.triggerText}, usageHint: 'body'}, }, }, // Content component { id: contentId, component: { - Text: { text: { literalString: props.contentText }, usageHint: 'body' }, + Text: {text: {literalString: props.contentText}, usageHint: 'body'}, }, }, // Modal component @@ -70,7 +76,7 @@ function createModalMessages( }, }, ], - surfaceId + surfaceId, ), createBeginRendering(id, surfaceId), ]; @@ -98,10 +104,10 @@ describe('Modal Component', () => { contentText: 'Modal Content', }); - const { container } = render( + const {container} = render( - + , ); const wrapper = container.querySelector('.a2ui-modal'); @@ -118,7 +124,7 @@ describe('Modal Component', () => { render( - + , ); expect(screen.getByText('Click to Open')).toBeInTheDocument(); @@ -133,7 +139,7 @@ describe('Modal Component', () => { render( - + , ); // Modal content should not be visible initially @@ -150,17 +156,17 @@ describe('Modal Component', () => { contentText: 'Content B', }); - const { container: container1 } = render( + const {container: container1} = render( - + , ); - const { unmount } = { unmount: () => {} }; + const {unmount} = {unmount: () => {}}; - const { container: container2 } = render( + const {container: container2} = render( - + , ); expect(container1.textContent).toContain('Trigger A'); @@ -179,7 +185,7 @@ describe('Modal Component', () => { render( - + , ); // Click the trigger @@ -200,7 +206,7 @@ describe('Modal Component', () => { render( - + , ); fireEvent.click(screen.getByText('Open Modal')); @@ -220,7 +226,7 @@ describe('Modal Component', () => { render( - + , ); fireEvent.click(screen.getByText('Open Modal')); @@ -236,10 +242,10 @@ describe('Modal Component', () => { contentText: 'Modal Content', }); - const { container } = render( + const {container} = render( - + , ); fireEvent.click(screen.getByText('Open Modal')); @@ -266,13 +272,13 @@ describe('Modal Component', () => { render( - + , ); fireEvent.click(screen.getByText('Open Modal')); await waitFor(() => { - const closeButton = screen.getByRole('button', { name: /close/i }); + const closeButton = screen.getByRole('button', {name: /close/i}); expect(closeButton).toBeInTheDocument(); }); }); @@ -286,7 +292,7 @@ describe('Modal Component', () => { render( - + , ); // Open modal @@ -297,7 +303,7 @@ describe('Modal Component', () => { }); // Click close button - const closeButton = screen.getByRole('button', { name: /close/i }); + const closeButton = screen.getByRole('button', {name: /close/i}); fireEvent.click(closeButton); // Modal content should be removed @@ -315,7 +321,7 @@ describe('Modal Component', () => { render( - + , ); // Open modal @@ -348,7 +354,7 @@ describe('Modal Component', () => { render( - + , ); // Open modal @@ -374,7 +380,7 @@ describe('Modal Component', () => { render( - + , ); // Open modal @@ -387,7 +393,7 @@ describe('Modal Component', () => { // Press Escape const dialog = document.querySelector('dialog'); if (dialog) { - fireEvent.keyDown(dialog, { key: 'Escape' }); + fireEvent.keyDown(dialog, {key: 'Escape'}); } // Modal should close @@ -402,12 +408,27 @@ describe('Modal Component', () => { const messages: Types.ServerToClientMessage[] = [ createSurfaceUpdate([ // Trigger - { id: 'trigger-text', component: { Text: { text: { literalString: 'Open' } , usageHint: 'body' } } }, + { + id: 'trigger-text', + component: {Text: {text: {literalString: 'Open'}, usageHint: 'body'}}, + }, // Modal content: Column with button - { id: 'modal-title', component: { Text: { text: { literalString: 'Modal Title' }, usageHint: 'h2' } } }, - { id: 'btn-text', component: { Text: { text: { literalString: 'Submit' } , usageHint: 'body' } } }, - { id: 'modal-btn', component: { Button: { child: 'btn-text', action: { name: 'submit' } } } }, - { id: 'modal-content', component: { Column: { children: { explicitList: ['modal-title', 'modal-btn'] } } } }, + { + id: 'modal-title', + component: {Text: {text: {literalString: 'Modal Title'}, usageHint: 'h2'}}, + }, + { + id: 'btn-text', + component: {Text: {text: {literalString: 'Submit'}, usageHint: 'body'}}, + }, + { + id: 'modal-btn', + component: {Button: {child: 'btn-text', action: {name: 'submit'}}}, + }, + { + id: 'modal-content', + component: {Column: {children: {explicitList: ['modal-title', 'modal-btn']}}}, + }, // Modal { id: 'modal-1', @@ -425,7 +446,7 @@ describe('Modal Component', () => { render( - + , ); // Open modal @@ -433,7 +454,7 @@ describe('Modal Component', () => { await waitFor(() => { expect(screen.getByText('Modal Title')).toBeInTheDocument(); - expect(screen.getByRole('button', { name: 'Submit' })).toBeInTheDocument(); + expect(screen.getByRole('button', {name: 'Submit'})).toBeInTheDocument(); }); }); @@ -441,9 +462,18 @@ describe('Modal Component', () => { const mockOnAction = vi.fn(); const messages: Types.ServerToClientMessage[] = [ createSurfaceUpdate([ - { id: 'trigger-text', component: { Text: { text: { literalString: 'Open' } , usageHint: 'body' } } }, - { id: 'btn-text', component: { Text: { text: { literalString: 'Action Button' } , usageHint: 'body' } } }, - { id: 'modal-btn', component: { Button: { child: 'btn-text', action: { name: 'modal-action' } } } }, + { + id: 'trigger-text', + component: {Text: {text: {literalString: 'Open'}, usageHint: 'body'}}, + }, + { + id: 'btn-text', + component: {Text: {text: {literalString: 'Action Button'}, usageHint: 'body'}}, + }, + { + id: 'modal-btn', + component: {Button: {child: 'btn-text', action: {name: 'modal-action'}}}, + }, { id: 'modal-1', component: { @@ -460,18 +490,18 @@ describe('Modal Component', () => { render( - + , ); // Open modal fireEvent.click(screen.getByText('Open')); await waitFor(() => { - expect(screen.getByRole('button', { name: 'Action Button' })).toBeInTheDocument(); + expect(screen.getByRole('button', {name: 'Action Button'})).toBeInTheDocument(); }); // Click the action button inside modal - fireEvent.click(screen.getByRole('button', { name: 'Action Button' })); + fireEvent.click(screen.getByRole('button', {name: 'Action Button'})); expect(mockOnAction).toHaveBeenCalled(); const event = getMockCallArg(mockOnAction, 0); @@ -487,15 +517,15 @@ describe('Modal Component', () => { contentText: 'Modal Content', }); - const { container } = render( + const {container} = render( - + , ); // cursor: pointer is on the section inside the wrapper (entry point) const section = container.querySelector('.a2ui-modal > section'); - expect(section).toHaveStyle({ cursor: 'pointer' }); + expect(section).toHaveStyle({cursor: 'pointer'}); }); }); @@ -509,7 +539,7 @@ describe('Modal Component', () => { render( - + , ); fireEvent.click(screen.getByText('Open Modal')); @@ -529,7 +559,7 @@ describe('Modal Component', () => { render( - + , ); fireEvent.click(screen.getByText('Open Modal')); @@ -549,10 +579,10 @@ describe('Modal Component', () => { contentText: 'Modal Content', }); - const { container } = render( + const {container} = render( - + , ); // Entry point wrapper @@ -576,7 +606,7 @@ describe('Modal Component', () => { render( - + , ); fireEvent.click(screen.getByText('Open Modal')); @@ -610,7 +640,7 @@ describe('Modal Component', () => { render( - + , ); // Open modal @@ -620,7 +650,7 @@ describe('Modal Component', () => { }); // Close modal - const closeButton = screen.getByRole('button', { name: /close/i }); + const closeButton = screen.getByRole('button', {name: /close/i}); fireEvent.click(closeButton); await waitFor(() => { expect(screen.queryByText('Modal Content')).not.toBeInTheDocument(); diff --git a/renderers/react/tests/v0_8/unit/components/MultipleChoice.test.tsx b/renderers/react/tests/v0_8/unit/components/MultipleChoice.test.tsx index 6daa845da..b48373b85 100644 --- a/renderers/react/tests/v0_8/unit/components/MultipleChoice.test.tsx +++ b/renderers/react/tests/v0_8/unit/components/MultipleChoice.test.tsx @@ -14,10 +14,10 @@ * limitations under the License. */ -import { describe, it, expect } from 'vitest'; -import { render, fireEvent } from '@testing-library/react'; +import {describe, it, expect} from 'vitest'; +import {render, fireEvent} from '@testing-library/react'; import React from 'react'; -import { TestWrapper, TestRenderer, createSimpleMessages } from '../../utils'; +import {TestWrapper, TestRenderer, createSimpleMessages} from '../../utils'; /** * MultipleChoice tests following A2UI specification. @@ -30,17 +30,17 @@ describe('MultipleChoice Component', () => { describe('Basic Rendering', () => { it('should render a select element', () => { const messages = createSimpleMessages('mc-1', 'MultipleChoice', { - selections: { path: '/mcSelections' }, + selections: {path: '/mcSelections'}, options: [ - { label: { literalString: 'Option A' }, value: 'a' }, - { label: { literalString: 'Option B' }, value: 'b' }, + {label: {literalString: 'Option A'}, value: 'a'}, + {label: {literalString: 'Option B'}, value: 'b'}, ], }); - const { container } = render( + const {container} = render( - + , ); const select = container.querySelector('select'); @@ -50,16 +50,14 @@ describe('MultipleChoice Component', () => { it('should render with wrapper div having correct class', () => { const messages = createSimpleMessages('mc-1', 'MultipleChoice', { - selections: { path: '/mcSelections' }, - options: [ - { label: { literalString: 'Option A' }, value: 'a' }, - ], + selections: {path: '/mcSelections'}, + options: [{label: {literalString: 'Option A'}, value: 'a'}], }); - const { container } = render( + const {container} = render( - + , ); const wrapper = container.querySelector('.a2ui-multiplechoice'); @@ -69,18 +67,18 @@ describe('MultipleChoice Component', () => { it('should render all option labels', () => { const messages = createSimpleMessages('mc-1', 'MultipleChoice', { - selections: { path: '/mcSelections' }, + selections: {path: '/mcSelections'}, options: [ - { label: { literalString: 'First Option' }, value: 'first' }, - { label: { literalString: 'Second Option' }, value: 'second' }, - { label: { literalString: 'Third Option' }, value: 'third' }, + {label: {literalString: 'First Option'}, value: 'first'}, + {label: {literalString: 'Second Option'}, value: 'second'}, + {label: {literalString: 'Third Option'}, value: 'third'}, ], }); - const { container } = render( + const {container} = render( - + , ); const options = container.querySelectorAll('option'); @@ -92,18 +90,18 @@ describe('MultipleChoice Component', () => { it('should render correct number of options', () => { const messages = createSimpleMessages('mc-1', 'MultipleChoice', { - selections: { path: '/mcSelections' }, + selections: {path: '/mcSelections'}, options: [ - { label: { literalString: 'A' }, value: 'a' }, - { label: { literalString: 'B' }, value: 'b' }, - { label: { literalString: 'C' }, value: 'c' }, + {label: {literalString: 'A'}, value: 'a'}, + {label: {literalString: 'B'}, value: 'b'}, + {label: {literalString: 'C'}, value: 'c'}, ], }); - const { container } = render( + const {container} = render( - + , ); const options = container.querySelectorAll('option'); @@ -112,28 +110,26 @@ describe('MultipleChoice Component', () => { it('should render different options for different inputs', () => { const messages1 = createSimpleMessages('mc-1', 'MultipleChoice', { - selections: { path: '/mcSelections1' }, - options: [ - { label: { literalString: 'Alpha' }, value: 'alpha' }, - ], + selections: {path: '/mcSelections1'}, + options: [{label: {literalString: 'Alpha'}, value: 'alpha'}], }); const messages2 = createSimpleMessages('mc-2', 'MultipleChoice', { - selections: { path: '/mcSelections2' }, + selections: {path: '/mcSelections2'}, options: [ - { label: { literalString: 'Beta' }, value: 'beta' }, - { label: { literalString: 'Gamma' }, value: 'gamma' }, + {label: {literalString: 'Beta'}, value: 'beta'}, + {label: {literalString: 'Gamma'}, value: 'gamma'}, ], }); - const { container: container1 } = render( + const {container: container1} = render( - + , ); - const { container: container2 } = render( + const {container: container2} = render( - + , ); expect(container1.textContent).toContain('Alpha'); @@ -147,16 +143,14 @@ describe('MultipleChoice Component', () => { describe('Description Label', () => { it('should render default description when not provided', () => { const messages = createSimpleMessages('mc-1', 'MultipleChoice', { - selections: { path: '/mcSelections' }, - options: [ - { label: { literalString: 'Option A' }, value: 'a' }, - ], + selections: {path: '/mcSelections'}, + options: [{label: {literalString: 'Option A'}, value: 'a'}], }); - const { container } = render( + const {container} = render( - + , ); const label = container.querySelector('label'); @@ -166,16 +160,14 @@ describe('MultipleChoice Component', () => { it('should associate label with select via htmlFor', () => { const messages = createSimpleMessages('mc-1', 'MultipleChoice', { - selections: { path: '/mcSelections' }, - options: [ - { label: { literalString: 'Option A' }, value: 'a' }, - ], + selections: {path: '/mcSelections'}, + options: [{label: {literalString: 'Option A'}, value: 'a'}], }); - const { container } = render( + const {container} = render( - + , ); const label = container.querySelector('label'); @@ -187,18 +179,18 @@ describe('MultipleChoice Component', () => { describe('Option Values', () => { it('should set correct value attributes on options', () => { const messages = createSimpleMessages('mc-1', 'MultipleChoice', { - selections: { path: '/mcSelections' }, + selections: {path: '/mcSelections'}, options: [ - { label: { literalString: 'Small' }, value: 'sm' }, - { label: { literalString: 'Medium' }, value: 'md' }, - { label: { literalString: 'Large' }, value: 'lg' }, + {label: {literalString: 'Small'}, value: 'sm'}, + {label: {literalString: 'Medium'}, value: 'md'}, + {label: {literalString: 'Large'}, value: 'lg'}, ], }); - const { container } = render( + const {container} = render( - + , ); const options = container.querySelectorAll('option'); @@ -211,51 +203,51 @@ describe('MultipleChoice Component', () => { describe('User Interaction', () => { it('should update select value on change', () => { const messages = createSimpleMessages('mc-1', 'MultipleChoice', { - selections: { path: '/mcSelections' }, + selections: {path: '/mcSelections'}, options: [ - { label: { literalString: 'Option A' }, value: 'a' }, - { label: { literalString: 'Option B' }, value: 'b' }, - { label: { literalString: 'Option C' }, value: 'c' }, + {label: {literalString: 'Option A'}, value: 'a'}, + {label: {literalString: 'Option B'}, value: 'b'}, + {label: {literalString: 'Option C'}, value: 'c'}, ], }); - const { container } = render( + const {container} = render( - + , ); const select = container.querySelector('select') as HTMLSelectElement; - fireEvent.change(select, { target: { value: 'b' } }); + fireEvent.change(select, {target: {value: 'b'}}); expect(select.value).toBe('b'); }); it('should handle multiple sequential changes', () => { const messages = createSimpleMessages('mc-1', 'MultipleChoice', { - selections: { path: '/mcSelections' }, + selections: {path: '/mcSelections'}, options: [ - { label: { literalString: 'Option A' }, value: 'a' }, - { label: { literalString: 'Option B' }, value: 'b' }, - { label: { literalString: 'Option C' }, value: 'c' }, + {label: {literalString: 'Option A'}, value: 'a'}, + {label: {literalString: 'Option B'}, value: 'b'}, + {label: {literalString: 'Option C'}, value: 'c'}, ], }); - const { container } = render( + const {container} = render( - + , ); const select = container.querySelector('select') as HTMLSelectElement; - fireEvent.change(select, { target: { value: 'a' } }); + fireEvent.change(select, {target: {value: 'a'}}); expect(select.value).toBe('a'); - fireEvent.change(select, { target: { value: 'c' } }); + fireEvent.change(select, {target: {value: 'c'}}); expect(select.value).toBe('c'); - fireEvent.change(select, { target: { value: 'b' } }); + fireEvent.change(select, {target: {value: 'b'}}); expect(select.value).toBe('b'); }); }); @@ -263,17 +255,17 @@ describe('MultipleChoice Component', () => { describe('Structure', () => { it('should have correct DOM structure: div > section > label + select', () => { const messages = createSimpleMessages('mc-1', 'MultipleChoice', { - selections: { path: '/mcSelections' }, + selections: {path: '/mcSelections'}, options: [ - { label: { literalString: 'Option A' }, value: 'a' }, - { label: { literalString: 'Option B' }, value: 'b' }, + {label: {literalString: 'Option A'}, value: 'a'}, + {label: {literalString: 'Option B'}, value: 'b'}, ], }); - const { container } = render( + const {container} = render( - + , ); const wrapper = container.querySelector('.a2ui-multiplechoice'); @@ -290,16 +282,14 @@ describe('MultipleChoice Component', () => { it('should have select inside section container', () => { const messages = createSimpleMessages('mc-1', 'MultipleChoice', { - selections: { path: '/mcSelections' }, - options: [ - { label: { literalString: 'Option A' }, value: 'a' }, - ], + selections: {path: '/mcSelections'}, + options: [{label: {literalString: 'Option A'}, value: 'a'}], }); - const { container } = render( + const {container} = render( - + , ); const section = container.querySelector('section'); @@ -309,17 +299,17 @@ describe('MultipleChoice Component', () => { it('should have options inside select', () => { const messages = createSimpleMessages('mc-1', 'MultipleChoice', { - selections: { path: '/mcSelections' }, + selections: {path: '/mcSelections'}, options: [ - { label: { literalString: 'Option A' }, value: 'a' }, - { label: { literalString: 'Option B' }, value: 'b' }, + {label: {literalString: 'Option A'}, value: 'a'}, + {label: {literalString: 'Option B'}, value: 'b'}, ], }); - const { container } = render( + const {container} = render( - + , ); const select = container.querySelector('select'); diff --git a/renderers/react/tests/v0_8/unit/components/Row.test.tsx b/renderers/react/tests/v0_8/unit/components/Row.test.tsx index 4a5152f9d..3e6029cb6 100644 --- a/renderers/react/tests/v0_8/unit/components/Row.test.tsx +++ b/renderers/react/tests/v0_8/unit/components/Row.test.tsx @@ -14,10 +14,10 @@ * limitations under the License. */ -import { describe, it, expect } from 'vitest'; -import { render, screen } from '@testing-library/react'; +import {describe, it, expect} from 'vitest'; +import {render, screen} from '@testing-library/react'; import React from 'react'; -import { TestWrapper, TestRenderer, createSurfaceUpdate, createBeginRendering } from '../../utils'; +import {TestWrapper, TestRenderer, createSurfaceUpdate, createBeginRendering} from '../../utils'; import type * as Types from '@a2ui/web_core/types/types'; /** @@ -33,16 +33,19 @@ describe('Row Component', () => { it('should render a section element', () => { const messages: Types.ServerToClientMessage[] = [ createSurfaceUpdate([ - { id: 'text-1', component: { Text: { text: { literalString: 'Item' } , usageHint: 'body' } } }, - { id: 'row-1', component: { Row: { children: { explicitList: ['text-1'] } } } }, + { + id: 'text-1', + component: {Text: {text: {literalString: 'Item'}, usageHint: 'body'}}, + }, + {id: 'row-1', component: {Row: {children: {explicitList: ['text-1']}}}}, ]), createBeginRendering('row-1'), ]; - const { container } = render( + const {container} = render( - + , ); const section = container.querySelector('section'); @@ -52,16 +55,19 @@ describe('Row Component', () => { it('should render with wrapper div', () => { const messages: Types.ServerToClientMessage[] = [ createSurfaceUpdate([ - { id: 'text-1', component: { Text: { text: { literalString: 'Item' } , usageHint: 'body' } } }, - { id: 'row-1', component: { Row: { children: { explicitList: ['text-1'] } } } }, + { + id: 'text-1', + component: {Text: {text: {literalString: 'Item'}, usageHint: 'body'}}, + }, + {id: 'row-1', component: {Row: {children: {explicitList: ['text-1']}}}}, ]), createBeginRendering('row-1'), ]; - const { container } = render( + const {container} = render( - + , ); const wrapper = container.querySelector('.a2ui-row'); @@ -73,9 +79,15 @@ describe('Row Component', () => { it('should render child Text components', () => { const messages: Types.ServerToClientMessage[] = [ createSurfaceUpdate([ - { id: 'text-1', component: { Text: { text: { literalString: 'Item 1' } , usageHint: 'body' } } }, - { id: 'text-2', component: { Text: { text: { literalString: 'Item 2' } , usageHint: 'body' } } }, - { id: 'row-1', component: { Row: { children: { explicitList: ['text-1', 'text-2'] } } } }, + { + id: 'text-1', + component: {Text: {text: {literalString: 'Item 1'}, usageHint: 'body'}}, + }, + { + id: 'text-2', + component: {Text: {text: {literalString: 'Item 2'}, usageHint: 'body'}}, + }, + {id: 'row-1', component: {Row: {children: {explicitList: ['text-1', 'text-2']}}}}, ]), createBeginRendering('row-1'), ]; @@ -83,7 +95,7 @@ describe('Row Component', () => { render( - + , ); expect(screen.getByText('Item 1')).toBeInTheDocument(); @@ -92,16 +104,14 @@ describe('Row Component', () => { it('should render empty row with empty explicitList', () => { const messages: Types.ServerToClientMessage[] = [ - createSurfaceUpdate([ - { id: 'row-1', component: { Row: { children: { explicitList: [] } } } }, - ]), + createSurfaceUpdate([{id: 'row-1', component: {Row: {children: {explicitList: []}}}}]), createBeginRendering('row-1'), ]; - const { container } = render( + const {container} = render( - + , ); const row = container.querySelector('.a2ui-row'); @@ -113,16 +123,19 @@ describe('Row Component', () => { it('should default to stretch alignment', () => { const messages: Types.ServerToClientMessage[] = [ createSurfaceUpdate([ - { id: 'text-1', component: { Text: { text: { literalString: 'Item' } , usageHint: 'body' } } }, - { id: 'row-1', component: { Row: { children: { explicitList: ['text-1'] } } } }, + { + id: 'text-1', + component: {Text: {text: {literalString: 'Item'}, usageHint: 'body'}}, + }, + {id: 'row-1', component: {Row: {children: {explicitList: ['text-1']}}}}, ]), createBeginRendering('row-1'), ]; - const { container } = render( + const {container} = render( - + , ); const wrapper = container.querySelector('.a2ui-row'); @@ -131,20 +144,26 @@ describe('Row Component', () => { const alignments = ['start', 'center', 'end', 'stretch'] as const; - alignments.forEach((alignment) => { + alignments.forEach(alignment => { it(`should set data-alignment="${alignment}"`, () => { const messages: Types.ServerToClientMessage[] = [ createSurfaceUpdate([ - { id: 'text-1', component: { Text: { text: { literalString: 'Item' } , usageHint: 'body' } } }, - { id: 'row-1', component: { Row: { children: { explicitList: ['text-1'] }, alignment } } }, + { + id: 'text-1', + component: {Text: {text: {literalString: 'Item'}, usageHint: 'body'}}, + }, + { + id: 'row-1', + component: {Row: {children: {explicitList: ['text-1']}, alignment}}, + }, ]), createBeginRendering('row-1'), ]; - const { container } = render( + const {container} = render( - + , ); const wrapper = container.querySelector('.a2ui-row'); @@ -157,38 +176,54 @@ describe('Row Component', () => { it('should default to start distribution', () => { const messages: Types.ServerToClientMessage[] = [ createSurfaceUpdate([ - { id: 'text-1', component: { Text: { text: { literalString: 'Item' } , usageHint: 'body' } } }, - { id: 'row-1', component: { Row: { children: { explicitList: ['text-1'] } } } }, + { + id: 'text-1', + component: {Text: {text: {literalString: 'Item'}, usageHint: 'body'}}, + }, + {id: 'row-1', component: {Row: {children: {explicitList: ['text-1']}}}}, ]), createBeginRendering('row-1'), ]; - const { container } = render( + const {container} = render( - + , ); const wrapper = container.querySelector('.a2ui-row'); expect(wrapper?.getAttribute('data-distribution')).toBe('start'); }); - const distributions = ['start', 'center', 'end', 'spaceBetween', 'spaceAround', 'spaceEvenly'] as const; + const distributions = [ + 'start', + 'center', + 'end', + 'spaceBetween', + 'spaceAround', + 'spaceEvenly', + ] as const; - distributions.forEach((distribution) => { + distributions.forEach(distribution => { it(`should set data-distribution="${distribution}"`, () => { const messages: Types.ServerToClientMessage[] = [ createSurfaceUpdate([ - { id: 'text-1', component: { Text: { text: { literalString: 'Item' } , usageHint: 'body' } } }, - { id: 'row-1', component: { Row: { children: { explicitList: ['text-1'] }, distribution } } }, + { + id: 'text-1', + component: {Text: {text: {literalString: 'Item'}, usageHint: 'body'}}, + }, + { + id: 'row-1', + component: {Row: {children: {explicitList: ['text-1']}, distribution}}, + }, ]), createBeginRendering('row-1'), ]; - const { container } = render( + const {container} = render( - + , ); const wrapper = container.querySelector('.a2ui-row'); @@ -201,11 +236,23 @@ describe('Row Component', () => { it('should render multiple Buttons in Row', () => { const messages: Types.ServerToClientMessage[] = [ createSurfaceUpdate([ - { id: 'btn1-text', component: { Text: { text: { literalString: 'Button 1' } , usageHint: 'body' } } }, - { id: 'btn2-text', component: { Text: { text: { literalString: 'Button 2' } , usageHint: 'body' } } }, - { id: 'btn-1', component: { Button: { child: 'btn1-text', action: { name: 'action1' } } } }, - { id: 'btn-2', component: { Button: { child: 'btn2-text', action: { name: 'action2' } } } }, - { id: 'row-1', component: { Row: { children: { explicitList: ['btn-1', 'btn-2'] } } } }, + { + id: 'btn1-text', + component: {Text: {text: {literalString: 'Button 1'}, usageHint: 'body'}}, + }, + { + id: 'btn2-text', + component: {Text: {text: {literalString: 'Button 2'}, usageHint: 'body'}}, + }, + { + id: 'btn-1', + component: {Button: {child: 'btn1-text', action: {name: 'action1'}}}, + }, + { + id: 'btn-2', + component: {Button: {child: 'btn2-text', action: {name: 'action2'}}}, + }, + {id: 'row-1', component: {Row: {children: {explicitList: ['btn-1', 'btn-2']}}}}, ]), createBeginRendering('row-1'), ]; @@ -213,7 +260,7 @@ describe('Row Component', () => { render( - + , ); const buttons = screen.getAllByRole('button'); @@ -227,16 +274,19 @@ describe('Row Component', () => { it('should apply theme classes to section', () => { const messages: Types.ServerToClientMessage[] = [ createSurfaceUpdate([ - { id: 'text-1', component: { Text: { text: { literalString: 'Item' } , usageHint: 'body' } } }, - { id: 'row-1', component: { Row: { children: { explicitList: ['text-1'] } } } }, + { + id: 'text-1', + component: {Text: {text: {literalString: 'Item'}, usageHint: 'body'}}, + }, + {id: 'row-1', component: {Row: {children: {explicitList: ['text-1']}}}}, ]), createBeginRendering('row-1'), ]; - const { container } = render( + const {container} = render( - + , ); const section = container.querySelector('section'); @@ -251,28 +301,40 @@ describe('Row Component', () => { it('should render different alignment for different alignment inputs', () => { const messagesStart: Types.ServerToClientMessage[] = [ createSurfaceUpdate([ - { id: 'text-1', component: { Text: { text: { literalString: 'Item' } , usageHint: 'body' } } }, - { id: 'row-1', component: { Row: { children: { explicitList: ['text-1'] }, alignment: 'start' } } }, + { + id: 'text-1', + component: {Text: {text: {literalString: 'Item'}, usageHint: 'body'}}, + }, + { + id: 'row-1', + component: {Row: {children: {explicitList: ['text-1']}, alignment: 'start'}}, + }, ]), createBeginRendering('row-1'), ]; const messagesEnd: Types.ServerToClientMessage[] = [ createSurfaceUpdate([ - { id: 'text-1', component: { Text: { text: { literalString: 'Item' } , usageHint: 'body' } } }, - { id: 'row-1', component: { Row: { children: { explicitList: ['text-1'] }, alignment: 'end' } } }, + { + id: 'text-1', + component: {Text: {text: {literalString: 'Item'}, usageHint: 'body'}}, + }, + { + id: 'row-1', + component: {Row: {children: {explicitList: ['text-1']}, alignment: 'end'}}, + }, ]), createBeginRendering('row-1'), ]; - const { container: containerStart } = render( + const {container: containerStart} = render( - + , ); - const { container: containerEnd } = render( + const {container: containerEnd} = render( - + , ); const rowStart = containerStart.querySelector('.a2ui-row'); @@ -280,34 +342,50 @@ describe('Row Component', () => { expect(rowStart?.getAttribute('data-alignment')).toBe('start'); expect(rowEnd?.getAttribute('data-alignment')).toBe('end'); - expect(rowStart?.getAttribute('data-alignment')).not.toBe(rowEnd?.getAttribute('data-alignment')); + expect(rowStart?.getAttribute('data-alignment')).not.toBe( + rowEnd?.getAttribute('data-alignment'), + ); }); it('should render different distribution for different distribution inputs', () => { const messagesCenter: Types.ServerToClientMessage[] = [ createSurfaceUpdate([ - { id: 'text-1', component: { Text: { text: { literalString: 'Item' } , usageHint: 'body' } } }, - { id: 'row-1', component: { Row: { children: { explicitList: ['text-1'] }, distribution: 'center' } } }, + { + id: 'text-1', + component: {Text: {text: {literalString: 'Item'}, usageHint: 'body'}}, + }, + { + id: 'row-1', + component: {Row: {children: {explicitList: ['text-1']}, distribution: 'center'}}, + }, ]), createBeginRendering('row-1'), ]; const messagesSpaceBetween: Types.ServerToClientMessage[] = [ createSurfaceUpdate([ - { id: 'text-1', component: { Text: { text: { literalString: 'Item' } , usageHint: 'body' } } }, - { id: 'row-1', component: { Row: { children: { explicitList: ['text-1'] }, distribution: 'spaceBetween' } } }, + { + id: 'text-1', + component: {Text: {text: {literalString: 'Item'}, usageHint: 'body'}}, + }, + { + id: 'row-1', + component: { + Row: {children: {explicitList: ['text-1']}, distribution: 'spaceBetween'}, + }, + }, ]), createBeginRendering('row-1'), ]; - const { container: containerCenter } = render( + const {container: containerCenter} = render( - + , ); - const { container: containerSpaceBetween } = render( + const {container: containerSpaceBetween} = render( - + , ); const rowCenter = containerCenter.querySelector('.a2ui-row'); @@ -315,7 +393,9 @@ describe('Row Component', () => { expect(rowCenter?.getAttribute('data-distribution')).toBe('center'); expect(rowSpaceBetween?.getAttribute('data-distribution')).toBe('spaceBetween'); - expect(rowCenter?.getAttribute('data-distribution')).not.toBe(rowSpaceBetween?.getAttribute('data-distribution')); + expect(rowCenter?.getAttribute('data-distribution')).not.toBe( + rowSpaceBetween?.getAttribute('data-distribution'), + ); }); }); @@ -323,16 +403,19 @@ describe('Row Component', () => { it('should have correct DOM structure', () => { const messages: Types.ServerToClientMessage[] = [ createSurfaceUpdate([ - { id: 'text-1', component: { Text: { text: { literalString: 'Item' } , usageHint: 'body' } } }, - { id: 'row-1', component: { Row: { children: { explicitList: ['text-1'] } } } }, + { + id: 'text-1', + component: {Text: {text: {literalString: 'Item'}, usageHint: 'body'}}, + }, + {id: 'row-1', component: {Row: {children: {explicitList: ['text-1']}}}}, ]), createBeginRendering('row-1'), ]; - const { container } = render( + const {container} = render( - + , ); const wrapper = container.querySelector('.a2ui-row'); diff --git a/renderers/react/tests/v0_8/unit/components/Slider.test.tsx b/renderers/react/tests/v0_8/unit/components/Slider.test.tsx index 6b388a72b..be20ffa39 100644 --- a/renderers/react/tests/v0_8/unit/components/Slider.test.tsx +++ b/renderers/react/tests/v0_8/unit/components/Slider.test.tsx @@ -14,10 +14,10 @@ * limitations under the License. */ -import { describe, it, expect } from 'vitest'; -import { render, fireEvent } from '@testing-library/react'; +import {describe, it, expect} from 'vitest'; +import {render, fireEvent} from '@testing-library/react'; import React from 'react'; -import { TestWrapper, TestRenderer, createSimpleMessages } from '../../utils'; +import {TestWrapper, TestRenderer, createSimpleMessages} from '../../utils'; /** * Slider tests following A2UI specification. @@ -28,13 +28,13 @@ describe('Slider Component', () => { describe('Basic Rendering', () => { it('should render a range input', () => { const messages = createSimpleMessages('sl-1', 'Slider', { - value: { literalNumber: 50 }, + value: {literalNumber: 50}, }); - const { container } = render( + const {container} = render( - + , ); const input = container.querySelector('input[type="range"]'); @@ -45,13 +45,13 @@ describe('Slider Component', () => { it('should render with wrapper div having correct class', () => { const messages = createSimpleMessages('sl-1', 'Slider', { - value: { literalNumber: 50 }, + value: {literalNumber: 50}, }); - const { container } = render( + const {container} = render( - + , ); const wrapper = container.querySelector('.a2ui-slider'); @@ -61,15 +61,15 @@ describe('Slider Component', () => { it('should render with exact initial value', () => { const messages = createSimpleMessages('sl-1', 'Slider', { - value: { literalNumber: 75 }, + value: {literalNumber: 75}, minValue: 0, maxValue: 100, }); - const { container } = render( + const {container} = render( - + , ); const input = container.querySelector('input[type="range"]') as HTMLInputElement; @@ -81,15 +81,15 @@ describe('Slider Component', () => { it('should display current value in span matching input value', () => { const messages = createSimpleMessages('sl-1', 'Slider', { - value: { literalNumber: 42 }, + value: {literalNumber: 42}, minValue: 0, maxValue: 100, }); - const { container } = render( + const {container} = render( - + , ); const input = container.querySelector('input[type="range"]') as HTMLInputElement; @@ -102,25 +102,25 @@ describe('Slider Component', () => { it('should render different values for different inputs', () => { const messages1 = createSimpleMessages('sl-1', 'Slider', { - value: { literalNumber: 25 }, + value: {literalNumber: 25}, minValue: 0, maxValue: 100, }); const messages2 = createSimpleMessages('sl-2', 'Slider', { - value: { literalNumber: 75 }, + value: {literalNumber: 75}, minValue: 0, maxValue: 100, }); - const { container: container1 } = render( + const {container: container1} = render( - + , ); - const { container: container2 } = render( + const {container: container2} = render( - + , ); const input1 = container1.querySelector('input[type="range"]') as HTMLInputElement; @@ -135,15 +135,15 @@ describe('Slider Component', () => { describe('Min/Max Values', () => { it('should set exact min attribute value', () => { const messages = createSimpleMessages('sl-1', 'Slider', { - value: { literalNumber: 50 }, + value: {literalNumber: 50}, minValue: 10, maxValue: 100, }); - const { container } = render( + const {container} = render( - + , ); const input = container.querySelector('input[type="range"]') as HTMLInputElement; @@ -153,15 +153,15 @@ describe('Slider Component', () => { it('should set exact max attribute value', () => { const messages = createSimpleMessages('sl-1', 'Slider', { - value: { literalNumber: 50 }, + value: {literalNumber: 50}, minValue: 0, maxValue: 200, }); - const { container } = render( + const {container} = render( - + , ); const input = container.querySelector('input[type="range"]') as HTMLInputElement; @@ -171,13 +171,13 @@ describe('Slider Component', () => { it('should default min/max to 0 when not provided', () => { const messages = createSimpleMessages('sl-1', 'Slider', { - value: { literalNumber: 0 }, + value: {literalNumber: 0}, }); - const { container } = render( + const {container} = render( - + , ); const input = container.querySelector('input[type="range"]') as HTMLInputElement; @@ -187,15 +187,15 @@ describe('Slider Component', () => { it('should handle negative min values', () => { const messages = createSimpleMessages('sl-1', 'Slider', { - value: { literalNumber: 0 }, + value: {literalNumber: 0}, minValue: -100, maxValue: 100, }); - const { container } = render( + const {container} = render( - + , ); const input = container.querySelector('input[type="range"]') as HTMLInputElement; @@ -206,21 +206,21 @@ describe('Slider Component', () => { describe('User Interaction', () => { it('should update input value on change', () => { const messages = createSimpleMessages('sl-1', 'Slider', { - value: { literalNumber: 50 }, + value: {literalNumber: 50}, minValue: 0, maxValue: 100, }); - const { container } = render( + const {container} = render( - + , ); const input = container.querySelector('input[type="range"]') as HTMLInputElement; expect(input.value).toBe('50'); // Initial value - fireEvent.change(input, { target: { value: '80' } }); + fireEvent.change(input, {target: {value: '80'}}); expect(input.value).toBe('80'); // New value expect(input.value).not.toBe('50'); // Not old value @@ -228,15 +228,15 @@ describe('Slider Component', () => { it('should update displayed span value in sync with input', () => { const messages = createSimpleMessages('sl-1', 'Slider', { - value: { literalNumber: 50 }, + value: {literalNumber: 50}, minValue: 0, maxValue: 100, }); - const { container } = render( + const {container} = render( - + , ); const input = container.querySelector('input[type="range"]') as HTMLInputElement; @@ -244,7 +244,7 @@ describe('Slider Component', () => { expect(span?.textContent).toBe('50'); // Initial - fireEvent.change(input, { target: { value: '25' } }); + fireEvent.change(input, {target: {value: '25'}}); // Both should update together expect(input.value).toBe('25'); @@ -253,29 +253,29 @@ describe('Slider Component', () => { it('should handle multiple sequential value changes', () => { const messages = createSimpleMessages('sl-1', 'Slider', { - value: { literalNumber: 50 }, + value: {literalNumber: 50}, minValue: 0, maxValue: 100, }); - const { container } = render( + const {container} = render( - + , ); const input = container.querySelector('input[type="range"]') as HTMLInputElement; const span = container.querySelector('section span'); - fireEvent.change(input, { target: { value: '10' } }); + fireEvent.change(input, {target: {value: '10'}}); expect(input.value).toBe('10'); expect(span?.textContent).toBe('10'); - fireEvent.change(input, { target: { value: '90' } }); + fireEvent.change(input, {target: {value: '90'}}); expect(input.value).toBe('90'); expect(span?.textContent).toBe('90'); - fireEvent.change(input, { target: { value: '50' } }); + fireEvent.change(input, {target: {value: '50'}}); expect(input.value).toBe('50'); expect(span?.textContent).toBe('50'); }); @@ -284,16 +284,16 @@ describe('Slider Component', () => { describe('Structure', () => { it('should have correct DOM structure: label, input, span in order', () => { const messages = createSimpleMessages('sl-1', 'Slider', { - value: { literalNumber: 50 }, - label: { literalString: 'Volume' }, + value: {literalNumber: 50}, + label: {literalString: 'Volume'}, minValue: 0, maxValue: 100, }); - const { container } = render( + const {container} = render( - + , ); const section = container.querySelector('section'); @@ -308,13 +308,13 @@ describe('Slider Component', () => { it('should omit label from DOM structure when not provided', () => { const messages = createSimpleMessages('sl-1', 'Slider', { - value: { literalNumber: 50 }, + value: {literalNumber: 50}, }); - const { container } = render( + const {container} = render( - + , ); const section = container.querySelector('section'); @@ -328,13 +328,13 @@ describe('Slider Component', () => { it('should have input inside section container', () => { const messages = createSimpleMessages('sl-1', 'Slider', { - value: { literalNumber: 50 }, + value: {literalNumber: 50}, }); - const { container } = render( + const {container} = render( - + , ); const section = container.querySelector('section'); diff --git a/renderers/react/tests/v0_8/unit/components/Tabs.test.tsx b/renderers/react/tests/v0_8/unit/components/Tabs.test.tsx index e5e49d2b8..647f3e856 100644 --- a/renderers/react/tests/v0_8/unit/components/Tabs.test.tsx +++ b/renderers/react/tests/v0_8/unit/components/Tabs.test.tsx @@ -14,10 +14,10 @@ * limitations under the License. */ -import { describe, it, expect } from 'vitest'; -import { render, screen, fireEvent } from '@testing-library/react'; +import {describe, it, expect} from 'vitest'; +import {render, screen, fireEvent} from '@testing-library/react'; import React from 'react'; -import { TestWrapper, TestRenderer, createSurfaceUpdate, createBeginRendering } from '../../utils'; +import {TestWrapper, TestRenderer, createSurfaceUpdate, createBeginRendering} from '../../utils'; import type * as Types from '@a2ui/web_core/types/types'; /** @@ -35,11 +35,11 @@ import type * as Types from '@a2ui/web_core/types/types'; function createTabsMessages( id: string, props: { - tabs: Array<{ title: string; contentText: string }>; + tabs: Array<{title: string; contentText: string}>; }, - surfaceId = '@default' + surfaceId = '@default', ): Types.ServerToClientMessage[] { - const components: Array<{ id: string; component: Record }> = []; + const components: Array<{id: string; component: Record}> = []; // Create content components for each tab const tabItems = props.tabs.map((tab, index) => { @@ -47,11 +47,11 @@ function createTabsMessages( components.push({ id: contentId, component: { - Text: { text: { literalString: tab.contentText }, usageHint: 'body' }, + Text: {text: {literalString: tab.contentText}, usageHint: 'body'}, }, }); return { - title: { literalString: tab.title }, + title: {literalString: tab.title}, child: contentId, }; }); @@ -60,14 +60,11 @@ function createTabsMessages( components.push({ id, component: { - Tabs: { tabItems }, + Tabs: {tabItems}, }, }); - return [ - createSurfaceUpdate(components, surfaceId), - createBeginRendering(id, surfaceId), - ]; + return [createSurfaceUpdate(components, surfaceId), createBeginRendering(id, surfaceId)]; } describe('Tabs Component', () => { @@ -75,15 +72,15 @@ describe('Tabs Component', () => { it('should render a tabs container with correct class', () => { const messages = createTabsMessages('tabs-1', { tabs: [ - { title: 'Tab 1', contentText: 'Content 1' }, - { title: 'Tab 2', contentText: 'Content 2' }, + {title: 'Tab 1', contentText: 'Content 1'}, + {title: 'Tab 2', contentText: 'Content 2'}, ], }); - const { container } = render( + const {container} = render( - + , ); const wrapper = container.querySelector('.a2ui-tabs'); @@ -93,13 +90,13 @@ describe('Tabs Component', () => { it('should render section element inside wrapper', () => { const messages = createTabsMessages('tabs-1', { - tabs: [{ title: 'Tab 1', contentText: 'Content 1' }], + tabs: [{title: 'Tab 1', contentText: 'Content 1'}], }); - const { container } = render( + const {container} = render( - + , ); const section = container.querySelector('.a2ui-tabs > section'); @@ -109,15 +106,15 @@ describe('Tabs Component', () => { it('should render buttons container with id="buttons"', () => { const messages = createTabsMessages('tabs-1', { tabs: [ - { title: 'Tab 1', contentText: 'Content 1' }, - { title: 'Tab 2', contentText: 'Content 2' }, + {title: 'Tab 1', contentText: 'Content 1'}, + {title: 'Tab 2', contentText: 'Content 2'}, ], }); - const { container } = render( + const {container} = render( - + , ); const buttonsContainer = container.querySelector('#buttons'); @@ -130,16 +127,16 @@ describe('Tabs Component', () => { it('should render a button for each tab', () => { const messages = createTabsMessages('tabs-1', { tabs: [ - { title: 'First', contentText: 'Content 1' }, - { title: 'Second', contentText: 'Content 2' }, - { title: 'Third', contentText: 'Content 3' }, + {title: 'First', contentText: 'Content 1'}, + {title: 'Second', contentText: 'Content 2'}, + {title: 'Third', contentText: 'Content 3'}, ], }); - const { container } = render( + const {container} = render( - + , ); const buttons = container.querySelectorAll('#buttons button'); @@ -149,37 +146,37 @@ describe('Tabs Component', () => { it('should display tab titles in buttons', () => { const messages = createTabsMessages('tabs-1', { tabs: [ - { title: 'Overview', contentText: 'Content 1' }, - { title: 'Details', contentText: 'Content 2' }, + {title: 'Overview', contentText: 'Content 1'}, + {title: 'Details', contentText: 'Content 2'}, ], }); render( - + , ); - expect(screen.getByRole('button', { name: 'Overview' })).toBeInTheDocument(); - expect(screen.getByRole('button', { name: 'Details' })).toBeInTheDocument(); + expect(screen.getByRole('button', {name: 'Overview'})).toBeInTheDocument(); + expect(screen.getByRole('button', {name: 'Details'})).toBeInTheDocument(); }); it('should disable the currently selected tab button', () => { const messages = createTabsMessages('tabs-1', { tabs: [ - { title: 'Tab 1', contentText: 'Content 1' }, - { title: 'Tab 2', contentText: 'Content 2' }, + {title: 'Tab 1', contentText: 'Content 1'}, + {title: 'Tab 2', contentText: 'Content 2'}, ], }); render( - + , ); - const tab1Button = screen.getByRole('button', { name: 'Tab 1' }); - const tab2Button = screen.getByRole('button', { name: 'Tab 2' }); + const tab1Button = screen.getByRole('button', {name: 'Tab 1'}); + const tab2Button = screen.getByRole('button', {name: 'Tab 2'}); // First tab selected by default - should be disabled expect(tab1Button).toBeDisabled(); @@ -189,19 +186,19 @@ describe('Tabs Component', () => { it('should have different disabled states for different tabs', () => { const messages = createTabsMessages('tabs-1', { tabs: [ - { title: 'Tab A', contentText: 'Content A' }, - { title: 'Tab B', contentText: 'Content B' }, + {title: 'Tab A', contentText: 'Content A'}, + {title: 'Tab B', contentText: 'Content B'}, ], }); render( - + , ); - const tabA = screen.getByRole('button', { name: 'Tab A' }) as HTMLButtonElement; - const tabB = screen.getByRole('button', { name: 'Tab B' }) as HTMLButtonElement; + const tabA = screen.getByRole('button', {name: 'Tab A'}) as HTMLButtonElement; + const tabB = screen.getByRole('button', {name: 'Tab B'}) as HTMLButtonElement; // Initially Tab A is selected (disabled) expect(tabA.disabled).not.toBe(tabB.disabled); @@ -212,15 +209,15 @@ describe('Tabs Component', () => { it('should select first tab by default', () => { const messages = createTabsMessages('tabs-1', { tabs: [ - { title: 'Tab 1', contentText: 'First tab content' }, - { title: 'Tab 2', contentText: 'Second tab content' }, + {title: 'Tab 1', contentText: 'First tab content'}, + {title: 'Tab 2', contentText: 'Second tab content'}, ], }); render( - + , ); // First tab's content should be visible @@ -230,22 +227,22 @@ describe('Tabs Component', () => { it('should switch content when clicking a different tab', () => { const messages = createTabsMessages('tabs-1', { tabs: [ - { title: 'Tab 1', contentText: 'First tab content' }, - { title: 'Tab 2', contentText: 'Second tab content' }, + {title: 'Tab 1', contentText: 'First tab content'}, + {title: 'Tab 2', contentText: 'Second tab content'}, ], }); render( - + , ); // Initially first tab content visible expect(screen.getByText('First tab content')).toBeInTheDocument(); // Click second tab - fireEvent.click(screen.getByRole('button', { name: 'Tab 2' })); + fireEvent.click(screen.getByRole('button', {name: 'Tab 2'})); // Second tab content should now be visible expect(screen.getByText('Second tab content')).toBeInTheDocument(); @@ -254,19 +251,19 @@ describe('Tabs Component', () => { it('should hide previous tab content when switching', () => { const messages = createTabsMessages('tabs-1', { tabs: [ - { title: 'Tab 1', contentText: 'First tab content' }, - { title: 'Tab 2', contentText: 'Second tab content' }, + {title: 'Tab 1', contentText: 'First tab content'}, + {title: 'Tab 2', contentText: 'Second tab content'}, ], }); render( - + , ); // Click second tab - fireEvent.click(screen.getByRole('button', { name: 'Tab 2' })); + fireEvent.click(screen.getByRole('button', {name: 'Tab 2'})); // First tab content should no longer be in document expect(screen.queryByText('First tab content')).not.toBeInTheDocument(); @@ -275,19 +272,19 @@ describe('Tabs Component', () => { it('should update disabled state when switching tabs', () => { const messages = createTabsMessages('tabs-1', { tabs: [ - { title: 'Tab 1', contentText: 'Content 1' }, - { title: 'Tab 2', contentText: 'Content 2' }, + {title: 'Tab 1', contentText: 'Content 1'}, + {title: 'Tab 2', contentText: 'Content 2'}, ], }); render( - + , ); - const tab1Button = screen.getByRole('button', { name: 'Tab 1' }); - const tab2Button = screen.getByRole('button', { name: 'Tab 2' }); + const tab1Button = screen.getByRole('button', {name: 'Tab 1'}); + const tab2Button = screen.getByRole('button', {name: 'Tab 2'}); // Initially Tab 1 is disabled expect(tab1Button).toBeDisabled(); @@ -304,33 +301,33 @@ describe('Tabs Component', () => { it('should handle switching between multiple tabs', () => { const messages = createTabsMessages('tabs-1', { tabs: [ - { title: 'Tab 1', contentText: 'Content One' }, - { title: 'Tab 2', contentText: 'Content Two' }, - { title: 'Tab 3', contentText: 'Content Three' }, + {title: 'Tab 1', contentText: 'Content One'}, + {title: 'Tab 2', contentText: 'Content Two'}, + {title: 'Tab 3', contentText: 'Content Three'}, ], }); render( - + , ); // Start at Tab 1 expect(screen.getByText('Content One')).toBeInTheDocument(); // Go to Tab 3 - fireEvent.click(screen.getByRole('button', { name: 'Tab 3' })); + fireEvent.click(screen.getByRole('button', {name: 'Tab 3'})); expect(screen.getByText('Content Three')).toBeInTheDocument(); expect(screen.queryByText('Content One')).not.toBeInTheDocument(); // Go to Tab 2 - fireEvent.click(screen.getByRole('button', { name: 'Tab 2' })); + fireEvent.click(screen.getByRole('button', {name: 'Tab 2'})); expect(screen.getByText('Content Two')).toBeInTheDocument(); expect(screen.queryByText('Content Three')).not.toBeInTheDocument(); // Go back to Tab 1 - fireEvent.click(screen.getByRole('button', { name: 'Tab 1' })); + fireEvent.click(screen.getByRole('button', {name: 'Tab 1'})); expect(screen.getByText('Content One')).toBeInTheDocument(); }); }); @@ -340,20 +337,32 @@ describe('Tabs Component', () => { const messages: Types.ServerToClientMessage[] = [ createSurfaceUpdate([ // Tab 1: Column with multiple text items - { id: 'text-1a', component: { Text: { text: { literalString: 'Item 1' } , usageHint: 'body' } } }, - { id: 'text-1b', component: { Text: { text: { literalString: 'Item 2' } , usageHint: 'body' } } }, - { id: 'col-1', component: { Column: { children: { explicitList: ['text-1a', 'text-1b'] } } } }, + { + id: 'text-1a', + component: {Text: {text: {literalString: 'Item 1'}, usageHint: 'body'}}, + }, + { + id: 'text-1b', + component: {Text: {text: {literalString: 'Item 2'}, usageHint: 'body'}}, + }, + { + id: 'col-1', + component: {Column: {children: {explicitList: ['text-1a', 'text-1b']}}}, + }, // Tab 2: Button - { id: 'btn-text', component: { Text: { text: { literalString: 'Click me' } , usageHint: 'body' } } }, - { id: 'btn-1', component: { Button: { child: 'btn-text', action: { name: 'test' } } } }, + { + id: 'btn-text', + component: {Text: {text: {literalString: 'Click me'}, usageHint: 'body'}}, + }, + {id: 'btn-1', component: {Button: {child: 'btn-text', action: {name: 'test'}}}}, // Tabs { id: 'tabs-1', component: { Tabs: { tabItems: [ - { title: { literalString: 'List' }, child: 'col-1' }, - { title: { literalString: 'Action' }, child: 'btn-1' }, + {title: {literalString: 'List'}, child: 'col-1'}, + {title: {literalString: 'Action'}, child: 'btn-1'}, ], }, }, @@ -365,7 +374,7 @@ describe('Tabs Component', () => { render( - + , ); // First tab shows column content @@ -373,23 +382,23 @@ describe('Tabs Component', () => { expect(screen.getByText('Item 2')).toBeInTheDocument(); // Switch to second tab - fireEvent.click(screen.getByRole('button', { name: 'Action' })); + fireEvent.click(screen.getByRole('button', {name: 'Action'})); // Button should be visible - expect(screen.getByRole('button', { name: 'Click me' })).toBeInTheDocument(); + expect(screen.getByRole('button', {name: 'Click me'})).toBeInTheDocument(); }); }); describe('Empty/Edge Cases', () => { it('should render with single tab', () => { const messages = createTabsMessages('tabs-1', { - tabs: [{ title: 'Only Tab', contentText: 'Only content' }], + tabs: [{title: 'Only Tab', contentText: 'Only content'}], }); - const { container } = render( + const {container} = render( - + , ); const buttons = container.querySelectorAll('#buttons button'); @@ -403,17 +412,17 @@ describe('Tabs Component', () => { { id: 'tabs-1', component: { - Tabs: { tabItems: [] }, + Tabs: {tabItems: []}, }, }, ]), createBeginRendering('tabs-1'), ]; - const { container } = render( + const {container} = render( - + , ); // Should still render the tabs wrapper @@ -431,13 +440,13 @@ describe('Tabs Component', () => { // Tabs styling comes from structural CSS, not theme classes. // The litTheme.components.Tabs.container is intentionally empty. const messages = createTabsMessages('tabs-1', { - tabs: [{ title: 'Tab 1', contentText: 'Content 1' }], + tabs: [{title: 'Tab 1', contentText: 'Content 1'}], }); - const { container } = render( + const {container} = render( - + , ); const section = container.querySelector('.a2ui-tabs > section'); @@ -448,13 +457,13 @@ describe('Tabs Component', () => { // Tabs button container styling comes from structural CSS. // The litTheme.components.Tabs.element is intentionally empty. const messages = createTabsMessages('tabs-1', { - tabs: [{ title: 'Tab 1', contentText: 'Content 1' }], + tabs: [{title: 'Tab 1', contentText: 'Content 1'}], }); - const { container } = render( + const {container} = render( - + , ); const buttonsContainer = container.querySelector('#buttons'); @@ -466,15 +475,15 @@ describe('Tabs Component', () => { // The litTheme.components.Tabs.controls.all/selected are intentionally empty. const messages = createTabsMessages('tabs-1', { tabs: [ - { title: 'Tab 1', contentText: 'Content 1' }, - { title: 'Tab 2', contentText: 'Content 2' }, + {title: 'Tab 1', contentText: 'Content 1'}, + {title: 'Tab 2', contentText: 'Content 2'}, ], }); - const { container } = render( + const {container} = render( - + , ); const buttons = container.querySelectorAll('#buttons button'); @@ -487,13 +496,13 @@ describe('Tabs Component', () => { describe('Structure', () => { it('should have correct DOM structure: div.a2ui-tabs > section > #buttons + content', () => { const messages = createTabsMessages('tabs-1', { - tabs: [{ title: 'Tab 1', contentText: 'Content 1' }], + tabs: [{title: 'Tab 1', contentText: 'Content 1'}], }); - const { container } = render( + const {container} = render( - + , ); const wrapper = container.querySelector('.a2ui-tabs'); @@ -516,18 +525,18 @@ describe('Tabs Component', () => { it('should have focusable tab buttons', () => { const messages = createTabsMessages('tabs-1', { tabs: [ - { title: 'Tab 1', contentText: 'Content 1' }, - { title: 'Tab 2', contentText: 'Content 2' }, + {title: 'Tab 1', contentText: 'Content 1'}, + {title: 'Tab 2', contentText: 'Content 2'}, ], }); render( - + , ); - const tab2Button = screen.getByRole('button', { name: 'Tab 2' }); + const tab2Button = screen.getByRole('button', {name: 'Tab 2'}); tab2Button.focus(); expect(document.activeElement).toBe(tab2Button); }); @@ -535,22 +544,22 @@ describe('Tabs Component', () => { it('should be keyboard activatable', () => { const messages = createTabsMessages('tabs-1', { tabs: [ - { title: 'Tab 1', contentText: 'Content 1' }, - { title: 'Tab 2', contentText: 'Content 2' }, + {title: 'Tab 1', contentText: 'Content 1'}, + {title: 'Tab 2', contentText: 'Content 2'}, ], }); render( - + , ); - const tab2Button = screen.getByRole('button', { name: 'Tab 2' }); + const tab2Button = screen.getByRole('button', {name: 'Tab 2'}); // Focus and press Enter tab2Button.focus(); - fireEvent.keyDown(tab2Button, { key: 'Enter' }); + fireEvent.keyDown(tab2Button, {key: 'Enter'}); fireEvent.click(tab2Button); // Buttons respond to click, not keyDown by default expect(screen.getByText('Content 2')).toBeInTheDocument(); diff --git a/renderers/react/tests/v0_8/unit/components/Text.test.tsx b/renderers/react/tests/v0_8/unit/components/Text.test.tsx index c8afd2b39..46640dd12 100644 --- a/renderers/react/tests/v0_8/unit/components/Text.test.tsx +++ b/renderers/react/tests/v0_8/unit/components/Text.test.tsx @@ -14,24 +14,24 @@ * limitations under the License. */ -import { describe, it, expect } from 'vitest'; -import { render, screen, waitFor } from '@testing-library/react'; +import {describe, it, expect} from 'vitest'; +import {render, screen, waitFor} from '@testing-library/react'; import React from 'react'; -import { TestWrapper, TestRenderer, createSimpleMessages } from '../../utils'; -import { litTheme, defaultTheme } from '../../../../src/v0_8'; +import {TestWrapper, TestRenderer, createSimpleMessages} from '../../utils'; +import {litTheme, defaultTheme} from '../../../../src/v0_8'; describe('Text Component', () => { describe('Basic Rendering', () => { it('should render text with literal string', async () => { const messages = createSimpleMessages('text-1', 'Text', { - text: { literalString: 'Hello World' }, + text: {literalString: 'Hello World'}, usageHint: 'body', }); render( - + , ); await waitFor(() => { @@ -41,14 +41,14 @@ describe('Text Component', () => { it('should render text with whitespace only', async () => { const messages = createSimpleMessages('text-1', 'Text', { - text: { literalString: ' ' }, + text: {literalString: ' '}, usageHint: 'body', }); - const { container } = render( + const {container} = render( - + , ); // Surface should exist with whitespace content @@ -58,14 +58,14 @@ describe('Text Component', () => { it('should render empty string', async () => { const messages = createSimpleMessages('text-1', 'Text', { - text: { literalString: '' }, + text: {literalString: ''}, usageHint: 'body', }); - const { container } = render( + const {container} = render( - + , ); expect(container.querySelector('.a2ui-surface')).toBeInTheDocument(); @@ -75,17 +75,17 @@ describe('Text Component', () => { describe('Usage Hints', () => { const usageHints = ['h1', 'h2', 'h3', 'h4', 'h5', 'caption', 'body'] as const; - usageHints.forEach((hint) => { + usageHints.forEach(hint => { it(`should render with usageHint="${hint}"`, async () => { const messages = createSimpleMessages('text-1', 'Text', { - text: { literalString: `${hint} text` }, + text: {literalString: `${hint} text`}, usageHint: hint, }); render( - + , ); await waitFor(() => { @@ -98,14 +98,14 @@ describe('Text Component', () => { // The usageHint only affects CSS classes, not the wrapper element. it('should render h1 with section wrapper (Lit DOM structure)', async () => { const messages = createSimpleMessages('text-1', 'Text', { - text: { literalString: 'Main Title' }, + text: {literalString: 'Main Title'}, usageHint: 'h1', }); - const { container } = render( + const {container} = render( - + , ); await waitFor(() => { @@ -118,14 +118,14 @@ describe('Text Component', () => { it('should render h2 with section wrapper (Lit DOM structure)', async () => { const messages = createSimpleMessages('text-1', 'Text', { - text: { literalString: 'Section Title' }, + text: {literalString: 'Section Title'}, usageHint: 'h2', }); - const { container } = render( + const {container} = render( - + , ); await waitFor(() => { @@ -137,14 +137,14 @@ describe('Text Component', () => { it('should render caption with section wrapper (Lit DOM structure)', async () => { const messages = createSimpleMessages('text-1', 'Text', { - text: { literalString: 'Caption text' }, + text: {literalString: 'Caption text'}, usageHint: 'caption', }); - const { container } = render( + const {container} = render( - + , ); await waitFor(() => { @@ -156,14 +156,14 @@ describe('Text Component', () => { it('should render body with section wrapper (Lit DOM structure)', async () => { const messages = createSimpleMessages('text-1', 'Text', { - text: { literalString: 'Body text' }, + text: {literalString: 'Body text'}, usageHint: 'body', }); - const { container } = render( + const {container} = render( - + , ); await waitFor(() => { @@ -177,14 +177,14 @@ describe('Text Component', () => { describe('Theme Support', () => { it('should apply default theme classes', async () => { const messages = createSimpleMessages('text-1', 'Text', { - text: { literalString: 'Themed text' }, + text: {literalString: 'Themed text'}, usageHint: 'body', }); - const { container } = render( + const {container} = render( - + , ); await waitFor(() => { @@ -197,14 +197,14 @@ describe('Text Component', () => { it('should apply lit theme classes for h1', async () => { const messages = createSimpleMessages('text-1', 'Text', { - text: { literalString: 'Lit theme text' }, + text: {literalString: 'Lit theme text'}, usageHint: 'h1', }); - const { container } = render( + const {container} = render( - + , ); await waitFor(() => { @@ -217,14 +217,14 @@ describe('Text Component', () => { it('should apply body variant classes from lit theme', async () => { const messages = createSimpleMessages('text-1', 'Text', { - text: { literalString: 'Body text' }, + text: {literalString: 'Body text'}, usageHint: 'body', }); - const { container } = render( + const {container} = render( - + , ); await waitFor(() => { @@ -239,14 +239,14 @@ describe('Text Component', () => { describe('Markdown Rendering', () => { it('should render bold text', async () => { const messages = createSimpleMessages('text-1', 'Text', { - text: { literalString: 'This is **bold** text' }, + text: {literalString: 'This is **bold** text'}, usageHint: 'body', }); - const { container } = render( + const {container} = render( - + , ); await waitFor(() => { @@ -258,14 +258,14 @@ describe('Text Component', () => { it('should render italic text', async () => { const messages = createSimpleMessages('text-1', 'Text', { - text: { literalString: 'This is *italic* text' }, + text: {literalString: 'This is *italic* text'}, usageHint: 'body', }); - const { container } = render( + const {container} = render( - + , ); await waitFor(() => { @@ -277,14 +277,14 @@ describe('Text Component', () => { it('should render inline code', async () => { const messages = createSimpleMessages('text-1', 'Text', { - text: { literalString: 'Use the `console.log()` function' }, + text: {literalString: 'Use the `console.log()` function'}, usageHint: 'body', }); - const { container } = render( + const {container} = render( - + , ); await waitFor(() => { @@ -296,14 +296,14 @@ describe('Text Component', () => { it('should render unordered lists', async () => { const messages = createSimpleMessages('text-1', 'Text', { - text: { literalString: '- Item 1\n- Item 2\n- Item 3' }, + text: {literalString: '- Item 1\n- Item 2\n- Item 3'}, usageHint: 'body', }); - const { container } = render( + const {container} = render( - + , ); await waitFor(() => { @@ -316,14 +316,14 @@ describe('Text Component', () => { it('should render ordered lists', async () => { const messages = createSimpleMessages('text-1', 'Text', { - text: { literalString: '1. First\n2. Second\n3. Third' }, + text: {literalString: '1. First\n2. Second\n3. Third'}, usageHint: 'body', }); - const { container } = render( + const {container} = render( - + , ); await waitFor(() => { @@ -336,14 +336,14 @@ describe('Text Component', () => { it('should render links', async () => { const messages = createSimpleMessages('text-1', 'Text', { - text: { literalString: 'Visit [Google](https://google.com)' }, + text: {literalString: 'Visit [Google](https://google.com)'}, usageHint: 'body', }); - const { container } = render( + const {container} = render( - + , ); await waitFor(() => { @@ -356,14 +356,14 @@ describe('Text Component', () => { it('should render plain URLs as text (auto-linkify not enabled)', async () => { const messages = createSimpleMessages('text-1', 'Text', { - text: { literalString: 'Check out https://example.com for more' }, + text: {literalString: 'Check out https://example.com for more'}, usageHint: 'body', }); - const { container } = render( + const {container} = render( - + , ); await waitFor(() => { @@ -375,14 +375,14 @@ describe('Text Component', () => { it('should render blockquotes', async () => { const messages = createSimpleMessages('text-1', 'Text', { - text: { literalString: '> This is a quote' }, + text: {literalString: '> This is a quote'}, usageHint: 'body', }); - const { container } = render( + const {container} = render( - + , ); await waitFor(() => { @@ -393,14 +393,14 @@ describe('Text Component', () => { it('should render code blocks', async () => { const messages = createSimpleMessages('text-1', 'Text', { - text: { literalString: '```\nconst x = 1;\n```' }, + text: {literalString: '```\nconst x = 1;\n```'}, usageHint: 'body', }); - const { container } = render( + const {container} = render( - + , ); await waitFor(() => { @@ -413,14 +413,14 @@ describe('Text Component', () => { it('should preserve line breaks in text', async () => { const messages = createSimpleMessages('text-1', 'Text', { - text: { literalString: 'Line 1\nLine 2' }, + text: {literalString: 'Line 1\nLine 2'}, usageHint: 'body', }); - const { container } = render( + const {container} = render( - + , ); await waitFor(() => { @@ -434,14 +434,14 @@ describe('Text Component', () => { describe('Markdown Theme Classes', () => { it('should apply theme classes to markdown elements', async () => { const messages = createSimpleMessages('text-1', 'Text', { - text: { literalString: '- List item' }, + text: {literalString: '- List item'}, usageHint: 'body', }); - const { container } = render( + const {container} = render( - + , ); await waitFor(() => { @@ -454,14 +454,14 @@ describe('Text Component', () => { it('should apply paragraph classes from theme', async () => { const messages = createSimpleMessages('text-1', 'Text', { - text: { literalString: 'A paragraph of text.' }, + text: {literalString: 'A paragraph of text.'}, usageHint: 'body', }); - const { container } = render( + const {container} = render( - + , ); await waitFor(() => { @@ -476,14 +476,14 @@ describe('Text Component', () => { describe('Security', () => { it('should not render raw HTML', async () => { const messages = createSimpleMessages('text-1', 'Text', { - text: { literalString: '' }, + text: {literalString: ''}, usageHint: 'body', }); - const { container } = render( + const {container} = render( - + , ); await waitFor(() => { @@ -497,14 +497,14 @@ describe('Text Component', () => { it('should not render onclick handlers', async () => { const messages = createSimpleMessages('text-1', 'Text', { - text: { literalString: '
Click me
' }, + text: {literalString: '
Click me
'}, usageHint: 'body', }); - const { container } = render( + const {container} = render( - + , ); await waitFor(() => { @@ -516,14 +516,14 @@ describe('Text Component', () => { it('should not render iframe tags', async () => { const messages = createSimpleMessages('text-1', 'Text', { - text: { literalString: '' }, + text: {literalString: ''}, usageHint: 'body', }); - const { container } = render( + const {container} = render( - + , ); await waitFor(() => { diff --git a/renderers/react/tests/v0_8/unit/components/TextField.test.tsx b/renderers/react/tests/v0_8/unit/components/TextField.test.tsx index f2d1f281b..8f0a51865 100644 --- a/renderers/react/tests/v0_8/unit/components/TextField.test.tsx +++ b/renderers/react/tests/v0_8/unit/components/TextField.test.tsx @@ -14,10 +14,10 @@ * limitations under the License. */ -import { describe, it, expect } from 'vitest'; -import { render, screen, fireEvent } from '@testing-library/react'; +import {describe, it, expect} from 'vitest'; +import {render, screen, fireEvent} from '@testing-library/react'; import React from 'react'; -import { TestWrapper, TestRenderer, createSimpleMessages } from '../../utils'; +import {TestWrapper, TestRenderer, createSimpleMessages} from '../../utils'; /** * TextField tests following A2UI specification. @@ -32,13 +32,13 @@ describe('TextField Component', () => { describe('Basic Rendering', () => { it('should render an input element', () => { const messages = createSimpleMessages('tf-1', 'TextField', { - label: { literalString: 'Username' }, + label: {literalString: 'Username'}, }); - const { container } = render( + const {container} = render( - + , ); const input = container.querySelector('input'); @@ -47,13 +47,13 @@ describe('TextField Component', () => { it('should render with wrapper div', () => { const messages = createSimpleMessages('tf-1', 'TextField', { - label: { literalString: 'Email' }, + label: {literalString: 'Email'}, }); - const { container } = render( + const {container} = render( - + , ); const wrapper = container.querySelector('.a2ui-textfield'); @@ -62,14 +62,14 @@ describe('TextField Component', () => { it('should render with initial text value', () => { const messages = createSimpleMessages('tf-1', 'TextField', { - label: { literalString: 'Name' }, - text: { literalString: 'John Doe' }, + label: {literalString: 'Name'}, + text: {literalString: 'John Doe'}, }); - const { container } = render( + const {container} = render( - + , ); const input = container.querySelector('input') as HTMLInputElement; @@ -78,13 +78,13 @@ describe('TextField Component', () => { it('should render placeholder text', () => { const messages = createSimpleMessages('tf-1', 'TextField', { - label: { literalString: 'Search' }, + label: {literalString: 'Search'}, }); - const { container } = render( + const {container} = render( - + , ); const input = container.querySelector('input') as HTMLInputElement; @@ -95,13 +95,13 @@ describe('TextField Component', () => { describe('Label Rendering', () => { it('should render label (required field)', () => { const messages = createSimpleMessages('tf-1', 'TextField', { - label: { literalString: 'Username' }, + label: {literalString: 'Username'}, }); render( - + , ); expect(screen.getByText('Username')).toBeInTheDocument(); @@ -109,13 +109,13 @@ describe('TextField Component', () => { it('should associate label with input via htmlFor', () => { const messages = createSimpleMessages('tf-1', 'TextField', { - label: { literalString: 'Email Address' }, + label: {literalString: 'Email Address'}, }); - const { container } = render( + const {container} = render( - + , ); const label = container.querySelector('label'); @@ -127,14 +127,14 @@ describe('TextField Component', () => { describe('Input Types (type)', () => { it('should render text input by default (shortText)', () => { const messages = createSimpleMessages('tf-1', 'TextField', { - label: { literalString: 'Name' }, + label: {literalString: 'Name'}, type: 'shortText', }); - const { container } = render( + const {container} = render( - + , ); const input = container.querySelector('input'); @@ -143,14 +143,14 @@ describe('TextField Component', () => { it('should render number input for type=number', () => { const messages = createSimpleMessages('tf-1', 'TextField', { - label: { literalString: 'Age' }, + label: {literalString: 'Age'}, textFieldType: 'number', }); - const { container } = render( + const {container} = render( - + , ); const input = container.querySelector('input'); @@ -159,14 +159,14 @@ describe('TextField Component', () => { it('should render date input for type=date', () => { const messages = createSimpleMessages('tf-1', 'TextField', { - label: { literalString: 'Birth Date' }, + label: {literalString: 'Birth Date'}, textFieldType: 'date', }); - const { container } = render( + const {container} = render( - + , ); const input = container.querySelector('input'); @@ -175,14 +175,14 @@ describe('TextField Component', () => { it('should render textarea for type=longText', () => { const messages = createSimpleMessages('tf-1', 'TextField', { - label: { literalString: 'Description' }, + label: {literalString: 'Description'}, textFieldType: 'longText', }); - const { container } = render( + const {container} = render( - + , ); const textarea = container.querySelector('textarea'); @@ -194,35 +194,35 @@ describe('TextField Component', () => { describe('User Interaction', () => { it('should update value on change', () => { const messages = createSimpleMessages('tf-1', 'TextField', { - label: { literalString: 'Input' }, + label: {literalString: 'Input'}, }); - const { container } = render( + const {container} = render( - + , ); const input = container.querySelector('input') as HTMLInputElement; - fireEvent.change(input, { target: { value: 'New value' } }); + fireEvent.change(input, {target: {value: 'New value'}}); expect(input.value).toBe('New value'); }); it('should update textarea value on change', () => { const messages = createSimpleMessages('tf-1', 'TextField', { - label: { literalString: 'Comments' }, + label: {literalString: 'Comments'}, textFieldType: 'longText', }); - const { container } = render( + const {container} = render( - + , ); const textarea = container.querySelector('textarea') as HTMLTextAreaElement; - fireEvent.change(textarea, { target: { value: 'Long text content' } }); + fireEvent.change(textarea, {target: {value: 'Long text content'}}); expect(textarea.value).toBe('Long text content'); }); @@ -231,13 +231,13 @@ describe('TextField Component', () => { describe('Theme Support', () => { it('should render within section container', () => { const messages = createSimpleMessages('tf-1', 'TextField', { - label: { literalString: 'Field' }, + label: {literalString: 'Field'}, }); - const { container } = render( + const {container} = render( - + , ); const section = container.querySelector('section'); diff --git a/renderers/react/tests/v0_8/utils/assertions.ts b/renderers/react/tests/v0_8/utils/assertions.ts index 518274ce5..73748fa4e 100644 --- a/renderers/react/tests/v0_8/utils/assertions.ts +++ b/renderers/react/tests/v0_8/utils/assertions.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import type { Mock } from 'vitest'; +import type {Mock} from 'vitest'; /** * Safely get a mock call argument with proper typing. @@ -36,7 +36,9 @@ export function getMockCallArg(mock: Mock, callIndex: number, argIndex = 0): export function getElement(array: T[], index: number): T { const element = array[index]; if (element === undefined) { - throw new Error(`Array element at index ${index} does not exist. Array length: ${array.length}`); + throw new Error( + `Array element at index ${index} does not exist. Array length: ${array.length}`, + ); } return element; } diff --git a/renderers/react/tests/v0_8/utils/messages.ts b/renderers/react/tests/v0_8/utils/messages.ts index 32f87a50e..6a1891432 100644 --- a/renderers/react/tests/v0_8/utils/messages.ts +++ b/renderers/react/tests/v0_8/utils/messages.ts @@ -20,13 +20,13 @@ import type * as Types from '@a2ui/web_core/types/types'; * Create a surface update message with components. */ export function createSurfaceUpdate( - components: Array<{ id: string; component: Record }>, - surfaceId = '@default' + components: Array<{id: string; component: Record}>, + surfaceId = '@default', ): Types.ServerToClientMessage { return { surfaceUpdate: { surfaceId, - components: components.map((c) => ({ + components: components.map(c => ({ id: c.id, component: c.component, })), @@ -39,7 +39,7 @@ export function createSurfaceUpdate( */ export function createBeginRendering( rootId: string, - surfaceId = '@default' + surfaceId = '@default', ): Types.ServerToClientMessage { return { beginRendering: { @@ -56,13 +56,10 @@ export function createSimpleMessages( id: string, componentType: string, props: Record, - surfaceId = '@default' + surfaceId = '@default', ): Types.ServerToClientMessage[] { return [ - createSurfaceUpdate( - [{ id, component: { [componentType]: props } }], - surfaceId - ), + createSurfaceUpdate([{id, component: {[componentType]: props}}], surfaceId), createBeginRendering(id, surfaceId), ]; } @@ -72,9 +69,15 @@ export function createSimpleMessages( * Per A2UI spec: Updates application state independently of UI structure. */ export function createDataModelUpdate( - contents: Array<{ key: string; valueString?: string; valueNumber?: number; valueBoolean?: boolean; valueMap?: unknown[] }>, + contents: Array<{ + key: string; + valueString?: string; + valueNumber?: number; + valueBoolean?: boolean; + valueMap?: unknown[]; + }>, surfaceId = '@default', - path?: string + path?: string, ): Types.ServerToClientMessage { return { dataModelUpdate: { @@ -102,9 +105,9 @@ export function createDeleteSurface(surfaceId: string): Types.ServerToClientMess * Uses valueString for JSON-serializable values. */ export function createDataModelUpdateSpec( - contents: Array<{ key: string; valueString?: string; valueMap?: unknown[] }>, + contents: Array<{key: string; valueString?: string; valueMap?: unknown[]}>, surfaceId = '@default', - path = '/' + path = '/', ): Types.ServerToClientMessage { return { dataModelUpdate: { diff --git a/renderers/react/tests/v0_8/utils/render.tsx b/renderers/react/tests/v0_8/utils/render.tsx index ff43f42a6..28acfac9e 100644 --- a/renderers/react/tests/v0_8/utils/render.tsx +++ b/renderers/react/tests/v0_8/utils/render.tsx @@ -14,8 +14,8 @@ * limitations under the License. */ -import React, { useEffect, type ReactNode } from 'react'; -import { A2UIProvider, A2UIRenderer, useA2UI } from '../../../src/v0_8'; +import React, {useEffect, type ReactNode} from 'react'; +import {A2UIProvider, A2UIRenderer, useA2UI} from '../../../src/v0_8'; import type * as Types from '@a2ui/web_core/types/types'; /** @@ -28,7 +28,7 @@ export function TestRenderer({ messages: Types.ServerToClientMessage[]; surfaceId?: string; }) { - const { processMessages } = useA2UI(); + const {processMessages} = useA2UI(); useEffect(() => { processMessages(messages); diff --git a/renderers/react/tests/v0_9/adapter.test.tsx b/renderers/react/tests/v0_9/adapter.test.tsx index 30fc781c2..96e952e03 100644 --- a/renderers/react/tests/v0_9/adapter.test.tsx +++ b/renderers/react/tests/v0_9/adapter.test.tsx @@ -14,19 +14,28 @@ * limitations under the License. */ -import { describe, it, expect, vi } from 'vitest'; -import { render, screen, act } from '@testing-library/react'; -import { createComponentImplementation } from '../../src/v0_9/adapter'; -import { A2uiSurface } from '../../src/v0_9/A2uiSurface'; -import { ComponentContext, ComponentModel, SurfaceModel, Catalog, CommonSchemas } from '@a2ui/web_core/v0_9'; -import { z } from 'zod'; +import {describe, it, expect, vi} from 'vitest'; +import {render, screen, act} from '@testing-library/react'; +import {createComponentImplementation} from '../../src/v0_9/adapter'; +import {A2uiSurface} from '../../src/v0_9/A2uiSurface'; +import { + ComponentContext, + ComponentModel, + SurfaceModel, + Catalog, + CommonSchemas, +} from '@a2ui/web_core/v0_9'; +import {z} from 'zod'; const mockCatalog = new Catalog('test', [], []); describe('adapter', () => { it('should render component with resolved props', () => { const surface = new SurfaceModel('test-surface', mockCatalog); - const compModel = new ComponentModel('c1', 'TestComp', { text: 'Hello World', child: 'child1' }); + const compModel = new ComponentModel('c1', 'TestComp', { + text: 'Hello World', + child: 'child1', + }); surface.componentsModel.addComponent(compModel); const context = new ComponentContext(surface, 'c1', '/'); @@ -35,21 +44,20 @@ describe('adapter', () => { name: 'TestComp', schema: z.object({ text: CommonSchemas.DynamicString, - child: CommonSchemas.ComponentId - }) + child: CommonSchemas.ComponentId, + }), }; - const TestComponent = createComponentImplementation( - TestApiDef, - ({ props, buildChild }) => { - return
+ const TestComponent = createComponentImplementation(TestApiDef, ({props, buildChild}) => { + return ( +
{props.text} {props.child && buildChild(props.child)} -
; - } - ); +
+ ); + }); - const buildChild = vi.fn().mockImplementation((id) =>
Child
); + const buildChild = vi.fn().mockImplementation(id =>
Child
); render(); @@ -59,9 +67,9 @@ describe('adapter', () => { it('should react to data model changes', async () => { const surface = new SurfaceModel('test-surface', mockCatalog); - const compModel = new ComponentModel('c1', 'TestComp', { text: { path: '/greeting' } }); + const compModel = new ComponentModel('c1', 'TestComp', {text: {path: '/greeting'}}); surface.componentsModel.addComponent(compModel); - + // Set initial data surface.dataModel.set('/greeting', 'Hello Reactive'); @@ -70,18 +78,17 @@ describe('adapter', () => { const TestApiDef = { name: 'TestComp', schema: z.object({ - text: CommonSchemas.DynamicString - }) + text: CommonSchemas.DynamicString, + }), }; - const TestComponent = createComponentImplementation( - TestApiDef, - ({ props }) => { - return
{props.text}
; - } - ); + const TestComponent = createComponentImplementation(TestApiDef, ({props}) => { + return
{props.text}
; + }); - const { getByTestId } = render( null} />); + const {getByTestId} = render( + null} />, + ); expect(getByTestId('msg').textContent).toBe('Hello Reactive'); @@ -95,9 +102,9 @@ describe('adapter', () => { it('should clean up listeners on unmount', () => { const surface = new SurfaceModel('test-surface', mockCatalog); - const compModel = new ComponentModel('c1', 'TestComp', { text: { path: '/greeting' } }); + const compModel = new ComponentModel('c1', 'TestComp', {text: {path: '/greeting'}}); surface.componentsModel.addComponent(compModel); - + const context = new ComponentContext(surface, 'c1', '/'); const unsubscribeSpy = vi.fn(); @@ -109,64 +116,69 @@ describe('adapter', () => { const TestApiDef = { name: 'TestComp', schema: z.object({ - text: CommonSchemas.DynamicString - }) + text: CommonSchemas.DynamicString, + }), }; - const TestComponent = createComponentImplementation( - TestApiDef, - ({ props }) => { - return
{props.text}
; - } - ); + const TestComponent = createComponentImplementation(TestApiDef, ({props}) => { + return
{props.text}
; + }); - const { unmount } = render( null} />); + const {unmount} = render( null} />); expect(spyAddListener).toHaveBeenCalled(); - + unmount(); - + expect(unsubscribeSpy).toHaveBeenCalled(); }); it('preserves progressive rendering (avoids stale closures from over-memoization)', async () => { - const ParentApiDef = { name: 'TestParent', schema: z.object({ child: CommonSchemas.ComponentId }) }; - const ChildApiDef = { name: 'TestChild', schema: z.object({ text: CommonSchemas.DynamicString }) }; - + const ParentApiDef = { + name: 'TestParent', + schema: z.object({child: CommonSchemas.ComponentId}), + }; + const ChildApiDef = { + name: 'TestChild', + schema: z.object({text: CommonSchemas.DynamicString}), + }; + let parentRenderCount = 0; - const TestParent = createComponentImplementation(ParentApiDef, ({ props, buildChild }) => { + const TestParent = createComponentImplementation(ParentApiDef, ({props, buildChild}) => { parentRenderCount++; return
{props.child && buildChild(props.child)}
; }); - const TestChild = createComponentImplementation(ChildApiDef, ({ props }) => ( + const TestChild = createComponentImplementation(ChildApiDef, ({props}) => ( {props.text} )); const testCatalog = new Catalog('test', [TestParent, TestChild], []); const surface = new SurfaceModel('test-surface', testCatalog); - + // 1. Initial State: Parent component exists, but its child is missing from the surface. - const parentModel = new ComponentModel('root', 'TestParent', { child: 'child1' }); + const parentModel = new ComponentModel('root', 'TestParent', {child: 'child1'}); surface.componentsModel.addComponent(parentModel); - const { getByTestId, queryByTestId } = render(); + const {getByTestId, queryByTestId} = render(); // Assert the missing child renders the fallback expect(getByTestId('parent').textContent).toContain('[Loading child1...]'); - + const countBeforeChild = parentRenderCount; // 2. Simulate streaming 'updateComponents' adding the missing child await act(async () => { - surface.componentsModel.addComponent(new ComponentModel('child1', 'TestChild', { text: 'Loaded Data' })); + surface.componentsModel.addComponent( + new ComponentModel('child1', 'TestChild', {text: 'Loaded Data'}), + ); }); // 3. Child should automatically resolve through DeferredChild's subscription expect(queryByTestId('resolved')).not.toBeNull(); expect(getByTestId('resolved').textContent).toBe('Loaded Data'); - + // Crucially, the parent should NOT have re-rendered because of the child addition. // The DeferredChild wrapper localized the update. expect(parentRenderCount).toBe(countBeforeChild); diff --git a/renderers/react/tests/v0_9/basic-catalog-examples.test.tsx b/renderers/react/tests/v0_9/basic-catalog-examples.test.tsx index 29d13cda0..18e2d7fa9 100644 --- a/renderers/react/tests/v0_9/basic-catalog-examples.test.tsx +++ b/renderers/react/tests/v0_9/basic-catalog-examples.test.tsx @@ -27,14 +27,14 @@ import path from 'path'; describe('v0.9 Basic Catalog Examples Rendering', () => { const examplesDir = path.resolve( process.cwd(), - '../../specification/v0_9/json/catalogs/basic/examples' + '../../specification/v0_9/json/catalogs/basic/examples', ); if (!fs.existsSync(examplesDir)) { throw new Error(`Examples directory not found: ${examplesDir}`); } - const files = fs.readdirSync(examplesDir).filter((f) => f.endsWith('.json')); + const files = fs.readdirSync(examplesDir).filter(f => f.endsWith('.json')); for (const file of files) { it(`should successfully render ${file}`, async () => { @@ -65,7 +65,7 @@ describe('v0.9 Basic Catalog Examples Rendering', () => { const {container} = render( - + , ); // Assert that it rendered something and didn't throw diff --git a/renderers/react/tests/v0_9/catalog-components.test.tsx b/renderers/react/tests/v0_9/catalog-components.test.tsx index 23ef100cd..59afa8bcc 100644 --- a/renderers/react/tests/v0_9/catalog-components.test.tsx +++ b/renderers/react/tests/v0_9/catalog-components.test.tsx @@ -14,10 +14,10 @@ * limitations under the License. */ -import { describe, it, expect, vi } from 'vitest'; -import { screen, fireEvent, act } from '@testing-library/react'; -import { ComponentModel } from '@a2ui/web_core/v0_9'; -import { renderA2uiComponent } from '../utils'; +import {describe, it, expect, vi} from 'vitest'; +import {screen, fireEvent, act} from '@testing-library/react'; +import {ComponentModel} from '@a2ui/web_core/v0_9'; +import {renderA2uiComponent} from '../utils'; import { Text, @@ -43,18 +43,18 @@ import { describe('Basic Catalog Components', () => { describe('Text', () => { it('renders static text', () => { - renderA2uiComponent(Text, 't1', { text: 'Hello World' }); + renderA2uiComponent(Text, 't1', {text: 'Hello World'}); expect(screen.getByText('Hello World')).toBeDefined(); }); it('renders reactive text from data model', async () => { - const { updateData } = renderA2uiComponent( - Text, - 't1', - { text: { path: '/msg' } }, - { initialData: { msg: 'Initial' } } + const {updateData} = renderA2uiComponent( + Text, + 't1', + {text: {path: '/msg'}}, + {initialData: {msg: 'Initial'}}, ); - + expect(screen.getByText('Initial')).toBeDefined(); await act(async () => { @@ -65,7 +65,7 @@ describe('Basic Catalog Components', () => { }); it('renders with correct heading tag based on variant', () => { - const { view } = renderA2uiComponent(Text, 't1', { text: 'Title', variant: 'h1' }); + const {view} = renderA2uiComponent(Text, 't1', {text: 'Title', variant: 'h1'}); const h1 = view.container.querySelector('div.h1'); expect(h1).not.toBeNull(); expect(h1?.textContent).toBe('# Title'); @@ -74,9 +74,9 @@ describe('Basic Catalog Components', () => { describe('Image', () => { it('renders image with url and object-fit', () => { - const { view } = renderA2uiComponent(Image, 'i1', { + const {view} = renderA2uiComponent(Image, 'i1', { url: 'https://example.com/img.png', - fit: 'cover' + fit: 'cover', }); const img = view.container.querySelector('img') as HTMLImageElement; expect(img.src).toBe('https://example.com/img.png'); @@ -84,18 +84,18 @@ describe('Basic Catalog Components', () => { }); it('renders image with description as alt text', () => { - const { view } = renderA2uiComponent(Image, 'i1', { + const {view} = renderA2uiComponent(Image, 'i1', { url: 'url', - description: 'A beautiful sunset' + description: 'A beautiful sunset', }); const img = view.container.querySelector('img') as HTMLImageElement; expect(img.alt).toBe('A beautiful sunset'); }); it('applies variant-specific styling (avatar)', () => { - const { view } = renderA2uiComponent(Image, 'i1', { + const {view} = renderA2uiComponent(Image, 'i1', { url: 'url', - variant: 'avatar' + variant: 'avatar', }); const img = view.container.querySelector('img') as HTMLImageElement; expect(img.style.borderRadius).toBe('50%'); @@ -105,13 +105,13 @@ describe('Basic Catalog Components', () => { describe('Icon', () => { it('renders material icon by name', () => { - const { view } = renderA2uiComponent(Icon, 'ic1', { name: 'settings' }); + const {view} = renderA2uiComponent(Icon, 'ic1', {name: 'settings'}); expect(view.container.textContent).toContain('settings'); expect(view.container.querySelector('.material-symbols-outlined')).not.toBeNull(); }); it('converts camelCase icon names to snake_case', () => { - const { view } = renderA2uiComponent(Icon, 'ic1', { name: 'shoppingCart' }); + const {view} = renderA2uiComponent(Icon, 'ic1', {name: 'shoppingCart'}); expect(view.container.textContent).toContain('shopping_cart'); }); @@ -121,14 +121,14 @@ describe('Basic Catalog Components', () => { ['favoriteOff', 'favorite_border'], ['starOff', 'star_border'], ])('maps "%s" to "%s"', (specName, materialName) => { - const { view } = renderA2uiComponent(Icon, 'ic1', { name: specName }); + const {view} = renderA2uiComponent(Icon, 'ic1', {name: specName}); expect(view.container.textContent).toContain(materialName); }); }); describe('Video', () => { it('renders video element with source and controls', () => { - const { view } = renderA2uiComponent(Video, 'v1', { url: 'vid.mp4' }); + const {view} = renderA2uiComponent(Video, 'v1', {url: 'vid.mp4'}); const video = view.container.querySelector('video') as HTMLVideoElement; expect(video.src).toContain('vid.mp4'); expect(video.controls).toBe(true); @@ -137,9 +137,9 @@ describe('Basic Catalog Components', () => { describe('AudioPlayer', () => { it('renders audio element and description', () => { - renderA2uiComponent(AudioPlayer, 'a1', { + renderA2uiComponent(AudioPlayer, 'a1', { url: 'audio.mp3', - description: 'Listen to this' + description: 'Listen to this', }); expect(screen.getByText('Listen to this')).toBeDefined(); const audio = document.querySelector('audio') as HTMLAudioElement; @@ -149,34 +149,34 @@ describe('Basic Catalog Components', () => { describe('Button', () => { it('dispatches action on click', async () => { - const { surface } = renderA2uiComponent(Button, 'b1', { - action: { event: { name: 'submit_clicked' } }, - child: 'label1' + const {surface} = renderA2uiComponent(Button, 'b1', { + action: {event: {name: 'submit_clicked'}}, + child: 'label1', }); const actionSpy = vi.fn(); surface.onAction.subscribe(actionSpy); fireEvent.click(screen.getByRole('button')); - - expect(actionSpy).toHaveBeenCalledWith(expect.objectContaining({ name: 'submit_clicked' })); + + expect(actionSpy).toHaveBeenCalledWith(expect.objectContaining({name: 'submit_clicked'})); }); it('is disabled when isValid is false (via checks)', async () => { - const { updateData } = renderA2uiComponent( - Button, - 'b1', - { - action: { event: { name: 'submit' } }, + const {updateData} = renderA2uiComponent( + Button, + 'b1', + { + action: {event: {name: 'submit'}}, checks: [ { call: 'required', - args: { value: { path: '/name' } }, - message: 'Name is required' - } - ] + args: {value: {path: '/name'}}, + message: 'Name is required', + }, + ], }, - { initialData: { name: '' } } + {initialData: {name: ''}}, ); const button = screen.getByRole('button') as HTMLButtonElement; @@ -190,7 +190,7 @@ describe('Basic Catalog Components', () => { }); it('delegates child rendering to buildChild', () => { - const { buildChild } = renderA2uiComponent(Button, 'b1', { child: 'inner1' }); + const {buildChild} = renderA2uiComponent(Button, 'b1', {child: 'inner1'}); expect(buildChild).toHaveBeenCalledWith('inner1'); expect(screen.getByTestId('child-inner1')).toBeDefined(); }); @@ -198,27 +198,27 @@ describe('Basic Catalog Components', () => { describe('TextField', () => { it('updates data model on change', () => { - const { surface } = renderA2uiComponent(TextField, 'f1', { + const {surface} = renderA2uiComponent(TextField, 'f1', { label: 'Name', - value: { path: '/user/name' } + value: {path: '/user/name'}, }); const input = screen.getByLabelText('Name'); - fireEvent.change(input, { target: { value: 'Bob' } }); - + fireEvent.change(input, {target: {value: 'Bob'}}); + expect(surface.dataModel.get('/user/name')).toBe('Bob'); }); it('shows validation error message', async () => { - const { updateData } = renderA2uiComponent( - TextField, - 'f1', - { + const {updateData} = renderA2uiComponent( + TextField, + 'f1', + { label: 'Email', - value: { path: '/email' }, - checks: [{ call: 'required', args: { value: { path: '/email' } }, message: 'Required!' }] + value: {path: '/email'}, + checks: [{call: 'required', args: {value: {path: '/email'}}, message: 'Required!'}], }, - { initialData: { email: '' } } + {initialData: {email: ''}}, ); expect(screen.getByText('Required!')).toBeDefined(); @@ -233,8 +233,8 @@ describe('Basic Catalog Components', () => { describe('Layout and Structural Components', () => { it('Row renders multiple children', () => { - const { buildChild } = renderA2uiComponent(Row, 'r1', { - children: ['c1', 'c2'] + const {buildChild} = renderA2uiComponent(Row, 'r1', { + children: ['c1', 'c2'], }); expect(buildChild).toHaveBeenCalledWith('c1'); @@ -244,27 +244,25 @@ describe('Basic Catalog Components', () => { }); it('Column renders children vertically', () => { - const { buildChild, view } = renderA2uiComponent(Column, 'col1', { - children: ['c1'] + const {buildChild, view} = renderA2uiComponent(Column, 'col1', { + children: ['c1'], }); expect(buildChild).toHaveBeenCalledWith('c1'); - expect(view.container.firstChild).toHaveStyle({ flexDirection: 'column' }); + expect(view.container.firstChild).toHaveStyle({flexDirection: 'column'}); }); it('List supports dynamic templates with scoped data context', () => { renderA2uiComponent( - List, - 'list1', - { - children: { componentId: 'itemComp', path: '/items' } + List, + 'list1', + { + children: {componentId: 'itemComp', path: '/items'}, }, { - initialData: { items: [{ n: 'A' }, { n: 'B' }] }, + initialData: {items: [{n: 'A'}, {n: 'B'}]}, additionalImpls: [Text], - additionalComponents: [ - new ComponentModel('itemComp', 'Text', { text: { path: 'n' } }) - ] - } + additionalComponents: [new ComponentModel('itemComp', 'Text', {text: {path: 'n'}})], + }, ); expect(screen.getByText('A')).toBeDefined(); @@ -272,7 +270,7 @@ describe('Basic Catalog Components', () => { }); it('Card renders its child', () => { - const { buildChild } = renderA2uiComponent(Card, 'card1', { child: 'c1' }); + const {buildChild} = renderA2uiComponent(Card, 'card1', {child: 'c1'}); expect(buildChild).toHaveBeenCalledWith('c1'); expect(screen.getByTestId('child-c1')).toBeDefined(); }); @@ -280,9 +278,9 @@ describe('Basic Catalog Components', () => { it('Tabs switches active tab content', () => { renderA2uiComponent(Tabs, 'tabs1', { tabs: [ - { title: 'Home', child: 'home_c' }, - { title: 'Settings', child: 'settings_c' } - ] + {title: 'Home', child: 'home_c'}, + {title: 'Settings', child: 'settings_c'}, + ], }); expect(screen.getByTestId('child-home_c')).toBeDefined(); @@ -297,7 +295,7 @@ describe('Basic Catalog Components', () => { it('Modal opens content on trigger click', () => { renderA2uiComponent(Modal, 'm1', { trigger: 't1', - content: 'c1' + content: 'c1', }); expect(screen.getByTestId('child-t1')).toBeDefined(); @@ -309,16 +307,16 @@ describe('Basic Catalog Components', () => { }); it('Divider renders a themed line', () => { - const { view } = renderA2uiComponent(Divider, 'd1', { axis: 'horizontal' }); - expect(view.container.firstChild).toHaveStyle({ height: 'var(--a2ui-border-width, 1px)' }); + const {view} = renderA2uiComponent(Divider, 'd1', {axis: 'horizontal'}); + expect(view.container.firstChild).toHaveStyle({height: 'var(--a2ui-border-width, 1px)'}); }); }); describe('Input Components', () => { it('CheckBox updates data', () => { - const { surface } = renderA2uiComponent(CheckBox, 'cb1', { + const {surface} = renderA2uiComponent(CheckBox, 'cb1', { label: 'Agree', - value: { path: '/agreed' } + value: {path: '/agreed'}, }); fireEvent.click(screen.getByLabelText('Agree')); @@ -326,27 +324,30 @@ describe('Basic Catalog Components', () => { }); it('Slider updates data', () => { - const { surface } = renderA2uiComponent(Slider, 's1', { + const {surface} = renderA2uiComponent(Slider, 's1', { label: 'Volume', - value: { path: '/vol' }, - max: 100 + value: {path: '/vol'}, + max: 100, }); - fireEvent.change(screen.getByLabelText('Volume'), { target: { value: '75' } }); + fireEvent.change(screen.getByLabelText('Volume'), {target: {value: '75'}}); expect(surface.dataModel.get('/vol')).toBe(75); }); it('ChoicePicker mutuallyExclusive selection', () => { - const { surface } = renderA2uiComponent(ChoicePicker, 'cp1', { + const {surface} = renderA2uiComponent(ChoicePicker, 'cp1', { label: 'Pick', - options: [{ label: 'A', value: 'a' }, { label: 'B', value: 'b' }], - value: { path: '/picked' }, - variant: 'mutuallyExclusive' + options: [ + {label: 'A', value: 'a'}, + {label: 'B', value: 'b'}, + ], + value: {path: '/picked'}, + variant: 'mutuallyExclusive', }); fireEvent.click(screen.getByLabelText('A')); expect(surface.dataModel.get('/picked')).toEqual(['a']); - + fireEvent.click(screen.getByLabelText('B')); expect(surface.dataModel.get('/picked')).toEqual(['b']); }); @@ -354,26 +355,34 @@ describe('Basic Catalog Components', () => { it('ChoicePicker filters options', () => { renderA2uiComponent(ChoicePicker, 'cp2', { label: 'Pick', - options: [{ label: 'Apple', value: 'apple' }, { label: 'Banana', value: 'banana' }], - value: { path: '/picked' }, - filterable: true + options: [ + {label: 'Apple', value: 'apple'}, + {label: 'Banana', value: 'banana'}, + ], + value: {path: '/picked'}, + filterable: true, }); expect(screen.getByText('Apple')).toBeDefined(); expect(screen.getByText('Banana')).toBeDefined(); - fireEvent.change(screen.getByPlaceholderText('Filter options...'), { target: { value: 'App' } }); + fireEvent.change(screen.getByPlaceholderText('Filter options...'), { + target: {value: 'App'}, + }); expect(screen.getByText('Apple')).toBeDefined(); expect(screen.queryByText('Banana')).toBeNull(); }); it('ChoicePicker renders chips and handles selection', () => { - const { surface } = renderA2uiComponent(ChoicePicker, 'cp3', { + const {surface} = renderA2uiComponent(ChoicePicker, 'cp3', { label: 'Pick', - options: [{ label: 'A', value: 'a' }, { label: 'B', value: 'b' }], - value: { path: '/picked' }, - displayStyle: 'chips' + options: [ + {label: 'A', value: 'a'}, + {label: 'B', value: 'b'}, + ], + value: {path: '/picked'}, + displayStyle: 'chips', }); fireEvent.click(screen.getByText('A')); @@ -381,13 +390,13 @@ describe('Basic Catalog Components', () => { }); it('DateTimeInput handles date changes', () => { - const { surface } = renderA2uiComponent(DateTimeInput, 'dt1', { + const {surface} = renderA2uiComponent(DateTimeInput, 'dt1', { label: 'When', - value: { path: '/date' }, - enableDate: true + value: {path: '/date'}, + enableDate: true, }); - fireEvent.change(screen.getByLabelText('When'), { target: { value: '2026-03-20' } }); + fireEvent.change(screen.getByLabelText('When'), {target: {value: '2026-03-20'}}); expect(surface.dataModel.get('/date')).toBe('2026-03-20'); }); }); diff --git a/renderers/react/tests/v0_9/integration-scenarios.test.tsx b/renderers/react/tests/v0_9/integration-scenarios.test.tsx index 855e11c27..b7419ec45 100644 --- a/renderers/react/tests/v0_9/integration-scenarios.test.tsx +++ b/renderers/react/tests/v0_9/integration-scenarios.test.tsx @@ -35,7 +35,7 @@ describe('Gallery Integration Tests', () => { render( - + , ); expect(screen.getByText('### Markdown Rendering')).toBeInTheDocument(); @@ -51,7 +51,7 @@ describe('Gallery Integration Tests', () => { render( - + , ); expect(screen.getByText('### Review pull request')).toBeInTheDocument(); @@ -68,7 +68,7 @@ describe('Gallery Integration Tests', () => { render( - + , ); const emailInput = screen.getByLabelText('Email') as HTMLInputElement; diff --git a/renderers/react/tests/v0_9/weight.test.tsx b/renderers/react/tests/v0_9/weight.test.tsx index ec6d7244e..0ced0d2fc 100644 --- a/renderers/react/tests/v0_9/weight.test.tsx +++ b/renderers/react/tests/v0_9/weight.test.tsx @@ -14,16 +14,10 @@ * limitations under the License. */ -import { describe, it, expect } from 'vitest'; -import { renderA2uiComponent } from '../utils'; -import { getWeightStyle } from '../../src/v0_9/catalog/basic/utils'; -import { - Image, - Text, - Card, - Row, - Column, -} from '../../src/v0_9/catalog/basic'; +import {describe, it, expect} from 'vitest'; +import {renderA2uiComponent} from '../utils'; +import {getWeightStyle} from '../../src/v0_9/catalog/basic/utils'; +import {Image, Text, Card, Row, Column} from '../../src/v0_9/catalog/basic'; describe('getWeightStyle', () => { it('returns empty object when weight is undefined', () => { @@ -31,38 +25,38 @@ describe('getWeightStyle', () => { }); it('returns flex, minWidth, and minHeight when weight is set', () => { - expect(getWeightStyle(2)).toEqual({ flex: '2', minWidth: 0, minHeight: 0 }); + expect(getWeightStyle(2)).toEqual({flex: '2', minWidth: 0, minHeight: 0}); }); it('handles fractional weights', () => { - expect(getWeightStyle(1.5)).toEqual({ flex: '1.5', minWidth: 0, minHeight: 0 }); + expect(getWeightStyle(1.5)).toEqual({flex: '1.5', minWidth: 0, minHeight: 0}); }); it('treats weight: 0 as a valid value (a child that does not grow)', () => { // Per spec, weight is "similar to flex-grow"; 0 is a meaningful value. - expect(getWeightStyle(0)).toEqual({ flex: '0', minWidth: 0, minHeight: 0 }); + expect(getWeightStyle(0)).toEqual({flex: '0', minWidth: 0, minHeight: 0}); }); }); describe('weight property is honored on basic catalog components', () => { - const cases: Array<{ name: string; impl: any; props: Record }> = [ - { name: 'Image', impl: Image, props: { url: 'https://example.com/x.png' } }, - { name: 'Text', impl: Text, props: { text: 'hello' } }, - { name: 'Card', impl: Card, props: { child: 'unknown-child' } }, - { name: 'Row', impl: Row, props: { children: [] } }, - { name: 'Column', impl: Column, props: { children: [] } }, + const cases: Array<{name: string; impl: any; props: Record}> = [ + {name: 'Image', impl: Image, props: {url: 'https://example.com/x.png'}}, + {name: 'Text', impl: Text, props: {text: 'hello'}}, + {name: 'Card', impl: Card, props: {child: 'unknown-child'}}, + {name: 'Row', impl: Row, props: {children: []}}, + {name: 'Column', impl: Column, props: {children: []}}, ]; - for (const { name, impl, props } of cases) { + for (const {name, impl, props} of cases) { it(`${name} applies flex when weight is set`, () => { - const { view } = renderA2uiComponent(impl, 'c1', { ...props, weight: 2 }); + const {view} = renderA2uiComponent(impl, 'c1', {...props, weight: 2}); const root = view.container.firstChild as HTMLElement; expect(root.style.flexGrow).toBe('2'); expect(root.style.minWidth).toBe('0'); }); it(`${name} does not apply flex when weight is unset`, () => { - const { view } = renderA2uiComponent(impl, 'c1', props); + const {view} = renderA2uiComponent(impl, 'c1', props); const root = view.container.firstChild as HTMLElement; expect(root.style.flex).toBe(''); }); diff --git a/renderers/react/tsup.config.ts b/renderers/react/tsup.config.ts index 26b5f1437..065912f30 100644 --- a/renderers/react/tsup.config.ts +++ b/renderers/react/tsup.config.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { defineConfig } from 'tsup'; +import {defineConfig} from 'tsup'; export default defineConfig([ // Main entry with DTS @@ -37,9 +37,9 @@ export default defineConfig([ }, // Styles entry without DTS (avoids symlink resolution issues) { - entry: { + entry: { 'styles/index': 'src/styles/index.ts', - 'v0_8/styles/index': 'src/v0_8/styles/index.ts' + 'v0_8/styles/index': 'src/v0_8/styles/index.ts', }, format: ['esm', 'cjs'], dts: false, diff --git a/renderers/react/visual-parity/PARITY.md b/renderers/react/visual-parity/PARITY.md index 1954704e9..9f1c7ef73 100644 --- a/renderers/react/visual-parity/PARITY.md +++ b/renderers/react/visual-parity/PARITY.md @@ -25,9 +25,13 @@ Each React component renders a wrapper div (representing `:host`) plus a section ```tsx // React component structure -
{/* ← :host equivalent */} -
{/* ← internal element */} - {children} {/* ← ::slotted(*) equivalent */} +
+ {' '} + {/* ← :host equivalent */} +
+ {' '} + {/* ← internal element */} + {children} {/* ← ::slotted(*) equivalent */}
``` @@ -40,16 +44,17 @@ This mirroring allows CSS selectors to target the same conceptual elements in bo Lit's Shadow DOM selectors need transformation for React's global CSS: -| Lit (Shadow DOM) | React (Light DOM) | -|-----------------------|------------------------------------------| -| `:host` | `.a2ui-surface .a2ui-{component}` | -| `section` | `.a2ui-surface .a2ui-{component} section`| -| `::slotted(*)` | `.a2ui-surface .a2ui-{component} section > *` | -| `element` (e.g., `h2`)| `:where(.a2ui-surface .a2ui-{component}) element` | +| Lit (Shadow DOM) | React (Light DOM) | +| ---------------------- | ------------------------------------------------- | +| `:host` | `.a2ui-surface .a2ui-{component}` | +| `section` | `.a2ui-surface .a2ui-{component} section` | +| `::slotted(*)` | `.a2ui-surface .a2ui-{component} section > *` | +| `element` (e.g., `h2`) | `:where(.a2ui-surface .a2ui-{component}) element` | ### Example: Card Component Lit's card.ts static styles: + ```css :host { display: block; @@ -72,6 +77,7 @@ section ::slotted(*) { ``` React's componentSpecificStyles equivalent: + ```css .a2ui-surface .a2ui-card { display: block; @@ -100,6 +106,7 @@ React's componentSpecificStyles equivalent: Shadow DOM provides natural style encapsulation with low specificity. A selector like `h2` inside Shadow DOM has specificity `(0,0,0,1)`. In React's global CSS, we need contextual selectors to scope styles: + ```css .a2ui-surface .a2ui-text h2 { ... } ``` @@ -109,10 +116,12 @@ This has specificity `(0,0,2,1)` — much higher than Lit's `(0,0,0,1)`. ### Why It Matters Utility classes like `.typography-w-500` have specificity `(0,0,1,0)`. In Lit: + - `h2 { font: inherit; }` = `(0,0,0,1)` — loses to utility class - `.typography-w-500` = `(0,0,1,0)` — **wins**, font-weight: 500 applied In React (without fix): + - `.a2ui-surface .a2ui-text h2 { font: inherit; }` = `(0,0,2,1)` — **wins** - `.typography-w-500` = `(0,0,1,0)` — loses, font-weight reset to 400 @@ -152,17 +161,20 @@ Use `:where()` when the Lit component has element selectors that should be overr ### Vite Cache Issues (504 Outdated Optimize Dep) If you see errors like: + ``` Failed to load resource: the server responded with a status of 504 (Outdated Optimize Dep) Uncaught TypeError: Failed to fetch dynamically imported module ``` This happens when Vite's dependency optimization cache becomes stale, typically after: + - Switching git branches - Updating dependencies - Rebuilding the React renderer **Fix:** Clear the Vite cache and restart: + ```bash # From renderers/react/visual-parity/ rm -rf node_modules/.vite react/node_modules/.vite lit/node_modules/.vite ../node_modules/.vite @@ -174,12 +186,14 @@ npm run dev:react # or dev:lit If you edit files in `renderers/react/src/` but the visual parity app doesn't reflect the changes, this is because the visual parity app imports from the **built** `@a2ui/react` package, not directly from source. **Why this happens:** + 1. Source changes are in `renderers/react/src/` 2. Visual parity app imports from `@a2ui/react/styles` (workspace package) 3. Vite pre-bundles workspace dependencies into `node_modules/.vite` 4. The pre-bundled cache still has the old built version **Fix:** Rebuild the package and clear Vite's cache: + ```bash # From renderers/react/visual-parity/ @@ -193,7 +207,7 @@ rm -rf react/node_modules/.vite node_modules/.vite npm run dev:react ``` -**Note:** Vite's HMR works for changes *within* the visual parity app, but changes to workspace dependencies require rebuilding + cache clearing. +**Note:** Vite's HMR works for changes _within_ the visual parity app, but changes to workspace dependencies require rebuilding + cache clearing. ## Testing Parity @@ -211,6 +225,7 @@ npm run dev ``` The tests: + 1. Load the same fixture in both Lit (localhost:5002) and React (localhost:5001) 2. Take screenshots of both renderers 3. Compare pixel differences using pixelmatch @@ -224,43 +239,43 @@ Each Lit component with `static styles` needs a corresponding entry in `componen ### ✅ Implemented (0% pixel diff in visual parity tests) -| Component | Lit File | Styles | Notes | -|-----------|----------|--------|-------| -| **Card** | `card.ts` | `:host`, `section`, `::slotted(*)` | Uses `> section` child combinator | -| **Text** | `text.ts` | `:host`, `h1-h5` (uses `:where()`) | Paragraph margin reset added | -| **Divider** | `divider.ts` | `:host`, `hr` | Added margin to match browser default | -| **TextField** | `text-field.ts` | `:host`, `input`, `label`, `textarea` | Uses `:where()` for element selectors. Multiline support added | -| **Button** | `button.ts` | `:host` | Simple display/flex | -| **Icon** | `icon.ts` | `:host` | Simple display/flex | -| **Column** | `column.ts` | `:host`, `section`, attribute selectors | Uses `data-alignment` and `data-distribution` | -| **Row** | `row.ts` | `:host`, `section`, attribute selectors | Uses `data-alignment` and `data-distribution` | -| **List** | `list.ts` | `:host`, `section`, `::slotted(*)` | All fixtures pass 0% including cards inside lists | -| **Image** | `image.ts` | `:host`, `img` | Uses `:where()` for `img`. All usage hints pass 0% | -| **Slider** | `slider.ts` | `:host`, `input[type="range"]` | Uses `:where()` for `input`. Basic slider passes 0% | -| **Tabs** | `tabs.ts` | `:host`, `section`, `button` | All fixtures pass 0% | -| **CheckBox** | `checkbox.ts` | `:host`, `input` | Uses `:where()` for `input`. Works via path binding | -| **DateTimeInput** | `datetime-input.ts` | `:host`, `input` | Uses `:where()` for `input`. React uses HTML5 inputs directly | -| **Modal** | `modal.ts` | `:host`, `dialog`, `#controls`, `button` | Renders dialog in place (no portal) to stay inside `.a2ui-surface`. Matches Lit: closed shows section with entry, open shows dialog | -| **Video** | `video.ts` | `:host`, `video` | Uses `:where()` for `video`. Minor pixel variance (~0.5%) due to native video element rendering | -| **AudioPlayer** | `audio.ts` | `:host`, `audio` | Uses `:where()` for `audio`. Note: Lit does NOT implement `description` property | -| **MultipleChoice** | `multiple-choice.ts` | `:host`, `select` | Uses `:where()` for `select`. Both renderers use `` dropdown | ### ⚠️ Lit Renderer Issues -| Component | Lit File | Issue | -|-----------|----------|-------| -| **Slider** | `slider.ts` | Value does not update when slider moves | -| **Divider** | `divider.ts` | Ignores `axis` property - always renders same orientation | -| **CheckBox** | `checkbox.ts` | Uses `.value` instead of `.checked` (line 100), so checked state only displays correctly when using path binding. Using `literalBoolean` with `false` causes component to not render. Visual parity tests pass using path binding. | -| **DateTimeInput** | `datetime-input.ts` | Uses `getMonth()` which is 0-indexed (0-11) without adding 1, causing issues in January and one month off otherwise. Also parses all values through `new Date()` constructor which does not accept time-only strings. React uses HTML5 inputs directly as they match A2UI format. | +| Component | Lit File | Issue | +| ------------------ | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Slider** | `slider.ts` | Value does not update when slider moves | +| **Divider** | `divider.ts` | Ignores `axis` property - always renders same orientation | +| **CheckBox** | `checkbox.ts` | Uses `.value` instead of `.checked` (line 100), so checked state only displays correctly when using path binding. Using `literalBoolean` with `false` causes component to not render. Visual parity tests pass using path binding. | +| **DateTimeInput** | `datetime-input.ts` | Uses `getMonth()` which is 0-indexed (0-11) without adding 1, causing issues in January and one month off otherwise. Also parses all values through `new Date()` constructor which does not accept time-only strings. React uses HTML5 inputs directly as they match A2UI format. | | **MultipleChoice** | `multiple-choice.ts` | `
\ No newline at end of file + diff --git a/samples/client/angular/projects/gallery/src/app/app.ts b/samples/client/angular/projects/gallery/src/app/app.ts index 0bb55c20c..04734cde4 100644 --- a/samples/client/angular/projects/gallery/src/app/app.ts +++ b/samples/client/angular/projects/gallery/src/app/app.ts @@ -14,9 +14,9 @@ * limitations under the License. */ -import { ChangeDetectionStrategy, Component, signal } from '@angular/core'; -import { LibraryComponent } from './features/library/library.component'; -import { GalleryComponent } from './features/gallery/gallery.component'; +import {ChangeDetectionStrategy, Component, signal} from '@angular/core'; +import {LibraryComponent} from './features/library/library.component'; +import {GalleryComponent} from './features/gallery/gallery.component'; @Component({ selector: 'app-root', diff --git a/samples/client/angular/projects/gallery/src/app/features/gallery/gallery.component.ts b/samples/client/angular/projects/gallery/src/app/features/gallery/gallery.component.ts index 1c83942a0..54971b39b 100644 --- a/samples/client/angular/projects/gallery/src/app/features/gallery/gallery.component.ts +++ b/samples/client/angular/projects/gallery/src/app/features/gallery/gallery.component.ts @@ -14,9 +14,9 @@ * limitations under the License. */ -import { ChangeDetectionStrategy, Component, ElementRef, ViewChild } from '@angular/core'; -import { CommonModule } from '@angular/common'; -import { Surface } from '@a2ui/angular'; +import {ChangeDetectionStrategy, Component, ElementRef, ViewChild} from '@angular/core'; +import {CommonModule} from '@angular/common'; +import {Surface} from '@a2ui/angular'; import * as Types from '@a2ui/web_core/types/types'; interface GallerySample { @@ -50,7 +50,7 @@ export class GalleryComponent { this.createComponent('Row', { children: [ this.createComponent('Image', { - url: { literalString: 'https://picsum.photos/id/11/300/300' }, + url: {literalString: 'https://picsum.photos/id/11/300/300'}, }), this.createComponent('Column', { children: [ @@ -66,7 +66,7 @@ export class GalleryComponent { this.createComponent('Row', { children: [ this.createComponent('Image', { - url: { literalString: 'https://picsum.photos/id/12/300/300' }, + url: {literalString: 'https://picsum.photos/id/12/300/300'}, }), this.createComponent('Column', { children: [ @@ -83,7 +83,7 @@ export class GalleryComponent { this.createComponent('Row', { children: [ this.createComponent('Image', { - url: { literalString: 'https://picsum.photos/id/13/300/300' }, + url: {literalString: 'https://picsum.photos/id/13/300/300'}, }), this.createComponent('Text', { text: { @@ -105,7 +105,7 @@ export class GalleryComponent { child: this.createComponent('Column', { children: [ this.createComponent('Image', { - url: { literalString: 'https://picsum.photos/id/10/600/300' }, + url: {literalString: 'https://picsum.photos/id/10/600/300'}, }), this.createComponent('Text', { text: { @@ -114,8 +114,8 @@ export class GalleryComponent { }, }), this.createComponent('Button', { - action: { type: 'submit' }, - child: this.createComponent('Text', { text: { literalString: 'Get Started' } }), + action: {type: 'submit'}, + child: this.createComponent('Text', {text: {literalString: 'Get Started'}}), }), ], alignment: 'center', @@ -132,32 +132,32 @@ export class GalleryComponent { this.createComponent('Row', { children: [ this.createComponent('TextField', { - label: { literalString: 'Name' }, + label: {literalString: 'Name'}, type: 'text', - text: { literalString: '' }, + text: {literalString: ''}, }), ], }), this.createComponent('Row', { children: [ this.createComponent('TextField', { - label: { literalString: 'Email Address' }, + label: {literalString: 'Email Address'}, type: 'email', - text: { literalString: '' }, + text: {literalString: ''}, }), ], }), this.createComponent('Row', { children: [ this.createComponent('TextField', { - label: { literalString: 'Message' }, - text: { literalString: '' }, + label: {literalString: 'Message'}, + text: {literalString: ''}, }), ], }), this.createComponent('Button', { - action: { type: 'submit' }, - child: this.createComponent('Text', { text: { literalString: 'Send Message' } }), + action: {type: 'submit'}, + child: this.createComponent('Text', {text: {literalString: 'Send Message'}}), }), ], }), @@ -178,7 +178,7 @@ export class GalleryComponent { this.activeSection = id; const element = document.getElementById('section-' + id); if (element) { - element.scrollIntoView({ behavior: 'smooth', block: 'start' }); + element.scrollIntoView({behavior: 'smooth', block: 'start'}); } } diff --git a/samples/client/angular/projects/gallery/src/app/features/gallery/gallery.css b/samples/client/angular/projects/gallery/src/app/features/gallery/gallery.css index 4f1fb9ecd..003307e83 100644 --- a/samples/client/angular/projects/gallery/src/app/features/gallery/gallery.css +++ b/samples/client/angular/projects/gallery/src/app/features/gallery/gallery.css @@ -26,7 +26,7 @@ dialog { transition: opacity 0.2s ease-out, transform 0.2s ease-out; - display: flex; + display: flex; flex-direction: column; } @@ -48,8 +48,8 @@ dialog[open]::backdrop { } dialog article { - flex-grow: 1; - display: flex; + flex-grow: 1; + display: flex; flex-direction: column; } @@ -78,8 +78,7 @@ dialog article { } .widget-gallery { - - display: flex; + display: flex; justify-content: space-around; flex-wrap: wrap; margin: 0 2em; @@ -89,8 +88,8 @@ dialog .dialog-content-grid { flex-grow: 1; display: grid; grid-template-columns: 1fr 1fr; - gap: 1.5rem; - height: 100%; + gap: 1.5rem; + height: 100%; align-items: stretch; } @@ -106,13 +105,13 @@ dialog .dialog-content-grid > .sample-surface { } .json-pane pre { - background-color: #272822; - color: #f8f8f2; - padding: 1em; - border-radius: 4px; + background-color: #272822; + color: #f8f8f2; + padding: 1em; + border-radius: 4px; flex-grow: 1; - overflow: auto; + overflow: auto; height: 100%; white-space: pre-wrap; - word-break: break-word; -} \ No newline at end of file + word-break: break-word; +} diff --git a/samples/client/angular/projects/gallery/src/app/features/gallery/gallery.html b/samples/client/angular/projects/gallery/src/app/features/gallery/gallery.html index 388bd927b..a0bbbbfdc 100644 --- a/samples/client/angular/projects/gallery/src/app/features/gallery/gallery.html +++ b/samples/client/angular/projects/gallery/src/app/features/gallery/gallery.html @@ -52,7 +52,7 @@

{{ sample.title }}

/>
-
{{ getJson(selectedSample.surface) }}
+
{{ getJson(selectedSample.surface) }}
} @else { diff --git a/samples/client/angular/projects/gallery/src/app/features/library/library.component.ts b/samples/client/angular/projects/gallery/src/app/features/library/library.component.ts index 83da7962c..d1fdc914f 100644 --- a/samples/client/angular/projects/gallery/src/app/features/library/library.component.ts +++ b/samples/client/angular/projects/gallery/src/app/features/library/library.component.ts @@ -14,8 +14,8 @@ * limitations under the License. */ -import { ChangeDetectionStrategy, Component, ElementRef, ViewChild } from '@angular/core'; -import { Surface } from '@a2ui/angular'; +import {ChangeDetectionStrategy, Component, ElementRef, ViewChild} from '@angular/core'; +import {Surface} from '@a2ui/angular'; import * as Types from '@a2ui/web_core/types/types'; @Component({ @@ -27,11 +27,11 @@ import * as Types from '@a2ui/web_core/types/types'; }) export class LibraryComponent { @ViewChild('dialog') dialog!: ElementRef; - selectedBlock: { name: string; surface: Types.Surface } | null = null; + selectedBlock: {name: string; surface: Types.Surface} | null = null; activeSection = ''; showJsonId: string | null = null; - openDialog(block: { name: string; surface: Types.Surface }) { + openDialog(block: {name: string; surface: Types.Surface}) { this.selectedBlock = block; this.dialog.nativeElement.showModal(); } @@ -44,7 +44,7 @@ export class LibraryComponent { this.activeSection = name; const element = document.getElementById('section-' + name); if (element) { - element.scrollIntoView({ behavior: 'smooth', block: 'start' }); + element.scrollIntoView({behavior: 'smooth', block: 'start'}); } } @@ -97,7 +97,7 @@ export class LibraryComponent { name: 'Card', tag: 'Layout', surface: this.createSingleComponentSurface('Card', { - child: this.createComponent('Text', { text: { literalString: 'Content inside a card' } }), + child: this.createComponent('Text', {text: {literalString: 'Content inside a card'}}), }), }, { @@ -105,9 +105,9 @@ export class LibraryComponent { tag: 'Layout', surface: this.createSingleComponentSurface('Column', { children: [ - this.createComponent('Text', { text: { literalString: 'Item 1' } }), - this.createComponent('Text', { text: { literalString: 'Item 2' } }), - this.createComponent('Text', { text: { literalString: 'Item 3' } }), + this.createComponent('Text', {text: {literalString: 'Item 1'}}), + this.createComponent('Text', {text: {literalString: 'Item 2'}}), + this.createComponent('Text', {text: {literalString: 'Item 3'}}), ], alignment: 'center', distribution: 'space-around', @@ -118,9 +118,9 @@ export class LibraryComponent { tag: 'Layout', surface: this.createSingleComponentSurface('Column', { children: [ - this.createComponent('Text', { text: { literalString: 'Above Divider' } }), + this.createComponent('Text', {text: {literalString: 'Above Divider'}}), this.createComponent('Divider', {}), - this.createComponent('Text', { text: { literalString: 'Below Divider' } }), + this.createComponent('Text', {text: {literalString: 'Below Divider'}}), ], }), }, @@ -129,9 +129,9 @@ export class LibraryComponent { tag: 'Layout', surface: this.createSingleComponentSurface('List', { children: [ - this.createComponent('Text', { text: { literalString: 'List Item 1' } }), - this.createComponent('Text', { text: { literalString: 'List Item 2' } }), - this.createComponent('Text', { text: { literalString: 'List Item 3' } }), + this.createComponent('Text', {text: {literalString: 'List Item 1'}}), + this.createComponent('Text', {text: {literalString: 'List Item 2'}}), + this.createComponent('Text', {text: {literalString: 'List Item 3'}}), ], direction: 'vertical', }), @@ -141,12 +141,12 @@ export class LibraryComponent { tag: 'Layout', surface: this.createSingleComponentSurface('Modal', { entryPointChild: this.createComponent('Button', { - action: { type: 'none' }, - child: this.createComponent('Text', { text: { literalString: 'Open Modal' } }), + action: {type: 'none'}, + child: this.createComponent('Text', {text: {literalString: 'Open Modal'}}), }), contentChild: this.createComponent('Card', { child: this.createComponent('Text', { - text: { literalString: 'This is the modal content.' }, + text: {literalString: 'This is the modal content.'}, }), }), }), @@ -156,9 +156,9 @@ export class LibraryComponent { tag: 'Layout', surface: this.createSingleComponentSurface('Row', { children: [ - this.createComponent('Text', { text: { literalString: 'Left' } }), - this.createComponent('Text', { text: { literalString: 'Center' } }), - this.createComponent('Text', { text: { literalString: 'Right' } }), + this.createComponent('Text', {text: {literalString: 'Left'}}), + this.createComponent('Text', {text: {literalString: 'Center'}}), + this.createComponent('Text', {text: {literalString: 'Right'}}), ], alignment: 'center', distribution: 'space-between', @@ -170,12 +170,12 @@ export class LibraryComponent { surface: this.createSingleComponentSurface('Tabs', { tabItems: [ { - title: { literalString: 'Tab 1' }, - child: this.createComponent('Text', { text: { literalString: 'Content for Tab 1' } }), + title: {literalString: 'Tab 1'}, + child: this.createComponent('Text', {text: {literalString: 'Content for Tab 1'}}), }, { - title: { literalString: 'Tab 2' }, - child: this.createComponent('Text', { text: { literalString: 'Content for Tab 2' } }), + title: {literalString: 'Tab 2'}, + child: this.createComponent('Text', {text: {literalString: 'Content for Tab 2'}}), }, ], }), @@ -185,10 +185,10 @@ export class LibraryComponent { tag: 'Layout', surface: this.createSingleComponentSurface('Column', { children: [ - this.createComponent('Heading', { text: { literalString: 'Heading Text' } }), - this.createComponent('Text', { text: { literalString: 'Standard body text.' } }), + this.createComponent('Heading', {text: {literalString: 'Heading Text'}}), + this.createComponent('Text', {text: {literalString: 'Standard body text.'}}), this.createComponent('Text', { - text: { literalString: 'Caption text' }, + text: {literalString: 'Caption text'}, usageHint: 'caption', }), ], @@ -199,7 +199,7 @@ export class LibraryComponent { name: 'AudioPlayer', tag: 'Media', surface: this.createSingleComponentSurface('AudioPlayer', { - url: { literalString: 'https://www.soundhelix.com/examples/mp3/SoundHelix-Song-1.mp3' }, + url: {literalString: 'https://www.soundhelix.com/examples/mp3/SoundHelix-Song-1.mp3'}, }), }, { @@ -207,9 +207,9 @@ export class LibraryComponent { tag: 'Media', surface: this.createSingleComponentSurface('Row', { children: [ - this.createComponent('Icon', { name: { literalString: 'home' } }), - this.createComponent('Icon', { name: { literalString: 'favorite' } }), - this.createComponent('Icon', { name: { literalString: 'settings' } }), + this.createComponent('Icon', {name: {literalString: 'home'}}), + this.createComponent('Icon', {name: {literalString: 'favorite'}}), + this.createComponent('Icon', {name: {literalString: 'settings'}}), ], distribution: 'space-around', }), @@ -218,7 +218,7 @@ export class LibraryComponent { name: 'Image', tag: 'Media', surface: this.createSingleComponentSurface('Image', { - url: { literalString: 'https://picsum.photos/id/10/300/200' }, + url: {literalString: 'https://picsum.photos/id/10/300/200'}, }), }, { @@ -237,14 +237,14 @@ export class LibraryComponent { surface: this.createSingleComponentSurface('Row', { children: [ this.createComponent('Button', { - label: { literalString: 'Primary' }, - action: { type: 'click' }, - child: this.createComponent('Text', { text: { literalString: 'Primary' } }), + label: {literalString: 'Primary'}, + action: {type: 'click'}, + child: this.createComponent('Text', {text: {literalString: 'Primary'}}), }), this.createComponent('Button', { - label: { literalString: 'Secondary' }, - action: { type: 'click' }, - child: this.createComponent('Text', { text: { literalString: 'Secondary' } }), + label: {literalString: 'Secondary'}, + action: {type: 'click'}, + child: this.createComponent('Text', {text: {literalString: 'Secondary'}}), }), ], distribution: 'space-around', @@ -256,12 +256,12 @@ export class LibraryComponent { surface: this.createSingleComponentSurface('Column', { children: [ this.createComponent('CheckBox', { - label: { literalString: 'Unchecked' }, - value: { literalBoolean: false }, + label: {literalString: 'Unchecked'}, + value: {literalBoolean: false}, }), this.createComponent('CheckBox', { - label: { literalString: 'Checked' }, - value: { literalBoolean: true }, + label: {literalString: 'Checked'}, + value: {literalBoolean: true}, }), ], }), @@ -274,12 +274,12 @@ export class LibraryComponent { this.createComponent('DateTimeInput', { enableDate: true, enableTime: false, - value: { literalString: '2025-12-09' }, + value: {literalString: '2025-12-09'}, }), this.createComponent('DateTimeInput', { enableDate: true, enableTime: true, - value: { literalString: '2025-12-09T12:00:00' }, + value: {literalString: '2025-12-09T12:00:00'}, }), ], }), @@ -289,18 +289,18 @@ export class LibraryComponent { tag: 'Inputs', surface: this.createSingleComponentSurface('MultipleChoice', { options: [ - { value: 'opt1', label: { literalString: 'Option 1' } }, - { value: 'opt2', label: { literalString: 'Option 2' } }, - { value: 'opt3', label: { literalString: 'Option 3' } }, + {value: 'opt1', label: {literalString: 'Option 1'}}, + {value: 'opt2', label: {literalString: 'Option 2'}}, + {value: 'opt3', label: {literalString: 'Option 3'}}, ], - selections: { literalString: 'opt1' }, + selections: {literalString: 'opt1'}, }), }, { name: 'Slider', tag: 'Inputs', surface: this.createSingleComponentSurface('Slider', { - value: { literalNumber: 50 }, + value: {literalNumber: 50}, minValue: 0, maxValue: 100, }), @@ -311,13 +311,13 @@ export class LibraryComponent { surface: this.createSingleComponentSurface('Column', { children: [ this.createComponent('TextField', { - label: { literalString: 'Standard Input' }, - text: { literalString: 'Some text' }, + label: {literalString: 'Standard Input'}, + text: {literalString: 'Some text'}, }), this.createComponent('TextField', { - label: { literalString: 'Password' }, + label: {literalString: 'Password'}, type: 'password', - text: { literalString: '' }, + text: {literalString: ''}, }), ], }), @@ -332,7 +332,7 @@ export class LibraryComponent { name: 'Card', surface: this.createSingleComponentSurface('Card', { child: this.createComponent('Text', { - text: { literalString: 'Content inside a card' }, + text: {literalString: 'Content inside a card'}, }), }), }, @@ -340,9 +340,9 @@ export class LibraryComponent { name: 'Column', surface: this.createSingleComponentSurface('Column', { children: [ - this.createComponent('Text', { text: { literalString: 'Item 1' } }), - this.createComponent('Text', { text: { literalString: 'Item 2' } }), - this.createComponent('Text', { text: { literalString: 'Item 3' } }), + this.createComponent('Text', {text: {literalString: 'Item 1'}}), + this.createComponent('Text', {text: {literalString: 'Item 2'}}), + this.createComponent('Text', {text: {literalString: 'Item 3'}}), ], alignment: 'center', distribution: 'space-around', @@ -352,9 +352,9 @@ export class LibraryComponent { name: 'Divider', surface: this.createSingleComponentSurface('Column', { children: [ - this.createComponent('Text', { text: { literalString: 'Above Divider' } }), + this.createComponent('Text', {text: {literalString: 'Above Divider'}}), this.createComponent('Divider', {}), - this.createComponent('Text', { text: { literalString: 'Below Divider' } }), + this.createComponent('Text', {text: {literalString: 'Below Divider'}}), ], }), }, @@ -362,9 +362,9 @@ export class LibraryComponent { name: 'List', surface: this.createSingleComponentSurface('List', { children: [ - this.createComponent('Text', { text: { literalString: 'List Item 1' } }), - this.createComponent('Text', { text: { literalString: 'List Item 2' } }), - this.createComponent('Text', { text: { literalString: 'List Item 3' } }), + this.createComponent('Text', {text: {literalString: 'List Item 1'}}), + this.createComponent('Text', {text: {literalString: 'List Item 2'}}), + this.createComponent('Text', {text: {literalString: 'List Item 3'}}), ], direction: 'vertical', }), @@ -373,12 +373,12 @@ export class LibraryComponent { name: 'Modal', surface: this.createSingleComponentSurface('Modal', { entryPointChild: this.createComponent('Button', { - action: { type: 'none' }, - child: this.createComponent('Text', { text: { literalString: 'Open Modal' } }), + action: {type: 'none'}, + child: this.createComponent('Text', {text: {literalString: 'Open Modal'}}), }), contentChild: this.createComponent('Card', { child: this.createComponent('Text', { - text: { literalString: 'This is the modal content.' }, + text: {literalString: 'This is the modal content.'}, }), }), }), @@ -387,9 +387,9 @@ export class LibraryComponent { name: 'Row', surface: this.createSingleComponentSurface('Row', { children: [ - this.createComponent('Text', { text: { literalString: 'Left' } }), - this.createComponent('Text', { text: { literalString: 'Center' } }), - this.createComponent('Text', { text: { literalString: 'Right' } }), + this.createComponent('Text', {text: {literalString: 'Left'}}), + this.createComponent('Text', {text: {literalString: 'Center'}}), + this.createComponent('Text', {text: {literalString: 'Right'}}), ], alignment: 'center', distribution: 'space-between', @@ -400,15 +400,15 @@ export class LibraryComponent { surface: this.createSingleComponentSurface('Tabs', { tabItems: [ { - title: { literalString: 'Tab 1' }, + title: {literalString: 'Tab 1'}, child: this.createComponent('Text', { - text: { literalString: 'Content for Tab 1' }, + text: {literalString: 'Content for Tab 1'}, }), }, { - title: { literalString: 'Tab 2' }, + title: {literalString: 'Tab 2'}, child: this.createComponent('Text', { - text: { literalString: 'Content for Tab 2' }, + text: {literalString: 'Content for Tab 2'}, }), }, ], @@ -418,10 +418,10 @@ export class LibraryComponent { name: 'Text', surface: this.createSingleComponentSurface('Column', { children: [ - this.createComponent('Heading', { text: { literalString: 'Heading Text' } }), - this.createComponent('Text', { text: { literalString: 'Standard body text.' } }), + this.createComponent('Heading', {text: {literalString: 'Heading Text'}}), + this.createComponent('Text', {text: {literalString: 'Standard body text.'}}), this.createComponent('Text', { - text: { literalString: 'Caption text' }, + text: {literalString: 'Caption text'}, usageHint: 'caption', }), ], @@ -435,16 +435,16 @@ export class LibraryComponent { { name: 'AudioPlayer', surface: this.createSingleComponentSurface('AudioPlayer', { - url: { literalString: 'https://www.soundhelix.com/examples/mp3/SoundHelix-Song-1.mp3' }, + url: {literalString: 'https://www.soundhelix.com/examples/mp3/SoundHelix-Song-1.mp3'}, }), }, { name: 'Icon', surface: this.createSingleComponentSurface('Row', { children: [ - this.createComponent('Icon', { name: { literalString: 'home' } }), - this.createComponent('Icon', { name: { literalString: 'favorite' } }), - this.createComponent('Icon', { name: { literalString: 'settings' } }), + this.createComponent('Icon', {name: {literalString: 'home'}}), + this.createComponent('Icon', {name: {literalString: 'favorite'}}), + this.createComponent('Icon', {name: {literalString: 'settings'}}), ], distribution: 'space-around', }), @@ -452,7 +452,7 @@ export class LibraryComponent { { name: 'Image', surface: this.createSingleComponentSurface('Image', { - url: { literalString: 'https://picsum.photos/id/10/300/200' }, + url: {literalString: 'https://picsum.photos/id/10/300/200'}, }), }, { @@ -474,14 +474,14 @@ export class LibraryComponent { surface: this.createSingleComponentSurface('Row', { children: [ this.createComponent('Button', { - label: { literalString: 'Primary' }, - action: { type: 'click' }, - child: this.createComponent('Text', { text: { literalString: 'Primary' } }), + label: {literalString: 'Primary'}, + action: {type: 'click'}, + child: this.createComponent('Text', {text: {literalString: 'Primary'}}), }), this.createComponent('Button', { - label: { literalString: 'Secondary' }, - action: { type: 'click' }, - child: this.createComponent('Text', { text: { literalString: 'Secondary' } }), + label: {literalString: 'Secondary'}, + action: {type: 'click'}, + child: this.createComponent('Text', {text: {literalString: 'Secondary'}}), }), ], distribution: 'space-around', @@ -492,12 +492,12 @@ export class LibraryComponent { surface: this.createSingleComponentSurface('Column', { children: [ this.createComponent('CheckBox', { - label: { literalString: 'Unchecked' }, - value: { literalBoolean: false }, + label: {literalString: 'Unchecked'}, + value: {literalBoolean: false}, }), this.createComponent('CheckBox', { - label: { literalString: 'Checked' }, - value: { literalBoolean: true }, + label: {literalString: 'Checked'}, + value: {literalBoolean: true}, }), ], }), @@ -509,12 +509,12 @@ export class LibraryComponent { this.createComponent('DateTimeInput', { enableDate: true, enableTime: false, - value: { literalString: '2025-12-09' }, + value: {literalString: '2025-12-09'}, }), this.createComponent('DateTimeInput', { enableDate: true, enableTime: true, - value: { literalString: '2025-12-09T12:00:00' }, + value: {literalString: '2025-12-09T12:00:00'}, }), ], }), @@ -523,17 +523,17 @@ export class LibraryComponent { name: 'MultipleChoice', surface: this.createSingleComponentSurface('MultipleChoice', { options: [ - { value: 'opt1', label: { literalString: 'Option 1' } }, - { value: 'opt2', label: { literalString: 'Option 2' } }, - { value: 'opt3', label: { literalString: 'Option 3' } }, + {value: 'opt1', label: {literalString: 'Option 1'}}, + {value: 'opt2', label: {literalString: 'Option 2'}}, + {value: 'opt3', label: {literalString: 'Option 3'}}, ], - selections: { literalString: 'opt1' }, + selections: {literalString: 'opt1'}, }), }, { name: 'Slider', surface: this.createSingleComponentSurface('Slider', { - value: { literalNumber: 50 }, + value: {literalNumber: 50}, minValue: 0, maxValue: 100, }), @@ -543,13 +543,13 @@ export class LibraryComponent { surface: this.createSingleComponentSurface('Column', { children: [ this.createComponent('TextField', { - label: { literalString: 'Standard Input' }, - text: { literalString: 'Some text' }, + label: {literalString: 'Standard Input'}, + text: {literalString: 'Some text'}, }), this.createComponent('TextField', { - label: { literalString: 'Password' }, + label: {literalString: 'Password'}, type: 'password', - text: { literalString: '' }, + text: {literalString: ''}, }), ], }), diff --git a/samples/client/angular/projects/gallery/src/app/features/library/library.css b/samples/client/angular/projects/gallery/src/app/features/library/library.css index 42a31f7f6..f1e4e66d1 100644 --- a/samples/client/angular/projects/gallery/src/app/features/library/library.css +++ b/samples/client/angular/projects/gallery/src/app/features/library/library.css @@ -26,7 +26,7 @@ dialog { transition: opacity 0.2s ease-out, transform 0.2s ease-out; - display: flex; + display: flex; flex-direction: column; } @@ -48,8 +48,8 @@ dialog[open]::backdrop { } dialog article { - flex-grow: 1; - display: flex; + flex-grow: 1; + display: flex; flex-direction: column; } @@ -87,8 +87,8 @@ dialog .dialog-content-grid { flex-grow: 1; display: grid; grid-template-columns: 1fr 1fr; - gap: 1.5rem; - height: 100%; + gap: 1.5rem; + height: 100%; align-items: stretch; } @@ -104,13 +104,13 @@ dialog .dialog-content-grid > .block-surface { } .json-pane pre { - background-color: #272822; - color: #f8f8f2; - padding: 1em; - border-radius: 4px; + background-color: #272822; + color: #f8f8f2; + padding: 1em; + border-radius: 4px; flex-grow: 1; - overflow: auto; + overflow: auto; height: 100%; white-space: pre-wrap; - word-break: break-word; -} \ No newline at end of file + word-break: break-word; +} diff --git a/samples/client/angular/projects/gallery/src/app/features/library/library.html b/samples/client/angular/projects/gallery/src/app/features/library/library.html index 879c18f80..4a552744d 100644 --- a/samples/client/angular/projects/gallery/src/app/features/library/library.html +++ b/samples/client/angular/projects/gallery/src/app/features/library/library.html @@ -13,7 +13,7 @@ See the License for the specific language governing permissions and limitations under the License. --> - +
-
{{ getJson(selectedBlock.surface) }}
+
{{ getJson(selectedBlock.surface) }}
} @else {

Please select a component from the gallery

} - \ No newline at end of file + diff --git a/samples/client/angular/projects/gallery/src/app/theme.ts b/samples/client/angular/projects/gallery/src/app/theme.ts index 626adc0f5..a3454b821 100644 --- a/samples/client/angular/projects/gallery/src/app/theme.ts +++ b/samples/client/angular/projects/gallery/src/app/theme.ts @@ -160,16 +160,16 @@ const video = { 'layout-el-cv': true, }; -const aLight = Styles.merge(a, { 'color-c-p30': true }); -const inputLight = Styles.merge(input, { 'color-c-n5': true }); -const textareaLight = Styles.merge(textarea, { 'color-c-n5': true }); -const buttonLight = Styles.merge(button, { 'color-c-n100': true }); -const h1Light = Styles.merge(h1, { 'color-c-n5': true }); -const h2Light = Styles.merge(h2, { 'color-c-n5': true }); -const h3Light = Styles.merge(h3, { 'color-c-n5': true }); -const bodyLight = Styles.merge(body, { 'color-c-n5': true }); -const pLight = Styles.merge(p, { 'color-c-n60': true }); -const preLight = Styles.merge(pre, { 'color-c-n35': true }); +const aLight = Styles.merge(a, {'color-c-p30': true}); +const inputLight = Styles.merge(input, {'color-c-n5': true}); +const textareaLight = Styles.merge(textarea, {'color-c-n5': true}); +const buttonLight = Styles.merge(button, {'color-c-n100': true}); +const h1Light = Styles.merge(h1, {'color-c-n5': true}); +const h2Light = Styles.merge(h2, {'color-c-n5': true}); +const h3Light = Styles.merge(h3, {'color-c-n5': true}); +const bodyLight = Styles.merge(body, {'color-c-n5': true}); +const pLight = Styles.merge(p, {'color-c-n60': true}); +const preLight = Styles.merge(pre, {'color-c-n35': true}); const orderedListLight = Styles.merge(orderedList, { 'color-c-n35': true, }); @@ -299,7 +299,7 @@ export const theme: Types.Theme = { 'layout-p-2': true, }, Modal: { - backdrop: { 'color-bbgc-p60_20': true }, + backdrop: {'color-bbgc-p60_20': true}, element: { 'border-br-2': true, 'color-bgc-p100': true, @@ -325,7 +325,7 @@ export const theme: Types.Theme = { }, Tabs: { container: {}, - controls: { all: {}, selected: {} }, + controls: {all: {}, selected: {}}, element: {}, }, Text: { diff --git a/samples/client/angular/projects/gallery/src/main.ts b/samples/client/angular/projects/gallery/src/main.ts index c1861c62b..3b5046207 100644 --- a/samples/client/angular/projects/gallery/src/main.ts +++ b/samples/client/angular/projects/gallery/src/main.ts @@ -14,8 +14,8 @@ * limitations under the License. */ -import { bootstrapApplication } from '@angular/platform-browser'; -import { appConfig } from './app/app.config'; -import { App } from './app/app'; +import {bootstrapApplication} from '@angular/platform-browser'; +import {appConfig} from './app/app.config'; +import {App} from './app/app'; -bootstrapApplication(App, appConfig).catch((err) => console.error(err)); +bootstrapApplication(App, appConfig).catch(err => console.error(err)); diff --git a/samples/client/angular/projects/gallery/src/styles.css b/samples/client/angular/projects/gallery/src/styles.css index ffb4234a4..06750be00 100644 --- a/samples/client/angular/projects/gallery/src/styles.css +++ b/samples/client/angular/projects/gallery/src/styles.css @@ -205,4 +205,4 @@ body { } .fade-in { animation: fade-in 0.2s ease-out forwards; -} \ No newline at end of file +} diff --git a/samples/client/angular/projects/gallery/tsconfig.app.json b/samples/client/angular/projects/gallery/tsconfig.app.json index dedf218fb..76bbc3982 100644 --- a/samples/client/angular/projects/gallery/tsconfig.app.json +++ b/samples/client/angular/projects/gallery/tsconfig.app.json @@ -2,14 +2,8 @@ "extends": "../../tsconfig.json", "compilerOptions": { "outDir": "../../out-tsc/app", - "types": [ - "node" - ] + "types": ["node"] }, - "include": [ - "src/**/*.ts" - ], - "exclude": [ - "src/**/*.spec.ts" - ] + "include": ["src/**/*.ts"], + "exclude": ["src/**/*.spec.ts"] } diff --git a/samples/client/angular/projects/lib/ng-package.json b/samples/client/angular/projects/lib/ng-package.json index bfd600420..a2d1bdda6 100644 --- a/samples/client/angular/projects/lib/ng-package.json +++ b/samples/client/angular/projects/lib/ng-package.json @@ -4,8 +4,5 @@ "lib": { "entryFile": "src/public-api.ts" }, - "allowedNonPeerDependencies": [ - "markdown-it", - "@a2ui/web_core" - ] + "allowedNonPeerDependencies": ["markdown-it", "@a2ui/web_core"] } diff --git a/samples/client/angular/projects/lib/tsconfig.lib.json b/samples/client/angular/projects/lib/tsconfig.lib.json index 7bd71c935..302c7bdc4 100644 --- a/samples/client/angular/projects/lib/tsconfig.lib.json +++ b/samples/client/angular/projects/lib/tsconfig.lib.json @@ -7,10 +7,6 @@ "inlineSources": true, "types": [] }, - "include": [ - "src/**/*.ts" - ], - "exclude": [ - "**/*.spec.ts" - ] + "include": ["src/**/*.ts"], + "exclude": ["**/*.spec.ts"] } diff --git a/samples/client/angular/projects/mcp_calculator/README.md b/samples/client/angular/projects/mcp_calculator/README.md index 498843813..7a79000e5 100644 --- a/samples/client/angular/projects/mcp_calculator/README.md +++ b/samples/client/angular/projects/mcp_calculator/README.md @@ -30,8 +30,8 @@ Once the application is running, open `http://localhost:4200/` in your browser. You can perform calculations using standard operations. When computing the expression, there are two different "equals" buttons: -- **Local JavaScript Calculation (💻 =)**: Click this button to evaluate the expression immediately using standard JavaScript in the browser. The result will appear in the calculator display. -- **MCP Agent Tool Call Calculation (🤖 =)**: Click this button to dispatch a tool call to the MCP server. The local display will reset, and the calculation will be handled by the agent asynchronously (the result will appear in the chat conversation). +- **Local JavaScript Calculation (💻 =)**: Click this button to evaluate the expression immediately using standard JavaScript in the browser. The result will appear in the calculator display. +- **MCP Agent Tool Call Calculation (🤖 =)**: Click this button to dispatch a tool call to the MCP server. The local display will reset, and the calculation will be handled by the agent asynchronously (the result will appear in the chat conversation). ## Features diff --git a/samples/client/angular/projects/mcp_calculator/src/a2ui-catalog/catalog.ts b/samples/client/angular/projects/mcp_calculator/src/a2ui-catalog/catalog.ts index 82f58fb61..f20087f16 100644 --- a/samples/client/angular/projects/mcp_calculator/src/a2ui-catalog/catalog.ts +++ b/samples/client/angular/projects/mcp_calculator/src/a2ui-catalog/catalog.ts @@ -14,13 +14,13 @@ * limitations under the License. */ -import { Catalog } from '@a2ui/angular'; -import { inputBinding } from '@angular/core'; +import {Catalog} from '@a2ui/angular'; +import {inputBinding} from '@angular/core'; export const DEMO_CATALOG = { McpApp: { - type: () => import('./mcp-app').then((r) => r.McpApp), - bindings: ({ properties }) => [ + type: () => import('./mcp-app').then(r => r.McpApp), + bindings: ({properties}) => [ inputBinding( 'content', () => ('content' in properties && properties['content']) || undefined, @@ -29,8 +29,8 @@ export const DEMO_CATALOG = { ], }, PongScoreBoard: { - type: () => import('./pong-scoreboard').then((r) => r.PongScoreBoard), - bindings: ({ properties }) => [ + type: () => import('./pong-scoreboard').then(r => r.PongScoreBoard), + bindings: ({properties}) => [ inputBinding( 'playerScore', () => ('playerScore' in properties && properties['playerScore']) || undefined, @@ -42,21 +42,22 @@ export const DEMO_CATALOG = { ], }, PongLayout: { - type: () => import('./pong-layout').then((r) => r.PongLayout), - bindings: ({ properties }) => [ + type: () => import('./pong-layout').then(r => r.PongLayout), + bindings: ({properties}) => [ inputBinding( 'mcpComponent', () => ('mcpComponent' in properties && properties['mcpComponent']) || undefined, ), inputBinding( 'scoreboardComponent', - () => ('scoreboardComponent' in properties && properties['scoreboardComponent']) || undefined, + () => + ('scoreboardComponent' in properties && properties['scoreboardComponent']) || undefined, ), ], }, Column: { - type: () => import('@a2ui/angular').then((r) => r.Column), - bindings: ({ properties }) => [ + type: () => import('@a2ui/angular').then(r => r.Column), + bindings: ({properties}) => [ inputBinding( 'alignment', () => ('alignment' in properties && properties['alignment']) || undefined, diff --git a/samples/client/angular/projects/mcp_calculator/src/a2ui-catalog/mcp-app.ts b/samples/client/angular/projects/mcp_calculator/src/a2ui-catalog/mcp-app.ts index 2b5f77815..02073b88c 100644 --- a/samples/client/angular/projects/mcp_calculator/src/a2ui-catalog/mcp-app.ts +++ b/samples/client/angular/projects/mcp_calculator/src/a2ui-catalog/mcp-app.ts @@ -14,7 +14,7 @@ limitations under the License. */ -import { DynamicComponent } from '@a2ui/angular'; +import {DynamicComponent} from '@a2ui/angular'; import * as Primitives from '@a2ui/web_core/types/primitives'; import * as Types from '@a2ui/web_core/types/types'; import { @@ -36,7 +36,7 @@ import { Signal, viewChild, } from '@angular/core'; -import { DomSanitizer, SafeHtml, SafeResourceUrl } from '@angular/platform-browser'; +import {DomSanitizer, SafeHtml, SafeResourceUrl} from '@angular/platform-browser'; @Component({ selector: 'a2ui-mcp-app', @@ -69,10 +69,7 @@ import { DomSanitizer, SafeHtml, SafeResourceUrl } from '@angular/platform-brows } `, }) -export class McpApp - extends DynamicComponent - implements OnDestroy, OnInit -{ +export class McpApp extends DynamicComponent implements OnDestroy, OnInit { private readonly sanitizer = inject(DomSanitizer); readonly content = input.required(); @@ -88,10 +85,12 @@ export class McpApp const rawContent = this.resolvedContent(); const bridge = this.appBridge(); if (bridge && rawContent) { - bridge.sendSandboxResourceReady({ - html: rawContent, - sandbox: 'allow-scripts', - }).catch(err => console.error('Failed to update sandbox content:', err)); + bridge + .sendSandboxResourceReady({ + html: rawContent, + sandbox: 'allow-scripts', + }) + .catch(err => console.error('Failed to update sandbox content:', err)); } }); @@ -102,7 +101,7 @@ export class McpApp ); protected readonly iframeSrc = signal( - this.sanitizer.bypassSecurityTrustResourceUrl('about:blank') + this.sanitizer.bypassSecurityTrustResourceUrl('about:blank'), ); private iframe = viewChild.required>('iframe'); @@ -142,7 +141,6 @@ export class McpApp window.addEventListener('message', this.messageHandler); - // Check for query param to opt-out of origin toggle (for testing) const urlParams = new URLSearchParams(window.location.search); const disableSecuritySelfTest = urlParams.get('disable_security_self_test') === 'true'; @@ -152,9 +150,7 @@ export class McpApp if (disableSecuritySelfTest) { sandboxUrl += '?disable_security_self_test=true'; } - this.iframeSrc.set( - this.sanitizer.bypassSecurityTrustResourceUrl(sandboxUrl), - ); + this.iframeSrc.set(this.sanitizer.bypassSecurityTrustResourceUrl(sandboxUrl)); } private async initializeBridge() { @@ -169,7 +165,7 @@ export class McpApp const emptyMcpClient = null; const bridge = new AppBridge( emptyMcpClient, - { name: 'MCP Calculator', version: '1.0.0' }, + {name: 'MCP Calculator', version: '1.0.0'}, { openLinks: {}, logging: {}, @@ -177,7 +173,7 @@ export class McpApp }, ); - bridge.onloggingmessage = (params) => { + bridge.onloggingmessage = params => { console.log(`[MCP App Log] ${params.level}:`, params.data); }; @@ -185,7 +181,7 @@ export class McpApp console.log('MCP App Initialized'); }; - bridge.onsizechange = ({ width, height }) => { + bridge.onsizechange = ({width, height}) => { // TODO: Implement dynamic resizing // Reference implementation in mcp-apps-custom-component.ts: // - Listen for size changes from the embedded app @@ -199,7 +195,7 @@ export class McpApp console.log(`[MCP App] Resize requested: ${width}x${height}`); }; - bridge.oncalltool = async (params) => { + bridge.oncalltool = async params => { console.log(`[MCP App] Tool call requested: ${params.name}`, params); if (!this.allowedTools().includes(params.name)) { @@ -208,16 +204,16 @@ export class McpApp } const args = params.arguments || {}; - + // Map arguments to A2UI Action context const context: any[] = []; for (const [key, value] of Object.entries(args)) { if (typeof value === 'number') { - context.push({ key, value: { literalNumber: value } }); + context.push({key, value: {literalNumber: value}}); } else if (typeof value === 'string') { - context.push({ key, value: { literalString: value } }); + context.push({key, value: {literalString: value}}); } else if (typeof value === 'boolean') { - context.push({ key, value: { literalBoolean: value } }); + context.push({key, value: {literalBoolean: value}}); } } @@ -229,12 +225,10 @@ export class McpApp console.log('Sending action:', action); // Dispatch action asynchronously to the host/agent - super.sendAction(action).catch((err) => - console.error('Failed to send action:', err), - ); + super.sendAction(action).catch(err => console.error('Failed to send action:', err)); // Return empty result immediately (calculator UI can forget about it) - return { content: [] }; + return {content: []}; }; // Connect the bridge diff --git a/samples/client/angular/projects/mcp_calculator/src/a2ui-catalog/pong-layout.ts b/samples/client/angular/projects/mcp_calculator/src/a2ui-catalog/pong-layout.ts index c7a2ff7c2..97a6b929f 100644 --- a/samples/client/angular/projects/mcp_calculator/src/a2ui-catalog/pong-layout.ts +++ b/samples/client/angular/projects/mcp_calculator/src/a2ui-catalog/pong-layout.ts @@ -13,26 +13,38 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { DynamicComponent } from '../../../../projects/lib/src/public-api'; -import { Renderer } from '../../../../projects/lib/src/v0_8/rendering/renderer'; -import { Component, input } from '@angular/core'; +import {DynamicComponent} from '../../../../projects/lib/src/public-api'; +import {Renderer} from '../../../../projects/lib/src/v0_8/rendering/renderer'; +import {Component, input} from '@angular/core'; @Component({ selector: 'a2ui-pong-layout', standalone: true, imports: [Renderer], template: ` -
-
- +
+
+
- +
`, }) export class PongLayout extends DynamicComponent { - readonly mcpComponent = input(null); - readonly scoreboardComponent = input(null); + readonly mcpComponent = input(null); + readonly scoreboardComponent = input(null); } diff --git a/samples/client/angular/projects/mcp_calculator/src/a2ui-catalog/pong-scoreboard.ts b/samples/client/angular/projects/mcp_calculator/src/a2ui-catalog/pong-scoreboard.ts index 34bd0f9fa..fe8a461a7 100644 --- a/samples/client/angular/projects/mcp_calculator/src/a2ui-catalog/pong-scoreboard.ts +++ b/samples/client/angular/projects/mcp_calculator/src/a2ui-catalog/pong-scoreboard.ts @@ -14,15 +14,10 @@ * limitations under the License. */ -import { DynamicComponent } from '@a2ui/angular'; +import {DynamicComponent} from '@a2ui/angular'; import * as Primitives from '@a2ui/web_core/types/primitives'; import * as Types from '@a2ui/web_core/types/types'; -import { - ChangeDetectionStrategy, - Component, - computed, - input, -} from '@angular/core'; +import {ChangeDetectionStrategy, Component, computed, input} from '@angular/core'; @Component({ selector: 'a2ui-pong-scoreboard', @@ -85,12 +80,12 @@ import { padding: 1rem; border: 1px solid #e8eaed; transition: all 0.2s ease; - box-shadow: 0 1px 3px rgba(0,0,0,0.05); + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05); width: 100%; box-sizing: border-box; } .score-card:hover { - box-shadow: 0 4px 6px rgba(0,0,0,0.08); + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.08); transform: translateY(-2px); border-color: #d2e3fc; } @@ -116,9 +111,7 @@ import { } `, template: ` -
- A2UI Native -
+
A2UI Native
Player @@ -131,16 +124,14 @@ import {
`, }) -export class PongScoreBoard - extends DynamicComponent -{ +export class PongScoreBoard extends DynamicComponent { readonly playerScore = input(); - protected readonly resolvedPlayerScore = computed(() => - super.resolvePrimitive(this.playerScore() ?? null) ?? 0 + protected readonly resolvedPlayerScore = computed( + () => super.resolvePrimitive(this.playerScore() ?? null) ?? 0, ); readonly cpuScore = input(); - protected readonly resolvedCpuScore = computed(() => - super.resolvePrimitive(this.cpuScore() ?? null) ?? 0 + protected readonly resolvedCpuScore = computed( + () => super.resolvePrimitive(this.cpuScore() ?? null) ?? 0, ); } diff --git a/samples/client/angular/projects/mcp_calculator/src/app/app.config.server.ts b/samples/client/angular/projects/mcp_calculator/src/app/app.config.server.ts index 125e66bed..b27c47b15 100644 --- a/samples/client/angular/projects/mcp_calculator/src/app/app.config.server.ts +++ b/samples/client/angular/projects/mcp_calculator/src/app/app.config.server.ts @@ -14,10 +14,10 @@ * limitations under the License. */ -import { ApplicationConfig, mergeApplicationConfig } from '@angular/core'; -import { provideServerRendering, withRoutes } from '@angular/ssr'; -import { appConfig } from './app.config'; -import { serverRoutes } from './app.routes.server'; +import {ApplicationConfig, mergeApplicationConfig} from '@angular/core'; +import {provideServerRendering, withRoutes} from '@angular/ssr'; +import {appConfig} from './app.config'; +import {serverRoutes} from './app.routes.server'; const serverConfig: ApplicationConfig = { providers: [provideServerRendering(withRoutes(serverRoutes))], diff --git a/samples/client/angular/projects/mcp_calculator/src/app/app.config.ts b/samples/client/angular/projects/mcp_calculator/src/app/app.config.ts index 60835c163..13e1ac720 100644 --- a/samples/client/angular/projects/mcp_calculator/src/app/app.config.ts +++ b/samples/client/angular/projects/mcp_calculator/src/app/app.config.ts @@ -25,9 +25,9 @@ import { provideBrowserGlobalErrorListeners, provideZonelessChangeDetection, } from '@angular/core'; -import { provideClientHydration, withEventReplay } from '@angular/platform-browser'; -import { DEMO_CATALOG } from '../a2ui-catalog/catalog'; -import { A2aServiceImpl } from '../services/a2a-service-impl'; +import {provideClientHydration, withEventReplay} from '@angular/platform-browser'; +import {DEMO_CATALOG} from '../a2ui-catalog/catalog'; +import {A2aServiceImpl} from '../services/a2a-service-impl'; export const appConfig: ApplicationConfig = { providers: [ diff --git a/samples/client/angular/projects/mcp_calculator/src/app/app.html b/samples/client/angular/projects/mcp_calculator/src/app/app.html index bdf5e9b3b..65fcee600 100644 --- a/samples/client/angular/projects/mcp_calculator/src/app/app.html +++ b/samples/client/angular/projects/mcp_calculator/src/app/app.html @@ -20,27 +20,23 @@
-
-
- {{ agentName() }} -
-
-
- -

- I summon a calculator app served from an MCP server over A2UI. -

-
- - +
+
+ {{ agentName() }}
+
+
+ +

I summon a calculator app served from an MCP server over A2UI.

+
+ + +
- - diff --git a/samples/client/angular/projects/mcp_calculator/src/app/app.routes.server.ts b/samples/client/angular/projects/mcp_calculator/src/app/app.routes.server.ts index 6e22a0113..417bd7b43 100644 --- a/samples/client/angular/projects/mcp_calculator/src/app/app.routes.server.ts +++ b/samples/client/angular/projects/mcp_calculator/src/app/app.routes.server.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { RenderMode, ServerRoute } from '@angular/ssr'; +import {RenderMode, ServerRoute} from '@angular/ssr'; export const serverRoutes: ServerRoute[] = [ { diff --git a/samples/client/angular/projects/mcp_calculator/src/app/app.scss b/samples/client/angular/projects/mcp_calculator/src/app/app.scss index dff7732af..570c4fc61 100644 --- a/samples/client/angular/projects/mcp_calculator/src/app/app.scss +++ b/samples/client/angular/projects/mcp_calculator/src/app/app.scss @@ -13,7 +13,7 @@ // limitations under the License. main { - height: 100vh; + height: 100vh; } .empty-history { @@ -44,12 +44,7 @@ main { } .agent-name { - background: linear-gradient( - 90deg, - #217bfe 28.03%, - #078efb 49.56%, - #ac87eb 71.1% - ); + background: linear-gradient(90deg, #217bfe 28.03%, #078efb 49.56%, #ac87eb 71.1%); background-clip: text; color: transparent; } @@ -64,17 +59,17 @@ main { .chip { // Override Material button styles to look like a chip - border-radius: 100px !important; // Force pill shape - padding: 10px 16px !important; - display: inline-flex !important; - align-items: center !important; - height: auto !important; - line-height: 25px !important; // Match icon height + border-radius: 100px !important; // Force pill shape + padding: 10px 16px !important; + display: inline-flex !important; + align-items: center !important; + height: auto !important; + line-height: 25px !important; // Match icon height .material-icons-outlined { font-size: 20px; margin-right: 8px; - line-height: 1; // Prevent icon from affecting line height - position: relative; - top: 4px; // Move icon down slightly to match text + line-height: 1; // Prevent icon from affecting line height + position: relative; + top: 4px; // Move icon down slightly to match text } -} \ No newline at end of file +} diff --git a/samples/client/angular/projects/mcp_calculator/src/app/app.ts b/samples/client/angular/projects/mcp_calculator/src/app/app.ts index 2f91c641d..2ff7ad0cc 100644 --- a/samples/client/angular/projects/mcp_calculator/src/app/app.ts +++ b/samples/client/angular/projects/mcp_calculator/src/app/app.ts @@ -14,16 +14,10 @@ * limitations under the License. */ -import { A2aChatCanvas } from '@a2a_chat_canvas/a2a-chat-canvas'; -import { ChatService } from '@a2a_chat_canvas/services/chat-service'; -import { - ChangeDetectionStrategy, - Component, - inject, - signal, -} from '@angular/core'; -import { MatButtonModule } from '@angular/material/button'; - +import {A2aChatCanvas} from '@a2a_chat_canvas/a2a-chat-canvas'; +import {ChatService} from '@a2a_chat_canvas/services/chat-service'; +import {ChangeDetectionStrategy, Component, inject, signal} from '@angular/core'; +import {MatButtonModule} from '@angular/material/button'; @Component({ selector: 'app-root', @@ -34,11 +28,9 @@ import { MatButtonModule } from '@angular/material/button'; changeDetection: ChangeDetectionStrategy.Eager, }) export class App { - protected readonly agentName = signal('MCP Calculator'); private readonly chatService = inject(ChatService); - sendMessage(text: string) { this.chatService.sendMessage(text); } diff --git a/samples/client/angular/projects/mcp_calculator/src/main.server.ts b/samples/client/angular/projects/mcp_calculator/src/main.server.ts index fcfe120c8..5bad09f44 100644 --- a/samples/client/angular/projects/mcp_calculator/src/main.server.ts +++ b/samples/client/angular/projects/mcp_calculator/src/main.server.ts @@ -14,9 +14,9 @@ * limitations under the License. */ -import { BootstrapContext, bootstrapApplication } from '@angular/platform-browser'; -import { App } from './app/app'; -import { config } from './app/app.config.server'; +import {BootstrapContext, bootstrapApplication} from '@angular/platform-browser'; +import {App} from './app/app'; +import {config} from './app/app.config.server'; const bootstrap = (context: BootstrapContext) => bootstrapApplication(App, config, context); diff --git a/samples/client/angular/projects/mcp_calculator/src/main.ts b/samples/client/angular/projects/mcp_calculator/src/main.ts index 3c4480110..98aa73d70 100644 --- a/samples/client/angular/projects/mcp_calculator/src/main.ts +++ b/samples/client/angular/projects/mcp_calculator/src/main.ts @@ -14,8 +14,8 @@ * limitations under the License. */ -import { bootstrapApplication } from '@angular/platform-browser'; -import { App } from './app/app'; -import { appConfig } from './app/app.config'; +import {bootstrapApplication} from '@angular/platform-browser'; +import {App} from './app/app'; +import {appConfig} from './app/app.config'; -bootstrapApplication(App, appConfig).catch((err) => console.error(err)); +bootstrapApplication(App, appConfig).catch(err => console.error(err)); diff --git a/samples/client/angular/projects/mcp_calculator/src/server.ts b/samples/client/angular/projects/mcp_calculator/src/server.ts index 3a15c8ec2..17a4c19cc 100644 --- a/samples/client/angular/projects/mcp_calculator/src/server.ts +++ b/samples/client/angular/projects/mcp_calculator/src/server.ts @@ -14,8 +14,8 @@ * limitations under the License. */ -import { MessageSendParams, Part, SendMessageResponse } from '@a2a-js/sdk'; -import { A2AClient } from '@a2a-js/sdk/client'; +import {MessageSendParams, Part, SendMessageResponse} from '@a2a-js/sdk'; +import {A2AClient} from '@a2a-js/sdk/client'; import { AngularNodeAppEngine, createNodeRequestHandler, @@ -23,8 +23,8 @@ import { writeResponseToNodeResponse, } from '@angular/ssr/node'; import express from 'express'; -import { join } from 'node:path'; -import { v4 as uuidv4 } from 'uuid'; +import {join} from 'node:path'; +import {v4 as uuidv4} from 'uuid'; const browserDistFolder = join(import.meta.dirname, '../browser'); const app = express(); @@ -42,7 +42,7 @@ app.use( app.post('/a2a', (req, res) => { let originalBody = ''; - req.on('data', (chunk) => { + req.on('data', chunk => { originalBody += chunk.toString(); }); @@ -74,7 +74,7 @@ app.post('/a2a', (req, res) => { try { client = await createOrGetClient(); } catch (error) { - res.status(500).json({ error: 'Failed to create A2A client.' }); + res.status(500).json({error: 'Failed to create A2A client.'}); return; } @@ -82,14 +82,14 @@ app.post('/a2a', (req, res) => { try { response = await client.sendMessage(sendParams); } catch (error) { - res.status(500).json({ error: 'Failed to send message.' }); + res.status(500).json({error: 'Failed to send message.'}); return; } res.set('Cache-Control', 'no-store'); if ('error' in response) { - res.status(500).json({ error: JSON.stringify(response.error) }); + res.status(500).json({error: JSON.stringify(response.error)}); return; } @@ -103,27 +103,27 @@ app.get('/a2a/agent-card', async (req, res) => { 'http://localhost:10006/.well-known/agent-card.json', ); if (!response.ok) { - res.status(response.status).json({ error: 'Failed to fetch agent card' }); + res.status(response.status).json({error: 'Failed to fetch agent card'}); return; } const card = await response.json(); res.json(card); } catch (error) { console.error('Error fetching agent card:', error); - res.status(500).json({ error: 'Internal server error' }); + res.status(500).json({error: 'Internal server error'}); } }); app.use((req, res, next) => { angularApp .handle(req) - .then((response) => (response ? writeResponseToNodeResponse(response, res) : next())) + .then(response => (response ? writeResponseToNodeResponse(response, res) : next())) .catch(next); }); if (isMainModule(import.meta.url) || process.env['pm_id']) { const port = process.env['PORT'] || 4000; - app.listen(port, (error) => { + app.listen(port, error => { if (error) { throw error; } @@ -135,7 +135,7 @@ if (isMainModule(import.meta.url) || process.env['pm_id']) { async function fetchWithCustomHeader(url: string | URL | Request, init?: RequestInit) { const headers = new Headers(init?.headers); headers.set('X-A2A-Extensions', 'https://a2ui.org/a2a-extension/a2ui/v0.8'); - const newInit = { ...init, headers }; + const newInit = {...init, headers}; return fetch(url, newInit); } diff --git a/samples/client/angular/projects/mcp_calculator/src/services/a2a-service-impl.ts b/samples/client/angular/projects/mcp_calculator/src/services/a2a-service-impl.ts index 55ac70497..54634b5c1 100644 --- a/samples/client/angular/projects/mcp_calculator/src/services/a2a-service-impl.ts +++ b/samples/client/angular/projects/mcp_calculator/src/services/a2a-service-impl.ts @@ -14,18 +14,17 @@ * limitations under the License. */ -import { AgentCard, Part, SendMessageSuccessResponse } from '@a2a-js/sdk'; -import { A2aService } from '@a2a_chat_canvas/interfaces/a2a-service'; -import { Injectable } from '@angular/core'; +import {AgentCard, Part, SendMessageSuccessResponse} from '@a2a-js/sdk'; +import {A2aService} from '@a2a_chat_canvas/interfaces/a2a-service'; +import {Injectable} from '@angular/core'; @Injectable({ providedIn: 'root', }) export class A2aServiceImpl implements A2aService { - async sendMessage(parts: Part[], signal?: AbortSignal): Promise { const response = await fetch('/a2a', { - body: JSON.stringify({ parts: parts }), + body: JSON.stringify({parts: parts}), method: 'POST', signal, }); @@ -35,7 +34,7 @@ export class A2aServiceImpl implements A2aService { return data; } - const error = (await response.json()) as { error: string }; + const error = (await response.json()) as {error: string}; throw new Error(error.error); } @@ -44,7 +43,7 @@ export class A2aServiceImpl implements A2aService { if (!response.ok) { throw new Error('Failed to fetch agent card'); } - const card = await response.json() as AgentCard; + const card = (await response.json()) as AgentCard; return card; } } diff --git a/samples/client/angular/projects/mcp_calculator/src/styles.scss b/samples/client/angular/projects/mcp_calculator/src/styles.scss index e6c8f2610..f89c852c7 100644 --- a/samples/client/angular/projects/mcp_calculator/src/styles.scss +++ b/samples/client/angular/projects/mcp_calculator/src/styles.scss @@ -14,7 +14,6 @@ @use '@angular/material' as mat; - @mixin styled-scrollbar { ::-webkit-scrollbar { width: 0.5rem; @@ -39,11 +38,13 @@ html { color-scheme: light dark; - @include mat.theme(( - color: mat.$blue-palette, - typography: Roboto, - density: 0 - )); + @include mat.theme( + ( + color: mat.$blue-palette, + typography: Roboto, + density: 0, + ) + ); @include styled-scrollbar; } @@ -201,4 +202,4 @@ body { padding: 0; width: 100svw; height: 100svh; -} \ No newline at end of file +} diff --git a/samples/client/angular/projects/mcp_calculator/tsconfig.app.json b/samples/client/angular/projects/mcp_calculator/tsconfig.app.json index 3175e5ae7..6c6f9b1d1 100644 --- a/samples/client/angular/projects/mcp_calculator/tsconfig.app.json +++ b/samples/client/angular/projects/mcp_calculator/tsconfig.app.json @@ -4,14 +4,8 @@ "extends": "../../tsconfig.json", "compilerOptions": { "outDir": "../../out-tsc/app", - "types": [ - "node" - ] + "types": ["node"] }, - "include": [ - "src/**/*.ts" - ], - "exclude": [ - "src/**/*.spec.ts" - ] + "include": ["src/**/*.ts"], + "exclude": ["src/**/*.spec.ts"] } diff --git a/samples/client/angular/projects/mcp_calculator/tsconfig.spec.json b/samples/client/angular/projects/mcp_calculator/tsconfig.spec.json index 0feea88ed..43ba0b186 100644 --- a/samples/client/angular/projects/mcp_calculator/tsconfig.spec.json +++ b/samples/client/angular/projects/mcp_calculator/tsconfig.spec.json @@ -4,11 +4,7 @@ "extends": "../../tsconfig.json", "compilerOptions": { "outDir": "../../out-tsc/spec", - "types": [ - "jasmine" - ] + "types": ["jasmine"] }, - "include": [ - "src/**/*.ts" - ] + "include": ["src/**/*.ts"] } diff --git a/samples/client/angular/projects/orchestrator/README.md b/samples/client/angular/projects/orchestrator/README.md index 85789a8ea..e64ef9428 100644 --- a/samples/client/angular/projects/orchestrator/README.md +++ b/samples/client/angular/projects/orchestrator/README.md @@ -2,7 +2,7 @@ Sample application using the Chat-Canvas component orchestrating multiple A2A and A2UI Agents. -This angular app connects to an Orchastrator Agent which takes user messages and delegates tasks to its subagents based on the assessed context. +This angular app connects to an Orchastrator Agent which takes user messages and delegates tasks to its subagents based on the assessed context. ## Prerequisites diff --git a/samples/client/angular/projects/orchestrator/src/a2ui-catalog/catalog.ts b/samples/client/angular/projects/orchestrator/src/a2ui-catalog/catalog.ts index 9d13204e7..9ae596a57 100644 --- a/samples/client/angular/projects/orchestrator/src/a2ui-catalog/catalog.ts +++ b/samples/client/angular/projects/orchestrator/src/a2ui-catalog/catalog.ts @@ -14,13 +14,13 @@ * limitations under the License. */ -import { Catalog } from '@a2ui/angular'; -import { inputBinding } from '@angular/core'; +import {Catalog} from '@a2ui/angular'; +import {inputBinding} from '@angular/core'; export const DEMO_CATALOG = { Chart: { - type: () => import('./chart').then((r) => r.Chart), - bindings: ({ properties }) => [ + type: () => import('./chart').then(r => r.Chart), + bindings: ({properties}) => [ inputBinding('type', () => ('type' in properties && properties['type']) || undefined), inputBinding('title', () => ('title' in properties && properties['title']) || undefined), inputBinding( @@ -30,8 +30,8 @@ export const DEMO_CATALOG = { ], }, GoogleMap: { - type: () => import('./google-map').then((r) => r.GoogleMap), - bindings: ({ properties }) => [ + type: () => import('./google-map').then(r => r.GoogleMap), + bindings: ({properties}) => [ inputBinding('zoom', () => ('zoom' in properties && properties['zoom']) || 8), inputBinding('center', () => ('center' in properties && properties['center']) || undefined), inputBinding('pins', () => ('pins' in properties && properties['pins']) || undefined), diff --git a/samples/client/angular/projects/orchestrator/src/a2ui-catalog/chart.ts b/samples/client/angular/projects/orchestrator/src/a2ui-catalog/chart.ts index 00b8c70f3..8e2a6313e 100644 --- a/samples/client/angular/projects/orchestrator/src/a2ui-catalog/chart.ts +++ b/samples/client/angular/projects/orchestrator/src/a2ui-catalog/chart.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { DynamicComponent } from '@a2ui/angular'; +import {DynamicComponent} from '@a2ui/angular'; import * as Primitives from '@a2ui/web_core/types/primitives'; import * as Types from '@a2ui/web_core/types/types'; import { @@ -26,8 +26,8 @@ import { signal, ViewChild, } from '@angular/core'; -import { MatIconButton } from '@angular/material/button'; -import { MatIcon } from '@angular/material/icon'; +import {MatIconButton} from '@angular/material/button'; +import {MatIcon} from '@angular/material/icon'; import { ChartData, ChartEvent, @@ -37,7 +37,7 @@ import { LegendItem, } from 'chart.js'; import ChartDataLabels from 'chartjs-plugin-datalabels'; -import { BaseChartDirective } from 'ng2-charts'; +import {BaseChartDirective} from 'ng2-charts'; ChartJS.register(ChartDataLabels); @@ -165,7 +165,7 @@ export class Chart extends DynamicComponent { if (!allData) { return undefined; } - return { ...allData.get(selectedCategory) } as ChartData<'pie', number[], string>; + return {...allData.get(selectedCategory)} as ChartData<'pie', number[], string>; }, ); @@ -209,8 +209,8 @@ export class Chart extends DynamicComponent { if (pathPrefix?.path) { for (let index: number = 0; index < 500; index++) { const itemPrefix = `${pathPrefix.path}[${index}]`; - const labelPath: Primitives.StringValue = { path: `${itemPrefix}.label` }; - const valuePath: Primitives.NumberValue = { path: `${itemPrefix}.value` }; + const labelPath: Primitives.StringValue = {path: `${itemPrefix}.label`}; + const valuePath: Primitives.NumberValue = {path: `${itemPrefix}.value`}; const label = super.resolvePrimitive(labelPath); const value = super.resolvePrimitive(valuePath); if (label === null || value === null) { @@ -267,7 +267,7 @@ export class Chart extends DynamicComponent { this.selectedCategory.set('root'); } - protected onClick(e: { event?: ChartEvent; active?: any[] | undefined }) { + protected onClick(e: {event?: ChartEvent; active?: any[] | undefined}) { const active = e.active; if (!active || active.length === 0) return; diff --git a/samples/client/angular/projects/orchestrator/src/a2ui-catalog/google-map.ts b/samples/client/angular/projects/orchestrator/src/a2ui-catalog/google-map.ts index facc13baa..0759ac120 100644 --- a/samples/client/angular/projects/orchestrator/src/a2ui-catalog/google-map.ts +++ b/samples/client/angular/projects/orchestrator/src/a2ui-catalog/google-map.ts @@ -14,13 +14,13 @@ * limitations under the License. */ -import { DynamicComponent } from '@a2ui/angular'; +import {DynamicComponent} from '@a2ui/angular'; import * as Primitives from '@a2ui/web_core/types/primitives'; import * as Types from '@a2ui/web_core/types/types'; -import { ChangeDetectionStrategy, Component, computed, input } from '@angular/core'; -import { GoogleMapsModule } from '@angular/google-maps'; -import { MatIconButton } from '@angular/material/button'; -import { MatIcon } from '@angular/material/icon'; +import {ChangeDetectionStrategy, Component, computed, input} from '@angular/core'; +import {GoogleMapsModule} from '@angular/google-maps'; +import {MatIconButton} from '@angular/material/button'; +import {MatIcon} from '@angular/material/icon'; // --- Location Definitions --- interface Pin { @@ -117,7 +117,7 @@ export interface CustomProperties { [zoom]="resolvedZoom" height="500px" width="100%" - [options]="{ mapId: mapId }" + [options]="{mapId: mapId}" > @for (pin of resolvedPins(); track pin) { { return null; } - const latValue: Primitives.NumberValue = { path: `${value}.lat` }; - const lngValue: Primitives.NumberValue = { path: `${value}.lng` }; - const nameValue: Primitives.StringValue = { path: `${value}.name` }; - const descriptionValue: Primitives.StringValue = { path: `${value}.description` }; - const backgroundValue: Primitives.StringValue = { path: `${value}.background` }; - const borderColorValue: Primitives.StringValue = { path: `${value}.borderColor` }; - const glyphColorValue: Primitives.StringValue = { path: `${value}.glyphColor` }; + const latValue: Primitives.NumberValue = {path: `${value}.lat`}; + const lngValue: Primitives.NumberValue = {path: `${value}.lng`}; + const nameValue: Primitives.StringValue = {path: `${value}.name`}; + const descriptionValue: Primitives.StringValue = {path: `${value}.description`}; + const backgroundValue: Primitives.StringValue = {path: `${value}.background`}; + const borderColorValue: Primitives.StringValue = {path: `${value}.borderColor`}; + const glyphColorValue: Primitives.StringValue = {path: `${value}.glyphColor`}; const lat = this.resolvePrimitive(latValue); const lng = this.resolvePrimitive(lngValue); @@ -217,8 +217,8 @@ export class GoogleMap extends DynamicComponent { private resolveLatLng(value: CustomProperties | null): google.maps.LatLngLiteral { if (value?.path) { - const latValue: Primitives.NumberValue = { path: `${value.path}.lat` }; - const lngValue: Primitives.NumberValue = { path: `${value.path}.lng` }; + const latValue: Primitives.NumberValue = {path: `${value.path}.lat`}; + const lngValue: Primitives.NumberValue = {path: `${value.path}.lng`}; const lat = this.resolvePrimitive(latValue)!; const lng = this.resolvePrimitive(lngValue)!; return { diff --git a/samples/client/angular/projects/orchestrator/src/app/app.config.server.ts b/samples/client/angular/projects/orchestrator/src/app/app.config.server.ts index 125e66bed..b27c47b15 100644 --- a/samples/client/angular/projects/orchestrator/src/app/app.config.server.ts +++ b/samples/client/angular/projects/orchestrator/src/app/app.config.server.ts @@ -14,10 +14,10 @@ * limitations under the License. */ -import { ApplicationConfig, mergeApplicationConfig } from '@angular/core'; -import { provideServerRendering, withRoutes } from '@angular/ssr'; -import { appConfig } from './app.config'; -import { serverRoutes } from './app.routes.server'; +import {ApplicationConfig, mergeApplicationConfig} from '@angular/core'; +import {provideServerRendering, withRoutes} from '@angular/ssr'; +import {appConfig} from './app.config'; +import {serverRoutes} from './app.routes.server'; const serverConfig: ApplicationConfig = { providers: [provideServerRendering(withRoutes(serverRoutes))], diff --git a/samples/client/angular/projects/orchestrator/src/app/app.config.ts b/samples/client/angular/projects/orchestrator/src/app/app.config.ts index 65f494e18..53c16d764 100644 --- a/samples/client/angular/projects/orchestrator/src/app/app.config.ts +++ b/samples/client/angular/projects/orchestrator/src/app/app.config.ts @@ -25,14 +25,14 @@ import { provideBrowserGlobalErrorListeners, provideZonelessChangeDetection, } from '@angular/core'; -import { provideClientHydration, withEventReplay } from '@angular/platform-browser'; -import { provideRouter } from '@angular/router'; -import { provideCharts, withDefaultRegisterables } from 'ng2-charts'; -import { DEMO_CATALOG } from '../a2ui-catalog/catalog'; -import { A2aServiceImpl } from '../services/a2a-service-impl'; -import { routes } from './app.routes'; -import { provideMarkdownRenderer } from '@a2ui/angular'; -import { renderMarkdown } from '@a2ui/markdown-it'; +import {provideClientHydration, withEventReplay} from '@angular/platform-browser'; +import {provideRouter} from '@angular/router'; +import {provideCharts, withDefaultRegisterables} from 'ng2-charts'; +import {DEMO_CATALOG} from '../a2ui-catalog/catalog'; +import {A2aServiceImpl} from '../services/a2a-service-impl'; +import {routes} from './app.routes'; +import {provideMarkdownRenderer} from '@a2ui/angular'; +import {renderMarkdown} from '@a2ui/markdown-it'; export const appConfig: ApplicationConfig = { providers: [ diff --git a/samples/client/angular/projects/orchestrator/src/app/app.html b/samples/client/angular/projects/orchestrator/src/app/app.html index b921a4810..390018abb 100644 --- a/samples/client/angular/projects/orchestrator/src/app/app.html +++ b/samples/client/angular/projects/orchestrator/src/app/app.html @@ -15,31 +15,36 @@ -->
- +
-
-
- {{ agentName() }} -
+
+
+ {{ agentName() }}
-
+
+
-

- I help you orchestrate tasks across multiple agents. -

-
- - -
+

I help you orchestrate tasks across multiple agents.

+
+ + +
diff --git a/samples/client/angular/projects/orchestrator/src/app/app.routes.server.ts b/samples/client/angular/projects/orchestrator/src/app/app.routes.server.ts index 6e22a0113..417bd7b43 100644 --- a/samples/client/angular/projects/orchestrator/src/app/app.routes.server.ts +++ b/samples/client/angular/projects/orchestrator/src/app/app.routes.server.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { RenderMode, ServerRoute } from '@angular/ssr'; +import {RenderMode, ServerRoute} from '@angular/ssr'; export const serverRoutes: ServerRoute[] = [ { diff --git a/samples/client/angular/projects/orchestrator/src/app/app.routes.ts b/samples/client/angular/projects/orchestrator/src/app/app.routes.ts index 54e9a215f..1cbd92c87 100644 --- a/samples/client/angular/projects/orchestrator/src/app/app.routes.ts +++ b/samples/client/angular/projects/orchestrator/src/app/app.routes.ts @@ -14,6 +14,6 @@ * limitations under the License. */ -import { Routes } from '@angular/router'; +import {Routes} from '@angular/router'; export const routes: Routes = []; diff --git a/samples/client/angular/projects/orchestrator/src/app/app.scss b/samples/client/angular/projects/orchestrator/src/app/app.scss index dff7732af..570c4fc61 100644 --- a/samples/client/angular/projects/orchestrator/src/app/app.scss +++ b/samples/client/angular/projects/orchestrator/src/app/app.scss @@ -13,7 +13,7 @@ // limitations under the License. main { - height: 100vh; + height: 100vh; } .empty-history { @@ -44,12 +44,7 @@ main { } .agent-name { - background: linear-gradient( - 90deg, - #217bfe 28.03%, - #078efb 49.56%, - #ac87eb 71.1% - ); + background: linear-gradient(90deg, #217bfe 28.03%, #078efb 49.56%, #ac87eb 71.1%); background-clip: text; color: transparent; } @@ -64,17 +59,17 @@ main { .chip { // Override Material button styles to look like a chip - border-radius: 100px !important; // Force pill shape - padding: 10px 16px !important; - display: inline-flex !important; - align-items: center !important; - height: auto !important; - line-height: 25px !important; // Match icon height + border-radius: 100px !important; // Force pill shape + padding: 10px 16px !important; + display: inline-flex !important; + align-items: center !important; + height: auto !important; + line-height: 25px !important; // Match icon height .material-icons-outlined { font-size: 20px; margin-right: 8px; - line-height: 1; // Prevent icon from affecting line height - position: relative; - top: 4px; // Move icon down slightly to match text + line-height: 1; // Prevent icon from affecting line height + position: relative; + top: 4px; // Move icon down slightly to match text } -} \ No newline at end of file +} diff --git a/samples/client/angular/projects/orchestrator/src/app/app.ts b/samples/client/angular/projects/orchestrator/src/app/app.ts index 65ac2a990..30784d1fc 100644 --- a/samples/client/angular/projects/orchestrator/src/app/app.ts +++ b/samples/client/angular/projects/orchestrator/src/app/app.ts @@ -14,8 +14,8 @@ * limitations under the License. */ -import { A2aChatCanvas } from '@a2a_chat_canvas/a2a-chat-canvas'; -import { ChatService } from '@a2a_chat_canvas/services/chat-service'; +import {A2aChatCanvas} from '@a2a_chat_canvas/a2a-chat-canvas'; +import {ChatService} from '@a2a_chat_canvas/services/chat-service'; import { ChangeDetectionStrategy, Component, @@ -25,10 +25,10 @@ import { inject, signal, } from '@angular/core'; -import { MatButtonModule } from '@angular/material/button'; -import { RouterOutlet } from '@angular/router'; -import { environment } from '../environments/environment'; -import { demoMessageDecorator } from '../message-decorator/demo-message-decorator'; +import {MatButtonModule} from '@angular/material/button'; +import {RouterOutlet} from '@angular/router'; +import {environment} from '../environments/environment'; +import {demoMessageDecorator} from '../message-decorator/demo-message-decorator'; @Component({ selector: 'app-root', diff --git a/samples/client/angular/projects/orchestrator/src/main.server.ts b/samples/client/angular/projects/orchestrator/src/main.server.ts index fcfe120c8..5bad09f44 100644 --- a/samples/client/angular/projects/orchestrator/src/main.server.ts +++ b/samples/client/angular/projects/orchestrator/src/main.server.ts @@ -14,9 +14,9 @@ * limitations under the License. */ -import { BootstrapContext, bootstrapApplication } from '@angular/platform-browser'; -import { App } from './app/app'; -import { config } from './app/app.config.server'; +import {BootstrapContext, bootstrapApplication} from '@angular/platform-browser'; +import {App} from './app/app'; +import {config} from './app/app.config.server'; const bootstrap = (context: BootstrapContext) => bootstrapApplication(App, config, context); diff --git a/samples/client/angular/projects/orchestrator/src/main.ts b/samples/client/angular/projects/orchestrator/src/main.ts index 3c4480110..98aa73d70 100644 --- a/samples/client/angular/projects/orchestrator/src/main.ts +++ b/samples/client/angular/projects/orchestrator/src/main.ts @@ -14,8 +14,8 @@ * limitations under the License. */ -import { bootstrapApplication } from '@angular/platform-browser'; -import { App } from './app/app'; -import { appConfig } from './app/app.config'; +import {bootstrapApplication} from '@angular/platform-browser'; +import {App} from './app/app'; +import {appConfig} from './app/app.config'; -bootstrapApplication(App, appConfig).catch((err) => console.error(err)); +bootstrapApplication(App, appConfig).catch(err => console.error(err)); diff --git a/samples/client/angular/projects/orchestrator/src/message-decorator/demo-message-decorator.ts b/samples/client/angular/projects/orchestrator/src/message-decorator/demo-message-decorator.ts index 56f2999e1..3ca8da42a 100644 --- a/samples/client/angular/projects/orchestrator/src/message-decorator/demo-message-decorator.ts +++ b/samples/client/angular/projects/orchestrator/src/message-decorator/demo-message-decorator.ts @@ -18,11 +18,11 @@ import { MessageDecorator, MessageDecoratorComponent, } from '@a2a_chat_canvas/components/chat/chat-history/message-decorator/types'; -import { UiMessage } from '@a2a_chat_canvas/types/ui-message'; // Assuming path based on context -import { NgTemplateOutlet } from '@angular/common'; -import { ChangeDetectionStrategy, Component, input, TemplateRef } from '@angular/core'; -import { MatIconButton } from '@angular/material/button'; -import { MatIcon } from '@angular/material/icon'; +import {UiMessage} from '@a2a_chat_canvas/types/ui-message'; // Assuming path based on context +import {NgTemplateOutlet} from '@angular/common'; +import {ChangeDetectionStrategy, Component, input, TemplateRef} from '@angular/core'; +import {MatIconButton} from '@angular/material/button'; +import {MatIcon} from '@angular/material/icon'; @Component({ selector: 'app-custom-message-decorator', diff --git a/samples/client/angular/projects/orchestrator/src/server.ts b/samples/client/angular/projects/orchestrator/src/server.ts index 1aacd931f..e09e571ab 100644 --- a/samples/client/angular/projects/orchestrator/src/server.ts +++ b/samples/client/angular/projects/orchestrator/src/server.ts @@ -14,8 +14,8 @@ * limitations under the License. */ -import { MessageSendParams, Part, SendMessageResponse } from '@a2a-js/sdk'; -import { A2AClient } from '@a2a-js/sdk/client'; +import {MessageSendParams, Part, SendMessageResponse} from '@a2a-js/sdk'; +import {A2AClient} from '@a2a-js/sdk/client'; import { AngularNodeAppEngine, createNodeRequestHandler, @@ -23,8 +23,8 @@ import { writeResponseToNodeResponse, } from '@angular/ssr/node'; import express from 'express'; -import { join } from 'node:path'; -import { v4 as uuidv4 } from 'uuid'; +import {join} from 'node:path'; +import {v4 as uuidv4} from 'uuid'; const browserDistFolder = join(import.meta.dirname, '../browser'); const app = express(); @@ -42,7 +42,7 @@ app.use( app.post('/a2a', (req, res) => { let originalBody = ''; - req.on('data', (chunk) => { + req.on('data', chunk => { originalBody += chunk.toString(); }); @@ -75,7 +75,7 @@ app.post('/a2a', (req, res) => { try { client = await createOrGetClient(); } catch (error) { - res.status(500).json({ error: 'Failed to create A2A client.' }); + res.status(500).json({error: 'Failed to create A2A client.'}); return; } @@ -83,14 +83,14 @@ app.post('/a2a', (req, res) => { try { response = await client.sendMessage(sendParams); } catch (error) { - res.status(500).json({ error: 'Failed to send message.' }); + res.status(500).json({error: 'Failed to send message.'}); return; } res.set('Cache-Control', 'no-store'); if ('error' in response) { - res.status(500).json({ error: JSON.stringify(response.error) }); + res.status(500).json({error: JSON.stringify(response.error)}); return; } @@ -104,27 +104,27 @@ app.get('/a2a/agent-card', async (req, res) => { 'http://localhost:10002/.well-known/agent-card.json', ); if (!response.ok) { - res.status(response.status).json({ error: 'Failed to fetch agent card' }); + res.status(response.status).json({error: 'Failed to fetch agent card'}); return; } const card = await response.json(); res.json(card); } catch (error) { console.error('Error fetching agent card:', error); - res.status(500).json({ error: 'Internal server error' }); + res.status(500).json({error: 'Internal server error'}); } }); app.use((req, res, next) => { angularApp .handle(req) - .then((response) => (response ? writeResponseToNodeResponse(response, res) : next())) + .then(response => (response ? writeResponseToNodeResponse(response, res) : next())) .catch(next); }); if (isMainModule(import.meta.url) || process.env['pm_id']) { const port = process.env['PORT'] || 4000; - app.listen(port, (error) => { + app.listen(port, error => { if (error) { throw error; } @@ -136,7 +136,7 @@ if (isMainModule(import.meta.url) || process.env['pm_id']) { async function fetchWithCustomHeader(url: string | URL | Request, init?: RequestInit) { const headers = new Headers(init?.headers); headers.set('X-A2A-Extensions', 'https://a2ui.org/a2a-extension/a2ui/v0.8'); - const newInit = { ...init, headers }; + const newInit = {...init, headers}; return fetch(url, newInit); } diff --git a/samples/client/angular/projects/orchestrator/src/services/a2a-service-impl.spec.ts b/samples/client/angular/projects/orchestrator/src/services/a2a-service-impl.spec.ts index 691e63755..235a56403 100644 --- a/samples/client/angular/projects/orchestrator/src/services/a2a-service-impl.spec.ts +++ b/samples/client/angular/projects/orchestrator/src/services/a2a-service-impl.spec.ts @@ -14,8 +14,8 @@ * limitations under the License. */ -import { TestBed } from '@angular/core/testing'; -import { A2aServiceImpl } from './a2a-service-impl'; +import {TestBed} from '@angular/core/testing'; +import {A2aServiceImpl} from './a2a-service-impl'; describe('A2aServiceImpl', () => { let service: A2aServiceImpl; @@ -32,7 +32,7 @@ describe('A2aServiceImpl', () => { }); it('should send contextId in request after receiving it from server', async () => { - // Mock first response to return a contextId + // Mock first response to return a contextId const mockResponse1 = { contextId: 'test-session-123', parts: [], @@ -42,12 +42,12 @@ describe('A2aServiceImpl', () => { Promise.resolve({ ok: true, json: () => Promise.resolve(mockResponse1), - } as Response) + } as Response), ); // First call should NOT send contextId (it doesn't have it yet) await service.sendMessage([]); - + let lastCall = fetchSpy.calls.mostRecent(); let body = JSON.parse(lastCall.args[1]!.body as string); expect(body.contextId).toBeUndefined(); @@ -60,7 +60,7 @@ describe('A2aServiceImpl', () => { Promise.resolve({ ok: true, json: () => Promise.resolve(mockResponse2), - } as Response) + } as Response), ); // Second call SHOULD send contextId @@ -83,7 +83,7 @@ describe('A2aServiceImpl', () => { Promise.resolve({ ok: true, json: () => Promise.resolve(mockResponse), - } as Response) + } as Response), ); await service.sendMessage([]); @@ -93,7 +93,7 @@ describe('A2aServiceImpl', () => { Promise.resolve({ ok: true, json: () => Promise.resolve({}), - } as Response) + } as Response), ); await service.sendMessage([]); diff --git a/samples/client/angular/projects/orchestrator/src/services/a2a-service-impl.ts b/samples/client/angular/projects/orchestrator/src/services/a2a-service-impl.ts index 4e7eae67c..eef53df81 100644 --- a/samples/client/angular/projects/orchestrator/src/services/a2a-service-impl.ts +++ b/samples/client/angular/projects/orchestrator/src/services/a2a-service-impl.ts @@ -14,9 +14,9 @@ * limitations under the License. */ -import { AgentCard, Part, SendMessageSuccessResponse } from '@a2a-js/sdk'; -import { A2aService } from '@a2a_chat_canvas/interfaces/a2a-service'; -import { Injectable } from '@angular/core'; +import {AgentCard, Part, SendMessageSuccessResponse} from '@a2a-js/sdk'; +import {A2aService} from '@a2a_chat_canvas/interfaces/a2a-service'; +import {Injectable} from '@angular/core'; @Injectable({ providedIn: 'root', @@ -26,9 +26,9 @@ export class A2aServiceImpl implements A2aService { async sendMessage(parts: Part[], signal?: AbortSignal): Promise { const response = await fetch('/a2a', { - body: JSON.stringify({ + body: JSON.stringify({ parts: parts, - contextId: this.contextId + contextId: this.contextId, }), method: 'POST', signal, @@ -42,7 +42,7 @@ export class A2aServiceImpl implements A2aService { return data; } - const error = (await response.json()) as { error: string }; + const error = (await response.json()) as {error: string}; throw new Error(error.error); } @@ -51,7 +51,7 @@ export class A2aServiceImpl implements A2aService { if (!response.ok) { throw new Error('Failed to fetch agent card'); } - const card = await response.json() as AgentCard; + const card = (await response.json()) as AgentCard; return card; } } diff --git a/samples/client/angular/projects/orchestrator/src/styles.scss b/samples/client/angular/projects/orchestrator/src/styles.scss index e6c8f2610..f89c852c7 100644 --- a/samples/client/angular/projects/orchestrator/src/styles.scss +++ b/samples/client/angular/projects/orchestrator/src/styles.scss @@ -14,7 +14,6 @@ @use '@angular/material' as mat; - @mixin styled-scrollbar { ::-webkit-scrollbar { width: 0.5rem; @@ -39,11 +38,13 @@ html { color-scheme: light dark; - @include mat.theme(( - color: mat.$blue-palette, - typography: Roboto, - density: 0 - )); + @include mat.theme( + ( + color: mat.$blue-palette, + typography: Roboto, + density: 0, + ) + ); @include styled-scrollbar; } @@ -201,4 +202,4 @@ body { padding: 0; width: 100svw; height: 100svh; -} \ No newline at end of file +} diff --git a/samples/client/angular/projects/orchestrator/tsconfig.app.json b/samples/client/angular/projects/orchestrator/tsconfig.app.json index 3175e5ae7..6c6f9b1d1 100644 --- a/samples/client/angular/projects/orchestrator/tsconfig.app.json +++ b/samples/client/angular/projects/orchestrator/tsconfig.app.json @@ -4,14 +4,8 @@ "extends": "../../tsconfig.json", "compilerOptions": { "outDir": "../../out-tsc/app", - "types": [ - "node" - ] + "types": ["node"] }, - "include": [ - "src/**/*.ts" - ], - "exclude": [ - "src/**/*.spec.ts" - ] + "include": ["src/**/*.ts"], + "exclude": ["src/**/*.spec.ts"] } diff --git a/samples/client/angular/projects/orchestrator/tsconfig.spec.json b/samples/client/angular/projects/orchestrator/tsconfig.spec.json index 0feea88ed..43ba0b186 100644 --- a/samples/client/angular/projects/orchestrator/tsconfig.spec.json +++ b/samples/client/angular/projects/orchestrator/tsconfig.spec.json @@ -4,11 +4,7 @@ "extends": "../../tsconfig.json", "compilerOptions": { "outDir": "../../out-tsc/spec", - "types": [ - "jasmine" - ] + "types": ["jasmine"] }, - "include": [ - "src/**/*.ts" - ] + "include": ["src/**/*.ts"] } diff --git a/samples/client/angular/projects/restaurant/src/app/app.config.server.ts b/samples/client/angular/projects/restaurant/src/app/app.config.server.ts index 7f94af4ce..a55f9a93c 100644 --- a/samples/client/angular/projects/restaurant/src/app/app.config.server.ts +++ b/samples/client/angular/projects/restaurant/src/app/app.config.server.ts @@ -14,12 +14,10 @@ * limitations under the License. */ -import { mergeApplicationConfig } from '@angular/core'; -import { provideServerRendering } from '@angular/ssr'; -import { appConfig } from './app.config'; +import {mergeApplicationConfig} from '@angular/core'; +import {provideServerRendering} from '@angular/ssr'; +import {appConfig} from './app.config'; export const config = mergeApplicationConfig(appConfig, { - providers: [ - provideServerRendering() - ] + providers: [provideServerRendering()], }); diff --git a/samples/client/angular/projects/restaurant/src/app/app.config.ts b/samples/client/angular/projects/restaurant/src/app/app.config.ts index 1e3b858b0..0d3f74662 100644 --- a/samples/client/angular/projects/restaurant/src/app/app.config.ts +++ b/samples/client/angular/projects/restaurant/src/app/app.config.ts @@ -14,7 +14,12 @@ * limitations under the License. */ -import {A2uiRendererService, A2UI_RENDERER_CONFIG, BasicCatalog, provideMarkdownRenderer} from '@a2ui/angular/v0_9'; +import { + A2uiRendererService, + A2UI_RENDERER_CONFIG, + BasicCatalog, + provideMarkdownRenderer, +} from '@a2ui/angular/v0_9'; import {Client} from './client'; import {inject, Injector} from '@angular/core'; import {IMAGE_CONFIG} from '@angular/common'; diff --git a/samples/client/angular/projects/restaurant/src/app/app.css b/samples/client/angular/projects/restaurant/src/app/app.css index e42933eb1..881322709 100644 --- a/samples/client/angular/projects/restaurant/src/app/app.css +++ b/samples/client/angular/projects/restaurant/src/app/app.css @@ -134,13 +134,13 @@ form { pointer-events: none; &::before { - content: "dark_mode"; + content: 'dark_mode'; } } } .g-icon { - font-family: "Material Symbols Outlined", "Google Symbols"; + font-family: 'Material Symbols Outlined', 'Google Symbols'; font-weight: normal; font-style: normal; font-display: optional; @@ -155,27 +155,39 @@ form { white-space: nowrap; word-wrap: normal; direction: ltr; - -webkit-font-feature-settings: "liga"; + -webkit-font-feature-settings: 'liga'; -webkit-font-smoothing: antialiased; overflow: hidden; - font-variation-settings: "FILL" 0, "wght" 300, "GRAD" 0, "opsz" 48, - "ROND" 100; + font-variation-settings: + 'FILL' 0, + 'wght' 300, + 'GRAD' 0, + 'opsz' 48, + 'ROND' 100; &.filled { - font-variation-settings: "FILL" 1, "wght" 300, "GRAD" 0, "opsz" 48, - "ROND" 100; + font-variation-settings: + 'FILL' 1, + 'wght' 300, + 'GRAD' 0, + 'opsz' 48, + 'ROND' 100; } &.filled-heavy { - font-variation-settings: "FILL" 1, "wght" 700, "GRAD" 0, "opsz" 48, - "ROND" 100; + font-variation-settings: + 'FILL' 1, + 'wght' 700, + 'GRAD' 0, + 'opsz' 48, + 'ROND' 100; } } @container style(--color-scheme: dark) { .theme-toggle .g-icon::before { - content: "light_mode"; + content: 'light_mode'; color: var(--n-90); } } @@ -252,7 +264,7 @@ form { position: relative; &::after { - content: ""; + content: ''; position: absolute; left: 0; top: 0; @@ -272,4 +284,4 @@ form { 100% { left: 100%; } -} \ No newline at end of file +} diff --git a/samples/client/angular/projects/restaurant/src/app/app.html b/samples/client/angular/projects/restaurant/src/app/app.html index 6f0af43b9..6b382e851 100644 --- a/samples/client/angular/projects/restaurant/src/app/app.html +++ b/samples/client/angular/projects/restaurant/src/app/app.html @@ -15,42 +15,49 @@ --> @if (!hasData()) { -
- Image of the restaurant - -

Restaurant Finder

-
- - -
-
+
+ Image of the restaurant + +

Restaurant Finder

+
+ + +
+
} @else { -
- @let surfaces = renderer.surfaceGroup.surfacesMap; - - @if (client.isLoading() && surfaces.size === 0) { -
-
-
{{loadingTextLines[loadingTextIndex()]}}
+
+ @let surfaces = renderer.surfaceGroup.surfacesMap; + + @if (client.isLoading() && surfaces.size === 0) { +
+
+
{{ loadingTextLines[loadingTextIndex()] }}
+
+ } @else { + @if (client.isLoading()) { +
+
+ Finding the best spots for you... +
+ } + + @for (entry of surfaces; track entry[0]) { + + } + }
- } @else { - @if (client.isLoading()) { -
-
- Finding the best spots for you... -
- } - - @for (entry of surfaces; track entry[0]) { - - } - } -
} \ No newline at end of file + diff --git a/samples/client/angular/projects/restaurant/src/app/app.ts b/samples/client/angular/projects/restaurant/src/app/app.ts index 4b47a593d..3a5f0b421 100644 --- a/samples/client/angular/projects/restaurant/src/app/app.ts +++ b/samples/client/angular/projects/restaurant/src/app/app.ts @@ -14,10 +14,10 @@ * limitations under the License. */ -import { SurfaceComponent, A2uiRendererService } from '@a2ui/angular/v0_9'; +import {SurfaceComponent, A2uiRendererService} from '@a2ui/angular/v0_9'; import * as Types from '@a2ui/web_core/types/types'; -import { ChangeDetectionStrategy, Component, DOCUMENT, inject, signal } from '@angular/core'; -import { Client } from './client'; +import {ChangeDetectionStrategy, Component, DOCUMENT, inject, signal} from '@angular/core'; +import {Client} from './client'; @Component({ selector: 'app-root', @@ -62,7 +62,7 @@ export class App { } protected toggleTheme(button: HTMLButtonElement) { - const { colorScheme } = window.getComputedStyle(button); + const {colorScheme} = window.getComputedStyle(button); const classList = this.document.body.classList; if (colorScheme === 'dark') { @@ -78,7 +78,7 @@ export class App { this.loadingTextIndex.set(0); this.loadingInterval = window.setInterval(() => { - this.loadingTextIndex.update((prev) => (prev + 1) % this.loadingTextLines.length); + this.loadingTextIndex.update(prev => (prev + 1) % this.loadingTextLines.length); }, 2000); } diff --git a/samples/client/angular/projects/restaurant/src/app/client.ts b/samples/client/angular/projects/restaurant/src/app/client.ts index 24edd3984..0957e95df 100644 --- a/samples/client/angular/projects/restaurant/src/app/client.ts +++ b/samples/client/angular/projects/restaurant/src/app/client.ts @@ -14,12 +14,12 @@ * limitations under the License. */ -import { A2uiRendererService } from '@a2ui/angular/v0_9'; +import {A2uiRendererService} from '@a2ui/angular/v0_9'; import * as Types from '@a2ui/web_core/types/types'; -import { inject, Injectable, signal } from '@angular/core'; +import {inject, Injectable, signal} from '@angular/core'; import {A2uiClientAction, A2uiMessage} from '@a2ui/web_core/v0_9'; -@Injectable({ providedIn: 'root' }) +@Injectable({providedIn: 'root'}) export class Client { private readonly renderer = inject(A2uiRendererService); private contextId?: string; @@ -35,7 +35,9 @@ export class Client { } } - async makeRequest(request: Types.A2UIClientEventMessage | string): Promise { + async makeRequest( + request: Types.A2UIClientEventMessage | string, + ): Promise { let messages: Types.ServerToClientMessage[] = []; try { this.isLoading.set(true); @@ -48,8 +50,8 @@ export class Client { const isString = typeof request === 'string'; const bodyData = isString - ? { query: request, contextId: this.contextId } - : { event: request, contextId: this.contextId }; + ? {query: request, contextId: this.contextId} + : {event: request, contextId: this.contextId}; const response = await fetch('/a2a', { body: JSON.stringify(bodyData), @@ -57,7 +59,7 @@ export class Client { }); if (!response.ok) { - const error = (await response.json()) as { error: string }; + const error = (await response.json()) as {error: string}; throw new Error(error.error); } @@ -79,7 +81,7 @@ export class Client { private async handleStreamingResponse( response: Response, - messages: Types.ServerToClientMessage[] + messages: Types.ServerToClientMessage[], ): Promise { const reader = response.body?.getReader(); if (!reader) { @@ -90,11 +92,11 @@ export class Client { let buffer = ''; while (true) { - const { done, value } = await reader.read(); + const {done, value} = await reader.read(); if (done) break; const now = performance.now(); - buffer += decoder.decode(value, { stream: true }); + buffer += decoder.decode(value, {stream: true}); // Parse SSE events. The server sends "data: \n\n" const lines = buffer.split('\n\n'); @@ -115,7 +117,7 @@ export class Client { } const parts = responseData.parts || (Array.isArray(responseData) ? responseData : []); console.log( - `[client] [${performance.now().toFixed(2)}ms] Scheduling processing for ${parts.length} parts` + `[client] [${performance.now().toFixed(2)}ms] Scheduling processing for ${parts.length} parts`, ); // Use a microtask to ensure we don't block the stream reader await Promise.resolve(); @@ -132,7 +134,7 @@ export class Client { private async handleNonStreamingResponse( response: Response, - messages: Types.ServerToClientMessage[] + messages: Types.ServerToClientMessage[], ): Promise { const responseData = await response.json(); console.log(`[client] Received JSON response:`, responseData); diff --git a/samples/client/angular/projects/restaurant/src/app/theme.ts b/samples/client/angular/projects/restaurant/src/app/theme.ts index 6eb7be6fd..ca80954a5 100644 --- a/samples/client/angular/projects/restaurant/src/app/theme.ts +++ b/samples/client/angular/projects/restaurant/src/app/theme.ts @@ -222,7 +222,7 @@ export const theme: Types.Theme = { 'behavior-ho-70': true, 'typography-w-400': true, }, - Card: { 'border-br-9': true, 'layout-p-4': true, 'color-bgc-n100': true }, + Card: {'border-br-9': true, 'layout-p-4': true, 'color-bgc-n100': true}, CheckBox: { element: { 'layout-m-0': true, @@ -287,7 +287,7 @@ export const theme: Types.Theme = { 'layout-w-100': true, 'layout-h-100': true, }, - avatar: { 'is-avatar': true }, + avatar: {'is-avatar': true}, header: {}, icon: {}, largeFeature: {}, @@ -300,7 +300,7 @@ export const theme: Types.Theme = { 'layout-p-2': true, }, Modal: { - backdrop: { 'color-bbgc-p60_20': true }, + backdrop: {'color-bbgc-p60_20': true}, element: { 'border-br-2': true, 'color-bgc-p100': true, @@ -325,7 +325,7 @@ export const theme: Types.Theme = { }, Tabs: { container: {}, - controls: { all: {}, selected: {} }, + controls: {all: {}, selected: {}}, element: {}, }, Text: { diff --git a/samples/client/angular/projects/restaurant/src/index.html b/samples/client/angular/projects/restaurant/src/index.html index 4885b26d8..136eb64ef 100644 --- a/samples/client/angular/projects/restaurant/src/index.html +++ b/samples/client/angular/projects/restaurant/src/index.html @@ -16,22 +16,22 @@ - - - Angular Restaurant demo - - - + + + Angular Restaurant demo + + + - - - - - + + + + + diff --git a/samples/client/angular/projects/restaurant/src/main.server.ts b/samples/client/angular/projects/restaurant/src/main.server.ts index fcfe120c8..5bad09f44 100644 --- a/samples/client/angular/projects/restaurant/src/main.server.ts +++ b/samples/client/angular/projects/restaurant/src/main.server.ts @@ -14,9 +14,9 @@ * limitations under the License. */ -import { BootstrapContext, bootstrapApplication } from '@angular/platform-browser'; -import { App } from './app/app'; -import { config } from './app/app.config.server'; +import {BootstrapContext, bootstrapApplication} from '@angular/platform-browser'; +import {App} from './app/app'; +import {config} from './app/app.config.server'; const bootstrap = (context: BootstrapContext) => bootstrapApplication(App, config, context); diff --git a/samples/client/angular/projects/restaurant/src/main.ts b/samples/client/angular/projects/restaurant/src/main.ts index c1861c62b..3b5046207 100644 --- a/samples/client/angular/projects/restaurant/src/main.ts +++ b/samples/client/angular/projects/restaurant/src/main.ts @@ -14,8 +14,8 @@ * limitations under the License. */ -import { bootstrapApplication } from '@angular/platform-browser'; -import { appConfig } from './app/app.config'; -import { App } from './app/app'; +import {bootstrapApplication} from '@angular/platform-browser'; +import {appConfig} from './app/app.config'; +import {App} from './app/app'; -bootstrapApplication(App, appConfig).catch((err) => console.error(err)); +bootstrapApplication(App, appConfig).catch(err => console.error(err)); diff --git a/samples/client/angular/projects/restaurant/src/restaurant-theme.css b/samples/client/angular/projects/restaurant/src/restaurant-theme.css index 86d674d42..01c30fb6a 100644 --- a/samples/client/angular/projects/restaurant/src/restaurant-theme.css +++ b/samples/client/angular/projects/restaurant/src/restaurant-theme.css @@ -16,7 +16,11 @@ :root { /* Button Overrides */ - --a2ui-button-background: linear-gradient(135deg, light-dark(var(--p-70), var(--p-40)) 0%, light-dark(var(--p-60), var(--p-30)) 100%); + --a2ui-button-background: linear-gradient( + 135deg, + light-dark(var(--p-70), var(--p-40)) 0%, + light-dark(var(--p-60), var(--p-30)) 100% + ); --a2ui-button-box-shadow: 0 4px 15px rgba(102, 126, 234, 0.4); --a2ui-button-font-weight: 500; --a2ui-button-border-radius: 9999px; @@ -28,7 +32,22 @@ /* Card Overrides */ --a2ui-card-border: none; --a2ui-card-border-radius: 24px; - --a2ui-card-background: radial-gradient(circle at top left, light-dark(transparent, rgba(6, 182, 212, 0.15)), transparent 40%), radial-gradient(circle at bottom right, light-dark(transparent, rgba(139, 92, 246, 0.15)), transparent 40%), linear-gradient(135deg, light-dark(rgba(255, 255, 255, 0.7), rgba(30, 41, 59, 0.7)), light-dark(rgba(255, 255, 255, 0.7), rgba(15, 23, 42, 0.8))); + --a2ui-card-background: + radial-gradient( + circle at top left, + light-dark(transparent, rgba(6, 182, 212, 0.15)), + transparent 40% + ), + radial-gradient( + circle at bottom right, + light-dark(transparent, rgba(139, 92, 246, 0.15)), + transparent 40% + ), + linear-gradient( + 135deg, + light-dark(rgba(255, 255, 255, 0.7), rgba(30, 41, 59, 0.7)), + light-dark(rgba(255, 255, 255, 0.7), rgba(15, 23, 42, 0.8)) + ); /* Image Overrides */ --a2ui-image-border-radius: 12px; diff --git a/samples/client/angular/projects/restaurant/src/server.ts b/samples/client/angular/projects/restaurant/src/server.ts index e90358e38..120a6ca3b 100644 --- a/samples/client/angular/projects/restaurant/src/server.ts +++ b/samples/client/angular/projects/restaurant/src/server.ts @@ -21,10 +21,10 @@ import { writeResponseToNodeResponse, } from '@angular/ssr/node'; import express from 'express'; -import { join } from 'node:path'; -import { v4 as uuidv4 } from 'uuid'; -import { A2AClient } from '@a2a-js/sdk/client'; -import { MessageSendParams, Part, SendMessageSuccessResponse, Task } from '@a2a-js/sdk'; +import {join} from 'node:path'; +import {v4 as uuidv4} from 'uuid'; +import {A2AClient} from '@a2a-js/sdk/client'; +import {MessageSendParams, Part, SendMessageSuccessResponse, Task} from '@a2a-js/sdk'; const browserDistFolder = join(import.meta.dirname, '../browser'); const app = express(); @@ -37,13 +37,13 @@ app.use( maxAge: '1y', index: false, redirect: false, - }) + }), ); app.post('/a2a', (req, res) => { let originalBody = ''; - req.on('data', (chunk) => { + req.on('data', chunk => { originalBody += chunk.toString(); }); @@ -65,7 +65,7 @@ app.post('/a2a', (req, res) => { { kind: 'data', data: requestData.event, - metadata: { 'mimeType': 'application/json+a2ui' }, + metadata: {mimeType: 'application/json+a2ui'}, } as Part, ], kind: 'message', @@ -78,7 +78,7 @@ app.post('/a2a', (req, res) => { messageId: uuidv4(), contextId, role: 'user', - parts: [{ kind: 'text', text: requestData.query }], + parts: [{kind: 'text', text: requestData.query}], kind: 'message', }, }; @@ -94,7 +94,7 @@ app.post('/a2a', (req, res) => { { kind: 'data', data: requestData, - metadata: { 'mimeType': 'application/json+a2ui' }, + metadata: {mimeType: 'application/json+a2ui'}, } as Part, ], kind: 'message', @@ -107,7 +107,7 @@ app.post('/a2a', (req, res) => { message: { messageId: uuidv4(), role: 'user', - parts: [{ kind: 'text', text: originalBody }], + parts: [{kind: 'text', text: originalBody}], kind: 'message', }, }; @@ -123,16 +123,20 @@ app.post('/a2a', (req, res) => { } catch (error: any) { console.error('Request error:', error.message); if (!res.headersSent) { - res.status(500).json({ error: error.message }); + res.status(500).json({error: error.message}); } else if (!res.writableEnded) { - res.write(`data: ${JSON.stringify({ error: error.message })}\n\n`); + res.write(`data: ${JSON.stringify({error: error.message})}\n\n`); res.end(); } } }); }); -async function handleStreamingResponse(client: A2AClient, sendParams: MessageSendParams, res: express.Response) { +async function handleStreamingResponse( + client: A2AClient, + sendParams: MessageSendParams, + res: express.Response, +) { process.stdout.write('[server] Streaming mode enabled\n'); const stream = client.sendMessageStream(sendParams); @@ -156,7 +160,7 @@ async function handleStreamingResponse(client: A2AClient, sendParams: MessageSen console.log(`[server] Streaming parts: ${JSON.stringify(parts)}`); const responseData = { parts, - contextId: (event as any).contextId || (event as any).status?.message?.contextId + contextId: (event as any).contextId || (event as any).status?.message?.contextId, }; res.write(`data: ${JSON.stringify(responseData)}\n\n`); } @@ -165,34 +169,38 @@ async function handleStreamingResponse(client: A2AClient, sendParams: MessageSen console.log('[server] Stream finished'); } -async function handleNonStreamingResponse(client: A2AClient, sendParams: MessageSendParams, res: express.Response) { +async function handleNonStreamingResponse( + client: A2AClient, + sendParams: MessageSendParams, + res: express.Response, +) { process.stdout.write('[server] Streaming mode disabled\n'); const response = await client.sendMessage(sendParams); res.set('Cache-Control', 'no-store'); if ('error' in response) { console.error('Error:', response.error.message); - res.status(500).json({ error: response.error.message }); + res.status(500).json({error: response.error.message}); return; } const result = (response as SendMessageSuccessResponse).result as Task; res.json({ parts: result.kind === 'task' ? result.status.message?.parts || [] : [], - contextId: result.contextId + contextId: result.contextId, }); } app.use((req, res, next) => { angularApp .handle(req) - .then((response) => (response ? writeResponseToNodeResponse(response, res) : next())) + .then(response => (response ? writeResponseToNodeResponse(response, res) : next())) .catch(next); }); if (isMainModule(import.meta.url) || process.env['pm_id']) { const port = process.env['PORT'] || 4000; - app.listen(port, (error) => { + app.listen(port, error => { if (error) { throw error; } @@ -204,7 +212,7 @@ if (isMainModule(import.meta.url) || process.env['pm_id']) { async function fetchWithCustomHeader(url: string | URL | Request, init?: RequestInit) { const headers = new Headers(init?.headers); headers.set('X-A2A-Extensions', 'https://a2ui.org/a2a-extension/a2ui/v0.9'); - const newInit = { ...init, headers }; + const newInit = {...init, headers}; return fetch(url, newInit); } diff --git a/samples/client/angular/projects/restaurant/src/styles.css b/samples/client/angular/projects/restaurant/src/styles.css index cdc8ac184..71c822b32 100644 --- a/samples/client/angular/projects/restaurant/src/styles.css +++ b/samples/client/angular/projects/restaurant/src/styles.css @@ -53,32 +53,28 @@ --bb-grid-size-15: calc(var(--bb-grid-size) * 15); --bb-grid-size-16: calc(var(--bb-grid-size) * 16); - --background: radial-gradient( - at 0% 0%, - light-dark(rgba(161, 196, 253, 0.3), rgba(6, 182, 212, 0.15)) 0px, - transparent 50% - ), - radial-gradient( - at 100% 0%, - light-dark(rgba(255, 226, 226, 0.3), rgba(59, 130, 246, 0.15)) 0px, - transparent 50% - ), - radial-gradient( - at 100% 100%, - light-dark(rgba(162, 210, 255, 0.3), rgba(20, 184, 166, 0.15)) 0px, - transparent 50% - ), - radial-gradient( - at 0% 100%, - light-dark(rgba(255, 200, 221, 0.3), rgba(99, 102, 241, 0.15)) 0px, - transparent 50% - ), - linear-gradient( - 120deg, - light-dark(#f0f4f8, #0f172a) 0%, - light-dark(#e2e8f0, #1e293b) 100% - ); - + --background: + radial-gradient( + at 0% 0%, + light-dark(rgba(161, 196, 253, 0.3), rgba(6, 182, 212, 0.15)) 0px, + transparent 50% + ), + radial-gradient( + at 100% 0%, + light-dark(rgba(255, 226, 226, 0.3), rgba(59, 130, 246, 0.15)) 0px, + transparent 50% + ), + radial-gradient( + at 100% 100%, + light-dark(rgba(162, 210, 255, 0.3), rgba(20, 184, 166, 0.15)) 0px, + transparent 50% + ), + radial-gradient( + at 0% 100%, + light-dark(rgba(255, 200, 221, 0.3), rgba(99, 102, 241, 0.15)) 0px, + transparent 50% + ), + linear-gradient(120deg, light-dark(#f0f4f8, #0f172a) 0%, light-dark(#e2e8f0, #1e293b) 100%); } * { @@ -87,9 +83,8 @@ html, body { - --font-family: "Outfit", "Helvetica Neue", Helvetica, Arial, sans-serif; - --font-family-flex: "Outfit", "Helvetica Neue", Helvetica, Arial, - sans-serif; + --font-family: 'Outfit', 'Helvetica Neue', Helvetica, Arial, sans-serif; + --font-family-flex: 'Outfit', 'Helvetica Neue', Helvetica, Arial, sans-serif; --font-family-mono: monospace; background: var(--background); diff --git a/samples/client/angular/projects/restaurant/tsconfig.app.json b/samples/client/angular/projects/restaurant/tsconfig.app.json index 9fb2965ee..4ffbd7c8e 100644 --- a/samples/client/angular/projects/restaurant/tsconfig.app.json +++ b/samples/client/angular/projects/restaurant/tsconfig.app.json @@ -2,14 +2,8 @@ "extends": "../../tsconfig.json", "compilerOptions": { "outDir": "../../out-tsc/restaurant", - "types": [ - "node" - ] + "types": ["node"] }, - "include": [ - "src/**/*.ts" - ], - "exclude": [ - "src/**/*.spec.ts" - ] + "include": ["src/**/*.ts"], + "exclude": ["src/**/*.spec.ts"] } diff --git a/samples/client/angular/projects/rizzcharts/README.md b/samples/client/angular/projects/rizzcharts/README.md index ebb991c8b..c761a5c1b 100644 --- a/samples/client/angular/projects/rizzcharts/README.md +++ b/samples/client/angular/projects/rizzcharts/README.md @@ -9,10 +9,12 @@ These are sample implementations of A2UI in Angular. ## Running -1. Update the `src/environments/environment.ts` file with your Google Maps API key. +1. Update the `src/environments/environment.ts` file with your Google Maps API key. 2. Build the shared dependencies by running `npm i`, then `npm run build` in the `renderers/web_core` directory 3. Install the dependencies: `npm i` 4. Run the A2A server for the [rizzcharts agent](../../../../agent/adk/rizzcharts/python/) 5. Run the relevant app: - * `npm start -- rizzcharts` + +- `npm start -- rizzcharts` + 6. Open http://localhost:4200/ diff --git a/samples/client/angular/projects/rizzcharts/src/a2ui-catalog/canvas.ts b/samples/client/angular/projects/rizzcharts/src/a2ui-catalog/canvas.ts index 3a2571a6f..b41a851c6 100644 --- a/samples/client/angular/projects/rizzcharts/src/a2ui-catalog/canvas.ts +++ b/samples/client/angular/projects/rizzcharts/src/a2ui-catalog/canvas.ts @@ -14,10 +14,10 @@ * limitations under the License. */ -import { DynamicComponent } from '@a2ui/angular'; +import {DynamicComponent} from '@a2ui/angular'; import * as Types from '@a2ui/web_core/types/types'; -import { ChangeDetectionStrategy, Component, computed, inject, OnInit } from '@angular/core'; -import { CanvasService } from '@a2a_chat_canvas/services/canvas-service'; +import {ChangeDetectionStrategy, Component, computed, inject, OnInit} from '@angular/core'; +import {CanvasService} from '@a2a_chat_canvas/services/canvas-service'; @Component({ selector: 'a2ui-canvas', diff --git a/samples/client/angular/projects/rizzcharts/src/a2ui-catalog/catalog.ts b/samples/client/angular/projects/rizzcharts/src/a2ui-catalog/catalog.ts index a4acf3234..51d2d5d02 100644 --- a/samples/client/angular/projects/rizzcharts/src/a2ui-catalog/catalog.ts +++ b/samples/client/angular/projects/rizzcharts/src/a2ui-catalog/catalog.ts @@ -14,15 +14,15 @@ * limitations under the License. */ -import { Catalog, DEFAULT_CATALOG } from '@a2ui/angular'; -import { inputBinding } from '@angular/core'; +import {Catalog, DEFAULT_CATALOG} from '@a2ui/angular'; +import {inputBinding} from '@angular/core'; export const RIZZ_CHARTS_CATALOG = { ...DEFAULT_CATALOG, - Canvas: () => import('./canvas').then((r) => r.Canvas), + Canvas: () => import('./canvas').then(r => r.Canvas), Chart: { - type: () => import('./chart').then((r) => r.Chart), - bindings: ({ properties }) => [ + type: () => import('./chart').then(r => r.Chart), + bindings: ({properties}) => [ inputBinding('type', () => ('type' in properties && properties['type']) || undefined), inputBinding('title', () => ('title' in properties && properties['title']) || undefined), inputBinding( @@ -32,8 +32,8 @@ export const RIZZ_CHARTS_CATALOG = { ], }, GoogleMap: { - type: () => import('./google-map').then((r) => r.GoogleMap), - bindings: ({ properties }) => [ + type: () => import('./google-map').then(r => r.GoogleMap), + bindings: ({properties}) => [ inputBinding('zoom', () => ('zoom' in properties && properties['zoom']) || 8), inputBinding('center', () => ('center' in properties && properties['center']) || undefined), inputBinding('pins', () => ('pins' in properties && properties['pins']) || undefined), @@ -41,11 +41,17 @@ export const RIZZ_CHARTS_CATALOG = { ], }, YouTube: { - type: () => import('./youtube').then((r) => r.YouTube), - bindings: ({ properties }) => [ - inputBinding('videoId', () => ('videoId' in properties && properties['videoId']) || undefined), + type: () => import('./youtube').then(r => r.YouTube), + bindings: ({properties}) => [ + inputBinding( + 'videoId', + () => ('videoId' in properties && properties['videoId']) || undefined, + ), inputBinding('title', () => ('title' in properties && properties['title']) || undefined), - inputBinding('autoplay', () => ('autoplay' in properties && properties['autoplay']) || undefined), + inputBinding( + 'autoplay', + () => ('autoplay' in properties && properties['autoplay']) || undefined, + ), ], }, } as Catalog; diff --git a/samples/client/angular/projects/rizzcharts/src/a2ui-catalog/chart.ts b/samples/client/angular/projects/rizzcharts/src/a2ui-catalog/chart.ts index 835488817..b637053ae 100644 --- a/samples/client/angular/projects/rizzcharts/src/a2ui-catalog/chart.ts +++ b/samples/client/angular/projects/rizzcharts/src/a2ui-catalog/chart.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { DynamicComponent } from '@a2ui/angular'; +import {DynamicComponent} from '@a2ui/angular'; import * as Primitives from '@a2ui/web_core/types/primitives'; import * as Types from '@a2ui/web_core/types/types'; import { @@ -26,10 +26,10 @@ import { signal, ViewChild, } from '@angular/core'; -import { MatIconButton } from '@angular/material/button'; -import { MatIcon } from '@angular/material/icon'; -import { ChartData, ChartEvent, ChartOptions, ChartType, LegendItem } from 'chart.js'; -import { BaseChartDirective } from 'ng2-charts'; +import {MatIconButton} from '@angular/material/button'; +import {MatIcon} from '@angular/material/icon'; +import {ChartData, ChartEvent, ChartOptions, ChartType, LegendItem} from 'chart.js'; +import {BaseChartDirective} from 'ng2-charts'; @Component({ selector: 'a2ui-chart', @@ -155,7 +155,7 @@ export class Chart extends DynamicComponent { if (!allData) { return undefined; } - return { ...allData.get(selectedCategory) } as ChartData<'pie', number[], string>; + return {...allData.get(selectedCategory)} as ChartData<'pie', number[], string>; }, ); @@ -199,8 +199,8 @@ export class Chart extends DynamicComponent { if (pathPrefix?.path) { for (let index: number = 0; index < 500; index++) { const itemPrefix = `${pathPrefix.path}[${index}]`; - const labelPath: Primitives.StringValue = { path: `${itemPrefix}.label` }; - const valuePath: Primitives.NumberValue = { path: `${itemPrefix}.value` }; + const labelPath: Primitives.StringValue = {path: `${itemPrefix}.label`}; + const valuePath: Primitives.NumberValue = {path: `${itemPrefix}.value`}; const label = super.resolvePrimitive(labelPath); const value = super.resolvePrimitive(valuePath); if (label === null || value === null) { @@ -257,7 +257,7 @@ export class Chart extends DynamicComponent { this.selectedCategory.set('root'); } - protected onClick(e: { event?: ChartEvent; active?: any[] | undefined }) { + protected onClick(e: {event?: ChartEvent; active?: any[] | undefined}) { const active = e.active; if (!active || active.length === 0) return; diff --git a/samples/client/angular/projects/rizzcharts/src/a2ui-catalog/google-map.ts b/samples/client/angular/projects/rizzcharts/src/a2ui-catalog/google-map.ts index facc13baa..0759ac120 100644 --- a/samples/client/angular/projects/rizzcharts/src/a2ui-catalog/google-map.ts +++ b/samples/client/angular/projects/rizzcharts/src/a2ui-catalog/google-map.ts @@ -14,13 +14,13 @@ * limitations under the License. */ -import { DynamicComponent } from '@a2ui/angular'; +import {DynamicComponent} from '@a2ui/angular'; import * as Primitives from '@a2ui/web_core/types/primitives'; import * as Types from '@a2ui/web_core/types/types'; -import { ChangeDetectionStrategy, Component, computed, input } from '@angular/core'; -import { GoogleMapsModule } from '@angular/google-maps'; -import { MatIconButton } from '@angular/material/button'; -import { MatIcon } from '@angular/material/icon'; +import {ChangeDetectionStrategy, Component, computed, input} from '@angular/core'; +import {GoogleMapsModule} from '@angular/google-maps'; +import {MatIconButton} from '@angular/material/button'; +import {MatIcon} from '@angular/material/icon'; // --- Location Definitions --- interface Pin { @@ -117,7 +117,7 @@ export interface CustomProperties { [zoom]="resolvedZoom" height="500px" width="100%" - [options]="{ mapId: mapId }" + [options]="{mapId: mapId}" > @for (pin of resolvedPins(); track pin) { { return null; } - const latValue: Primitives.NumberValue = { path: `${value}.lat` }; - const lngValue: Primitives.NumberValue = { path: `${value}.lng` }; - const nameValue: Primitives.StringValue = { path: `${value}.name` }; - const descriptionValue: Primitives.StringValue = { path: `${value}.description` }; - const backgroundValue: Primitives.StringValue = { path: `${value}.background` }; - const borderColorValue: Primitives.StringValue = { path: `${value}.borderColor` }; - const glyphColorValue: Primitives.StringValue = { path: `${value}.glyphColor` }; + const latValue: Primitives.NumberValue = {path: `${value}.lat`}; + const lngValue: Primitives.NumberValue = {path: `${value}.lng`}; + const nameValue: Primitives.StringValue = {path: `${value}.name`}; + const descriptionValue: Primitives.StringValue = {path: `${value}.description`}; + const backgroundValue: Primitives.StringValue = {path: `${value}.background`}; + const borderColorValue: Primitives.StringValue = {path: `${value}.borderColor`}; + const glyphColorValue: Primitives.StringValue = {path: `${value}.glyphColor`}; const lat = this.resolvePrimitive(latValue); const lng = this.resolvePrimitive(lngValue); @@ -217,8 +217,8 @@ export class GoogleMap extends DynamicComponent { private resolveLatLng(value: CustomProperties | null): google.maps.LatLngLiteral { if (value?.path) { - const latValue: Primitives.NumberValue = { path: `${value.path}.lat` }; - const lngValue: Primitives.NumberValue = { path: `${value.path}.lng` }; + const latValue: Primitives.NumberValue = {path: `${value.path}.lat`}; + const lngValue: Primitives.NumberValue = {path: `${value.path}.lng`}; const lat = this.resolvePrimitive(latValue)!; const lng = this.resolvePrimitive(lngValue)!; return { diff --git a/samples/client/angular/projects/rizzcharts/src/a2ui-catalog/youtube.ts b/samples/client/angular/projects/rizzcharts/src/a2ui-catalog/youtube.ts index 914ea1858..f0fa50d2a 100644 --- a/samples/client/angular/projects/rizzcharts/src/a2ui-catalog/youtube.ts +++ b/samples/client/angular/projects/rizzcharts/src/a2ui-catalog/youtube.ts @@ -14,16 +14,11 @@ limitations under the License. */ -import { DynamicComponent } from '@a2ui/angular'; +import {DynamicComponent} from '@a2ui/angular'; import * as Primitives from '@a2ui/web_core/types/primitives'; import * as Types from '@a2ui/web_core/types/types'; -import { - ChangeDetectionStrategy, - Component, - computed, - input, -} from '@angular/core'; -import { DomSanitizer, SafeResourceUrl } from '@angular/platform-browser'; +import {ChangeDetectionStrategy, Component, computed, input} from '@angular/core'; +import {DomSanitizer, SafeResourceUrl} from '@angular/platform-browser'; @Component({ selector: 'a2ui-youtube', @@ -94,14 +89,10 @@ import { DomSanitizer, SafeResourceUrl } from '@angular/platform-browser'; }) export class YouTube extends DynamicComponent { readonly videoId = input.required(); - protected readonly resolvedVideoId = computed(() => - this.resolvePrimitive(this.videoId()), - ); + protected readonly resolvedVideoId = computed(() => this.resolvePrimitive(this.videoId())); readonly title = input(); - protected readonly resolvedTitle = computed(() => - this.resolvePrimitive(this.title() ?? null), - ); + protected readonly resolvedTitle = computed(() => this.resolvePrimitive(this.title() ?? null)); readonly autoplay = input(); protected readonly resolvedAutoplay = computed(() => diff --git a/samples/client/angular/projects/rizzcharts/src/app/app.config.server.ts b/samples/client/angular/projects/rizzcharts/src/app/app.config.server.ts index 017a5233a..3f3c54719 100644 --- a/samples/client/angular/projects/rizzcharts/src/app/app.config.server.ts +++ b/samples/client/angular/projects/rizzcharts/src/app/app.config.server.ts @@ -14,15 +14,13 @@ * limitations under the License. */ -import { mergeApplicationConfig, ApplicationConfig } from '@angular/core'; -import { provideServerRendering, withRoutes } from '@angular/ssr'; -import { appConfig } from './app.config'; -import { serverRoutes } from './app.routes.server'; +import {mergeApplicationConfig, ApplicationConfig} from '@angular/core'; +import {provideServerRendering, withRoutes} from '@angular/ssr'; +import {appConfig} from './app.config'; +import {serverRoutes} from './app.routes.server'; const serverConfig: ApplicationConfig = { - providers: [ - provideServerRendering(withRoutes(serverRoutes)) - ] + providers: [provideServerRendering(withRoutes(serverRoutes))], }; export const config = mergeApplicationConfig(appConfig, serverConfig); diff --git a/samples/client/angular/projects/rizzcharts/src/app/app.config.ts b/samples/client/angular/projects/rizzcharts/src/app/app.config.ts index 266e05194..d1bd293ac 100644 --- a/samples/client/angular/projects/rizzcharts/src/app/app.config.ts +++ b/samples/client/angular/projects/rizzcharts/src/app/app.config.ts @@ -25,17 +25,17 @@ import { provideBrowserGlobalErrorListeners, provideZonelessChangeDetection, } from '@angular/core'; -import { provideRouter } from '@angular/router'; -import { RIZZ_CHARTS_CATALOG } from '@rizzcharts/a2ui-catalog/catalog'; -import { provideCharts, withDefaultRegisterables } from 'ng2-charts'; -import { A2aService } from '../services/a2a_service'; -import { RizzchartsMarkdownRendererService } from '../services/markdown-renderer.service'; -import { theme } from './theme'; +import {provideRouter} from '@angular/router'; +import {RIZZ_CHARTS_CATALOG} from '@rizzcharts/a2ui-catalog/catalog'; +import {provideCharts, withDefaultRegisterables} from 'ng2-charts'; +import {A2aService} from '../services/a2a_service'; +import {RizzchartsMarkdownRendererService} from '../services/markdown-renderer.service'; +import {theme} from './theme'; -import { provideClientHydration, withEventReplay } from '@angular/platform-browser'; -import { routes } from './app.routes'; -import { provideMarkdownRenderer } from '@a2ui/angular'; -import { renderMarkdown } from '@a2ui/markdown-it'; +import {provideClientHydration, withEventReplay} from '@angular/platform-browser'; +import {routes} from './app.routes'; +import {provideMarkdownRenderer} from '@a2ui/angular'; +import {renderMarkdown} from '@a2ui/markdown-it'; export const appConfig: ApplicationConfig = { providers: [ diff --git a/samples/client/angular/projects/rizzcharts/src/app/app.html b/samples/client/angular/projects/rizzcharts/src/app/app.html index 0a6c46e98..5ce1d071b 100644 --- a/samples/client/angular/projects/rizzcharts/src/app/app.html +++ b/samples/client/angular/projects/rizzcharts/src/app/app.html @@ -19,35 +19,43 @@
-
-
-
- Rizz agent icon -
-
-
- {{ agentName() }} +
+
+
+ Rizz agent icon
-
- -

- I help you understand and visualize sales pipelines and analyze customer performance data. -

-
- - - +
+ {{ agentName() }}
+
+
+ +

+ I help you understand and visualize sales pipelines and analyze customer performance data. +

+
+ + + +
diff --git a/samples/client/angular/projects/rizzcharts/src/app/app.routes.server.ts b/samples/client/angular/projects/rizzcharts/src/app/app.routes.server.ts index 7a761dad0..417bd7b43 100644 --- a/samples/client/angular/projects/rizzcharts/src/app/app.routes.server.ts +++ b/samples/client/angular/projects/rizzcharts/src/app/app.routes.server.ts @@ -14,11 +14,11 @@ * limitations under the License. */ -import { RenderMode, ServerRoute } from '@angular/ssr'; +import {RenderMode, ServerRoute} from '@angular/ssr'; export const serverRoutes: ServerRoute[] = [ { path: '**', - renderMode: RenderMode.Prerender - } + renderMode: RenderMode.Prerender, + }, ]; diff --git a/samples/client/angular/projects/rizzcharts/src/app/app.routes.ts b/samples/client/angular/projects/rizzcharts/src/app/app.routes.ts index 54e9a215f..1cbd92c87 100644 --- a/samples/client/angular/projects/rizzcharts/src/app/app.routes.ts +++ b/samples/client/angular/projects/rizzcharts/src/app/app.routes.ts @@ -14,6 +14,6 @@ * limitations under the License. */ -import { Routes } from '@angular/router'; +import {Routes} from '@angular/router'; export const routes: Routes = []; diff --git a/samples/client/angular/projects/rizzcharts/src/app/app.scss b/samples/client/angular/projects/rizzcharts/src/app/app.scss index ef84a8eb7..abfa95cbd 100644 --- a/samples/client/angular/projects/rizzcharts/src/app/app.scss +++ b/samples/client/angular/projects/rizzcharts/src/app/app.scss @@ -21,7 +21,7 @@ main { .empty-history { display: block; padding-bottom: 8rem; - background-image: url("/Gradient.png"); + background-image: url('/Gradient.png'); background-size: contain; background-repeat: no-repeat; background-position-y: bottom; @@ -32,12 +32,7 @@ main { margin: 0; font: var(--mat-sys-display-small); // These values are not from the spec, but UX wants them for the gradient. - background: linear-gradient( - 90deg, - #217bfe 28.03%, - #078efb 49.56%, - #ac87eb 71.1% - ); + background: linear-gradient(90deg, #217bfe 28.03%, #078efb 49.56%, #ac87eb 71.1%); background-clip: text; color: transparent; } @@ -65,14 +60,13 @@ main { } .large-icon { - width: 32px; - height: 32px; - font-size: 32px; - border-radius: var(--mat-sys-corner-extra-large); - vertical-align: top; + width: 32px; + height: 32px; + font-size: 32px; + border-radius: var(--mat-sys-corner-extra-large); + vertical-align: top; } - .agent-header { display: flex; align-items: center; @@ -93,17 +87,17 @@ main { .chip { // Override Material button styles to look like a chip - border-radius: 100px !important; // Force pill shape - padding: 10px 16px !important; - display: inline-flex !important; - align-items: center !important; - height: auto !important; - line-height: 25px !important; // Match icon height + border-radius: 100px !important; // Force pill shape + padding: 10px 16px !important; + display: inline-flex !important; + align-items: center !important; + height: auto !important; + line-height: 25px !important; // Match icon height .material-icons-outlined { font-size: 20px; margin-right: 8px; - line-height: 1; // Prevent icon from affecting line height - position: relative; - top: 4px; // Move icon down slightly to match text + line-height: 1; // Prevent icon from affecting line height + position: relative; + top: 4px; // Move icon down slightly to match text } } diff --git a/samples/client/angular/projects/rizzcharts/src/app/app.spec.ts b/samples/client/angular/projects/rizzcharts/src/app/app.spec.ts index 20fdeeb37..6d28332ad 100644 --- a/samples/client/angular/projects/rizzcharts/src/app/app.spec.ts +++ b/samples/client/angular/projects/rizzcharts/src/app/app.spec.ts @@ -14,9 +14,9 @@ * limitations under the License. */ -import { provideZonelessChangeDetection } from '@angular/core'; -import { TestBed } from '@angular/core/testing'; -import { App } from './app'; +import {provideZonelessChangeDetection} from '@angular/core'; +import {TestBed} from '@angular/core/testing'; +import {App} from './app'; describe('App', () => { beforeEach(async () => { diff --git a/samples/client/angular/projects/rizzcharts/src/app/app.ts b/samples/client/angular/projects/rizzcharts/src/app/app.ts index 6ccf25c05..abf3be519 100644 --- a/samples/client/angular/projects/rizzcharts/src/app/app.ts +++ b/samples/client/angular/projects/rizzcharts/src/app/app.ts @@ -23,14 +23,14 @@ import { inject, signal, } from '@angular/core'; -import { DOCUMENT } from '@angular/common'; -import { RouterOutlet } from '@angular/router'; -import { MatButtonModule } from '@angular/material/button'; -import { A2aChatCanvas } from '@a2a_chat_canvas/a2a-chat-canvas'; -import { ChatService } from '@a2a_chat_canvas/services/chat-service'; -import { Toolbar } from '@rizzcharts/components/toolbar/toolbar'; -import { environment } from '@rizzcharts/environments/environment'; -import { A2aService } from '@rizzcharts/services/a2a_service'; +import {DOCUMENT} from '@angular/common'; +import {RouterOutlet} from '@angular/router'; +import {MatButtonModule} from '@angular/material/button'; +import {A2aChatCanvas} from '@a2a_chat_canvas/a2a-chat-canvas'; +import {ChatService} from '@a2a_chat_canvas/services/chat-service'; +import {Toolbar} from '@rizzcharts/components/toolbar/toolbar'; +import {environment} from '@rizzcharts/environments/environment'; +import {A2aService} from '@rizzcharts/services/a2a_service'; @Component({ selector: 'app-root', @@ -55,7 +55,7 @@ export class App implements OnInit { script.async = true; script.defer = true; this._renderer2.appendChild(this._document.body, script); - this.a2aService.getAgentCard().then((card) => { + this.a2aService.getAgentCard().then(card => { this.agentName.set(card.name); }); } diff --git a/samples/client/angular/projects/rizzcharts/src/app/theme.ts b/samples/client/angular/projects/rizzcharts/src/app/theme.ts index 06f1a0242..54f1d383d 100644 --- a/samples/client/angular/projects/rizzcharts/src/app/theme.ts +++ b/samples/client/angular/projects/rizzcharts/src/app/theme.ts @@ -160,16 +160,16 @@ const video = { 'layout-el-cv': true, }; -const aLight = Styles.merge(a, { 'color-c-n5': true }); -const inputLight = Styles.merge(input, { 'color-c-n5': true }); -const textareaLight = Styles.merge(textarea, { 'color-c-n5': true }); -const buttonLight = Styles.merge(button, { 'color-c-n100': true }); -const h1Light = Styles.merge(h1, { 'color-c-n5': true }); -const h2Light = Styles.merge(h2, { 'color-c-n5': true }); -const h3Light = Styles.merge(h3, { 'color-c-n5': true }); -const bodyLight = Styles.merge(body, { 'color-c-n5': true }); -const pLight = Styles.merge(p, { 'color-c-n35': true }); -const preLight = Styles.merge(pre, { 'color-c-n35': true }); +const aLight = Styles.merge(a, {'color-c-n5': true}); +const inputLight = Styles.merge(input, {'color-c-n5': true}); +const textareaLight = Styles.merge(textarea, {'color-c-n5': true}); +const buttonLight = Styles.merge(button, {'color-c-n100': true}); +const h1Light = Styles.merge(h1, {'color-c-n5': true}); +const h2Light = Styles.merge(h2, {'color-c-n5': true}); +const h3Light = Styles.merge(h3, {'color-c-n5': true}); +const bodyLight = Styles.merge(body, {'color-c-n5': true}); +const pLight = Styles.merge(p, {'color-c-n35': true}); +const preLight = Styles.merge(pre, {'color-c-n35': true}); const orderedListLight = Styles.merge(orderedList, { 'color-c-n35': true, }); @@ -200,7 +200,7 @@ export const theme: Types.Theme = { 'color-c-n100': true, 'behavior-ho-70': true, }, - Card: { 'border-br-9': true, 'color-bgc-p100': true, 'layout-p-4': true }, + Card: {'border-br-9': true, 'color-bgc-p100': true, 'layout-p-4': true}, CheckBox: { element: { 'layout-m-0': true, @@ -268,7 +268,7 @@ export const theme: Types.Theme = { 'layout-p-2': true, }, Modal: { - backdrop: { 'color-bbgc-p60_20': true }, + backdrop: {'color-bbgc-p60_20': true}, element: { 'border-br-2': true, 'color-bgc-p100': true, @@ -293,7 +293,7 @@ export const theme: Types.Theme = { }, Tabs: { container: {}, - controls: { all: {}, selected: {} }, + controls: {all: {}, selected: {}}, element: {}, }, Text: { @@ -308,7 +308,7 @@ export const theme: Types.Theme = { h4: {}, h5: {}, caption: {}, - body: {} + body: {}, }, TextField: { container: { diff --git a/samples/client/angular/projects/rizzcharts/src/components/toolbar/toolbar.html b/samples/client/angular/projects/rizzcharts/src/components/toolbar/toolbar.html index 3b06d0c68..ec78476ec 100644 --- a/samples/client/angular/projects/rizzcharts/src/components/toolbar/toolbar.html +++ b/samples/client/angular/projects/rizzcharts/src/components/toolbar/toolbar.html @@ -24,13 +24,17 @@ A2UI Component Catalog - + @for (option of catalogs; track option) { - {{ option.viewValue }} + {{ option.viewValue }} } - \ No newline at end of file + diff --git a/samples/client/angular/projects/rizzcharts/src/components/toolbar/toolbar.scss b/samples/client/angular/projects/rizzcharts/src/components/toolbar/toolbar.scss index f41813f36..3f6487020 100644 --- a/samples/client/angular/projects/rizzcharts/src/components/toolbar/toolbar.scss +++ b/samples/client/angular/projects/rizzcharts/src/components/toolbar/toolbar.scss @@ -12,22 +12,20 @@ // See the License for the specific language governing permissions and // limitations under the License. - .example-spacer { flex: 1 1 auto; } .logo { - } .profile-photo { - width: 40px; - height: 40px; - font-size: 40px; - border-radius: var(--mat-sys-corner-extra-large); - vertical-align: top; - margin-right: 0.5rem; + width: 40px; + height: 40px; + font-size: 40px; + border-radius: var(--mat-sys-corner-extra-large); + vertical-align: top; + margin-right: 0.5rem; } .logo-dark { @@ -57,4 +55,4 @@ mat-toolbar { mat-form-field { width: 350px; -} \ No newline at end of file +} diff --git a/samples/client/angular/projects/rizzcharts/src/components/toolbar/toolbar.spec.ts b/samples/client/angular/projects/rizzcharts/src/components/toolbar/toolbar.spec.ts index 75d69973d..12bd46f70 100644 --- a/samples/client/angular/projects/rizzcharts/src/components/toolbar/toolbar.spec.ts +++ b/samples/client/angular/projects/rizzcharts/src/components/toolbar/toolbar.spec.ts @@ -14,9 +14,9 @@ * limitations under the License. */ -import { ComponentFixture, TestBed } from '@angular/core/testing'; +import {ComponentFixture, TestBed} from '@angular/core/testing'; -import { Toolbar } from './toolbar'; +import {Toolbar} from './toolbar'; describe('Toolbar', () => { let component: Toolbar; @@ -24,9 +24,8 @@ describe('Toolbar', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [Toolbar] - }) - .compileComponents(); + imports: [Toolbar], + }).compileComponents(); fixture = TestBed.createComponent(Toolbar); component = fixture.componentInstance; diff --git a/samples/client/angular/projects/rizzcharts/src/components/toolbar/toolbar.ts b/samples/client/angular/projects/rizzcharts/src/components/toolbar/toolbar.ts index 093ec7c48..e0ead1854 100644 --- a/samples/client/angular/projects/rizzcharts/src/components/toolbar/toolbar.ts +++ b/samples/client/angular/projects/rizzcharts/src/components/toolbar/toolbar.ts @@ -14,16 +14,16 @@ * limitations under the License. */ -import { inject, Component, ChangeDetectionStrategy } from '@angular/core'; -import { CatalogService } from '../../services/catalog_service'; -import { MatButtonModule } from '@angular/material/button'; -import { MatIconModule } from '@angular/material/icon'; -import { MatToolbarModule } from '@angular/material/toolbar'; +import {inject, Component, ChangeDetectionStrategy} from '@angular/core'; +import {CatalogService} from '../../services/catalog_service'; +import {MatButtonModule} from '@angular/material/button'; +import {MatIconModule} from '@angular/material/icon'; +import {MatToolbarModule} from '@angular/material/toolbar'; -import { MatInputModule } from '@angular/material/input'; -import { MatSelectModule } from '@angular/material/select'; -import { MatFormFieldModule } from '@angular/material/form-field'; -import { FormsModule } from '@angular/forms'; +import {MatInputModule} from '@angular/material/input'; +import {MatSelectModule} from '@angular/material/select'; +import {MatFormFieldModule} from '@angular/material/form-field'; +import {FormsModule} from '@angular/forms'; @Component({ selector: 'app-toolbar', @@ -57,7 +57,7 @@ export class Toolbar { ]; ngOnInit() { - this.selectedCatalogs = this.catalogs.map((c) => c.value); + this.selectedCatalogs = this.catalogs.map(c => c.value); this.updateCatalogService(); } diff --git a/samples/client/angular/projects/rizzcharts/src/environments/environment.ts b/samples/client/angular/projects/rizzcharts/src/environments/environment.ts index f545848b8..eb25a48fa 100644 --- a/samples/client/angular/projects/rizzcharts/src/environments/environment.ts +++ b/samples/client/angular/projects/rizzcharts/src/environments/environment.ts @@ -15,5 +15,5 @@ */ export const environment = { - googleMapsApiKey: 'YOUR_API_KEY_HERE' + googleMapsApiKey: 'YOUR_API_KEY_HERE', }; diff --git a/samples/client/angular/projects/rizzcharts/src/index.html b/samples/client/angular/projects/rizzcharts/src/index.html index 5ffb846f5..5355f072a 100644 --- a/samples/client/angular/projects/rizzcharts/src/index.html +++ b/samples/client/angular/projects/rizzcharts/src/index.html @@ -22,8 +22,8 @@ - - + + - bootstrapApplication(App, config, context); +const bootstrap = (context: BootstrapContext) => bootstrapApplication(App, config, context); export default bootstrap; diff --git a/samples/client/angular/projects/rizzcharts/src/main.ts b/samples/client/angular/projects/rizzcharts/src/main.ts index d9d99cb08..796e8b29e 100644 --- a/samples/client/angular/projects/rizzcharts/src/main.ts +++ b/samples/client/angular/projects/rizzcharts/src/main.ts @@ -14,13 +14,12 @@ * limitations under the License. */ -import { bootstrapApplication } from '@angular/platform-browser'; -import { appConfig } from './app/app.config'; -import { App } from './app/app'; -import { Chart, registerables } from 'chart.js'; +import {bootstrapApplication} from '@angular/platform-browser'; +import {appConfig} from './app/app.config'; +import {App} from './app/app'; +import {Chart, registerables} from 'chart.js'; import ChartDataLabels from 'chartjs-plugin-datalabels'; Chart.register(...registerables, ChartDataLabels); -bootstrapApplication(App, appConfig) - .catch((err) => console.error(err)); +bootstrapApplication(App, appConfig).catch(err => console.error(err)); diff --git a/samples/client/angular/projects/rizzcharts/src/pipes/markdown.pipe.spec.ts b/samples/client/angular/projects/rizzcharts/src/pipes/markdown.pipe.spec.ts index 945d9287e..3ebfe961d 100644 --- a/samples/client/angular/projects/rizzcharts/src/pipes/markdown.pipe.spec.ts +++ b/samples/client/angular/projects/rizzcharts/src/pipes/markdown.pipe.spec.ts @@ -14,9 +14,9 @@ * limitations under the License. */ -import { TestBed } from '@angular/core/testing'; -import { DomSanitizer } from '@angular/platform-browser'; -import { MarkdownPipe } from './markdown.pipe'; +import {TestBed} from '@angular/core/testing'; +import {DomSanitizer} from '@angular/platform-browser'; +import {MarkdownPipe} from './markdown.pipe'; describe('MarkdownPipe', () => { let pipe: MarkdownPipe; @@ -48,7 +48,6 @@ describe('MarkdownPipe', () => { expect(result).toContain('bold'); }); - it('should open links in new tab', () => { const markdown = '[link](http://example.com)'; const result = pipe.transform(markdown); diff --git a/samples/client/angular/projects/rizzcharts/src/pipes/markdown.pipe.ts b/samples/client/angular/projects/rizzcharts/src/pipes/markdown.pipe.ts index 77e13e7f1..abc6b3932 100644 --- a/samples/client/angular/projects/rizzcharts/src/pipes/markdown.pipe.ts +++ b/samples/client/angular/projects/rizzcharts/src/pipes/markdown.pipe.ts @@ -14,11 +14,11 @@ * limitations under the License. */ -import { Pipe, PipeTransform, inject } from '@angular/core'; -import { DomSanitizer, SafeHtml } from '@angular/platform-browser'; +import {Pipe, PipeTransform, inject} from '@angular/core'; +import {DomSanitizer, SafeHtml} from '@angular/platform-browser'; import markdownit from 'markdown-it'; -@Pipe({ name: 'markdown' }) +@Pipe({name: 'markdown'}) export class MarkdownPipe implements PipeTransform { private readonly sanitizer = inject(DomSanitizer); private readonly md = markdownit({ diff --git a/samples/client/angular/projects/rizzcharts/src/server.ts b/samples/client/angular/projects/rizzcharts/src/server.ts index 5bf454a1e..76ff0906b 100644 --- a/samples/client/angular/projects/rizzcharts/src/server.ts +++ b/samples/client/angular/projects/rizzcharts/src/server.ts @@ -21,10 +21,10 @@ import { writeResponseToNodeResponse, } from '@angular/ssr/node'; import express from 'express'; -import { join } from 'node:path'; -import { v4 as uuidv4 } from 'uuid'; -import { A2AClient } from '@a2a-js/sdk/client'; -import { Message, MessageSendParams, Part, SendMessageSuccessResponse, Task } from '@a2a-js/sdk'; +import {join} from 'node:path'; +import {v4 as uuidv4} from 'uuid'; +import {A2AClient} from '@a2a-js/sdk/client'; +import {Message, MessageSendParams, Part, SendMessageSuccessResponse, Task} from '@a2a-js/sdk'; const browserDistFolder = join(import.meta.dirname, '../browser'); const app = express(); @@ -42,7 +42,7 @@ app.use( app.post('/a2a', (req, res) => { let originalBody = ''; - req.on('data', (chunk) => { + req.on('data', chunk => { originalBody += chunk.toString(); }); @@ -73,7 +73,7 @@ app.post('/a2a', (req, res) => { if ('error' in response) { console.error('Error:', response.error.message); - res.status(500).json({ error: response.error.message }); + res.status(500).json({error: response.error.message}); return; } @@ -83,29 +83,31 @@ app.post('/a2a', (req, res) => { app.get('/a2a/agent-card', async (req, res) => { try { - const response = await fetchWithCustomHeader('http://localhost:10002/.well-known/agent-card.json'); + const response = await fetchWithCustomHeader( + 'http://localhost:10002/.well-known/agent-card.json', + ); if (!response.ok) { - res.status(response.status).json({ error: 'Failed to fetch agent card' }); + res.status(response.status).json({error: 'Failed to fetch agent card'}); return; } const card = await response.json(); res.json(card); } catch (error) { console.error('Error fetching agent card:', error); - res.status(500).json({ error: 'Internal server error' }); + res.status(500).json({error: 'Internal server error'}); } }); app.use((req, res, next) => { angularApp .handle(req) - .then((response) => (response ? writeResponseToNodeResponse(response, res) : next())) + .then(response => (response ? writeResponseToNodeResponse(response, res) : next())) .catch(next); }); if (isMainModule(import.meta.url) || process.env['pm_id']) { const port = process.env['PORT'] || 4000; - app.listen(port, (error) => { + app.listen(port, error => { if (error) { throw error; } @@ -117,7 +119,7 @@ if (isMainModule(import.meta.url) || process.env['pm_id']) { async function fetchWithCustomHeader(url: string | URL | Request, init?: RequestInit) { const headers = new Headers(init?.headers); headers.set('X-A2A-Extensions', 'https://a2ui.org/a2a-extension/a2ui/v0.8'); - const newInit = { ...init, headers }; + const newInit = {...init, headers}; return fetch(url, newInit); } diff --git a/samples/client/angular/projects/rizzcharts/src/services/a2a_service.ts b/samples/client/angular/projects/rizzcharts/src/services/a2a_service.ts index 1f501d259..0c766d170 100644 --- a/samples/client/angular/projects/rizzcharts/src/services/a2a_service.ts +++ b/samples/client/angular/projects/rizzcharts/src/services/a2a_service.ts @@ -14,43 +14,43 @@ * limitations under the License. */ -import { AgentCard, Part, SendMessageSuccessResponse } from '@a2a-js/sdk'; -import { A2aService as A2aServiceInterface } from '@a2a_chat_canvas/interfaces/a2a-service'; -import { Injectable } from '@angular/core'; -import { CatalogService } from './catalog_service'; +import {AgentCard, Part, SendMessageSuccessResponse} from '@a2a-js/sdk'; +import {A2aService as A2aServiceInterface} from '@a2a_chat_canvas/interfaces/a2a-service'; +import {Injectable} from '@angular/core'; +import {CatalogService} from './catalog_service'; -@Injectable({ providedIn: 'root' }) +@Injectable({providedIn: 'root'}) export class A2aService implements A2aServiceInterface { private contextId?: string; - constructor(private catalogService: CatalogService) { } + constructor(private catalogService: CatalogService) {} async sendMessage(parts: Part[], signal?: AbortSignal): Promise { const currentCatalogUris = this.catalogService.catalogUris; - console.log("Attaching supported A2UI catalogs to message: ", currentCatalogUris); + console.log('Attaching supported A2UI catalogs to message: ', currentCatalogUris); const response = await fetch('/a2a', { body: JSON.stringify({ - 'parts': parts, - 'metadata': { - "a2uiClientCapabilities": { - "supportedCatalogIds": currentCatalogUris - } + parts: parts, + metadata: { + a2uiClientCapabilities: { + supportedCatalogIds: currentCatalogUris, + }, }, - 'contextId': this.contextId + contextId: this.contextId, }), method: 'POST', signal, }); if (response.ok) { - const json = await response.json() as SendMessageSuccessResponse & { contextId?: string }; + const json = (await response.json()) as SendMessageSuccessResponse & {contextId?: string}; if (json.contextId || json.result?.contextId) { this.contextId = json.contextId || json.result?.contextId; } return json; } - const error = (await response.json()) as { error: string }; + const error = (await response.json()) as {error: string}; throw new Error(error.error); } @@ -59,7 +59,7 @@ export class A2aService implements A2aServiceInterface { if (!response.ok) { throw new Error('Failed to fetch agent card'); } - const card = await response.json() as AgentCard; + const card = (await response.json()) as AgentCard; // Override iconUrl to use local asset card.iconUrl = 'rizz-agent.png'; return card; diff --git a/samples/client/angular/projects/rizzcharts/src/services/catalog_service.ts b/samples/client/angular/projects/rizzcharts/src/services/catalog_service.ts index c2e034f66..dafb4a64a 100644 --- a/samples/client/angular/projects/rizzcharts/src/services/catalog_service.ts +++ b/samples/client/angular/projects/rizzcharts/src/services/catalog_service.ts @@ -14,9 +14,9 @@ * limitations under the License. */ -import { Injectable } from '@angular/core'; +import {Injectable} from '@angular/core'; -@Injectable({ providedIn: 'root' }) +@Injectable({providedIn: 'root'}) export class CatalogService { public catalogUris: string[] = []; } diff --git a/samples/client/angular/projects/rizzcharts/src/services/markdown-renderer.service.spec.ts b/samples/client/angular/projects/rizzcharts/src/services/markdown-renderer.service.spec.ts index 835265cbf..5d598d946 100644 --- a/samples/client/angular/projects/rizzcharts/src/services/markdown-renderer.service.spec.ts +++ b/samples/client/angular/projects/rizzcharts/src/services/markdown-renderer.service.spec.ts @@ -14,9 +14,9 @@ * limitations under the License. */ -import { TestBed } from '@angular/core/testing'; -import { DomSanitizer } from '@angular/platform-browser'; -import { RizzchartsMarkdownRendererService } from './markdown-renderer.service'; +import {TestBed} from '@angular/core/testing'; +import {DomSanitizer} from '@angular/platform-browser'; +import {RizzchartsMarkdownRendererService} from './markdown-renderer.service'; describe('RizzchartsMarkdownRendererService', () => { let service: RizzchartsMarkdownRendererService; diff --git a/samples/client/angular/projects/rizzcharts/src/services/markdown-renderer.service.ts b/samples/client/angular/projects/rizzcharts/src/services/markdown-renderer.service.ts index 30beebb1d..3b5fab772 100644 --- a/samples/client/angular/projects/rizzcharts/src/services/markdown-renderer.service.ts +++ b/samples/client/angular/projects/rizzcharts/src/services/markdown-renderer.service.ts @@ -14,9 +14,9 @@ * limitations under the License. */ -import { MarkdownRendererService } from '@a2a_chat_canvas/interfaces/markdown-renderer-service'; -import { inject, Injectable } from '@angular/core'; -import { DomSanitizer, SafeHtml } from '@angular/platform-browser'; +import {MarkdownRendererService} from '@a2a_chat_canvas/interfaces/markdown-renderer-service'; +import {inject, Injectable} from '@angular/core'; +import {DomSanitizer, SafeHtml} from '@angular/platform-browser'; import markdownit from 'markdown-it'; @Injectable({ diff --git a/samples/client/angular/projects/rizzcharts/src/styles.scss b/samples/client/angular/projects/rizzcharts/src/styles.scss index 236d15a04..7238c36f6 100644 --- a/samples/client/angular/projects/rizzcharts/src/styles.scss +++ b/samples/client/angular/projects/rizzcharts/src/styles.scss @@ -12,11 +12,9 @@ // See the License for the specific language governing permissions and // limitations under the License. - // You can add global styles to this file, and also import other style files @use '@angular/material' as mat; - @mixin styled-scrollbar { ::-webkit-scrollbar { width: 0.5rem; @@ -41,15 +39,17 @@ html { color-scheme: light dark; - @include mat.theme(( - color: mat.$blue-palette, - typography: Roboto, - density: 0 - )); + @include mat.theme( + ( + color: mat.$blue-palette, + typography: Roboto, + density: 0, + ) + ); @include styled-scrollbar; } - + :root { --n-100: #ffffff; --n-99: #fcfcfc; diff --git a/samples/client/angular/projects/rizzcharts/src/utils/utils.ts b/samples/client/angular/projects/rizzcharts/src/utils/utils.ts index 37d688495..cfd9eed17 100644 --- a/samples/client/angular/projects/rizzcharts/src/utils/utils.ts +++ b/samples/client/angular/projects/rizzcharts/src/utils/utils.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { Part } from '@a2a-js/sdk'; +import {Part} from '@a2a-js/sdk'; /** * Returns true if the part is a thought. @@ -27,6 +27,6 @@ export function isAgentThought(part: Part): boolean { return false; } - const metadata = part.metadata as { adk_thought: boolean }; + const metadata = part.metadata as {adk_thought: boolean}; return metadata.adk_thought; } diff --git a/samples/client/angular/projects/rizzcharts/tsconfig.app.json b/samples/client/angular/projects/rizzcharts/tsconfig.app.json index e04b233e0..6c6f9b1d1 100644 --- a/samples/client/angular/projects/rizzcharts/tsconfig.app.json +++ b/samples/client/angular/projects/rizzcharts/tsconfig.app.json @@ -4,14 +4,8 @@ "extends": "../../tsconfig.json", "compilerOptions": { "outDir": "../../out-tsc/app", - "types": [ - "node" - ], + "types": ["node"] }, - "include": [ - "src/**/*.ts" - ], - "exclude": [ - "src/**/*.spec.ts" - ] + "include": ["src/**/*.ts"], + "exclude": ["src/**/*.spec.ts"] } diff --git a/samples/client/angular/projects/rizzcharts/tsconfig.spec.json b/samples/client/angular/projects/rizzcharts/tsconfig.spec.json index 0feea88ed..43ba0b186 100644 --- a/samples/client/angular/projects/rizzcharts/tsconfig.spec.json +++ b/samples/client/angular/projects/rizzcharts/tsconfig.spec.json @@ -4,11 +4,7 @@ "extends": "../../tsconfig.json", "compilerOptions": { "outDir": "../../out-tsc/spec", - "types": [ - "jasmine" - ] + "types": ["jasmine"] }, - "include": [ - "src/**/*.ts" - ] + "include": ["src/**/*.ts"] } diff --git a/samples/client/angular/tsconfig.json b/samples/client/angular/tsconfig.json index c51960598..999f107af 100644 --- a/samples/client/angular/tsconfig.json +++ b/samples/client/angular/tsconfig.json @@ -5,18 +5,10 @@ "noImplicitOverride": true, "noPropertyAccessFromIndexSignature": true, "paths": { - "@a2ui/angular": [ - "./projects/lib/src/public-api.ts" - ], - "@a2ui/angular/v0_9": [ - "./projects/lib/src/v0_9/public-api.ts" - ], - "@a2a_chat_canvas/*": [ - "./projects/a2a-chat-canvas/src/lib/*" - ], - "@rizzcharts/*": [ - "./projects/rizzcharts/src/*" - ] + "@a2ui/angular": ["./projects/lib/src/public-api.ts"], + "@a2ui/angular/v0_9": ["./projects/lib/src/v0_9/public-api.ts"], + "@a2a_chat_canvas/*": ["./projects/a2a-chat-canvas/src/lib/*"], + "@rizzcharts/*": ["./projects/rizzcharts/src/*"] }, "noImplicitReturns": true, "noFallthroughCasesInSwitch": true, diff --git a/samples/client/flutter/restaurant_finder/app/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/samples/client/flutter/restaurant_finder/app/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json index a2ec33f19..60e60c947 100644 --- a/samples/client/flutter/restaurant_finder/app/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json +++ b/samples/client/flutter/restaurant_finder/app/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -1,68 +1,68 @@ { - "images" : [ + "images": [ { - "size" : "16x16", - "idiom" : "mac", - "filename" : "app_icon_16.png", - "scale" : "1x" + "size": "16x16", + "idiom": "mac", + "filename": "app_icon_16.png", + "scale": "1x" }, { - "size" : "16x16", - "idiom" : "mac", - "filename" : "app_icon_32.png", - "scale" : "2x" + "size": "16x16", + "idiom": "mac", + "filename": "app_icon_32.png", + "scale": "2x" }, { - "size" : "32x32", - "idiom" : "mac", - "filename" : "app_icon_32.png", - "scale" : "1x" + "size": "32x32", + "idiom": "mac", + "filename": "app_icon_32.png", + "scale": "1x" }, { - "size" : "32x32", - "idiom" : "mac", - "filename" : "app_icon_64.png", - "scale" : "2x" + "size": "32x32", + "idiom": "mac", + "filename": "app_icon_64.png", + "scale": "2x" }, { - "size" : "128x128", - "idiom" : "mac", - "filename" : "app_icon_128.png", - "scale" : "1x" + "size": "128x128", + "idiom": "mac", + "filename": "app_icon_128.png", + "scale": "1x" }, { - "size" : "128x128", - "idiom" : "mac", - "filename" : "app_icon_256.png", - "scale" : "2x" + "size": "128x128", + "idiom": "mac", + "filename": "app_icon_256.png", + "scale": "2x" }, { - "size" : "256x256", - "idiom" : "mac", - "filename" : "app_icon_256.png", - "scale" : "1x" + "size": "256x256", + "idiom": "mac", + "filename": "app_icon_256.png", + "scale": "1x" }, { - "size" : "256x256", - "idiom" : "mac", - "filename" : "app_icon_512.png", - "scale" : "2x" + "size": "256x256", + "idiom": "mac", + "filename": "app_icon_512.png", + "scale": "2x" }, { - "size" : "512x512", - "idiom" : "mac", - "filename" : "app_icon_512.png", - "scale" : "1x" + "size": "512x512", + "idiom": "mac", + "filename": "app_icon_512.png", + "scale": "1x" }, { - "size" : "512x512", - "idiom" : "mac", - "filename" : "app_icon_1024.png", - "scale" : "2x" + "size": "512x512", + "idiom": "mac", + "filename": "app_icon_1024.png", + "scale": "2x" } ], - "info" : { - "version" : 1, - "author" : "xcode" + "info": { + "version": 1, + "author": "xcode" } } diff --git a/samples/client/flutter/restaurant_finder/app/pubspec.yaml b/samples/client/flutter/restaurant_finder/app/pubspec.yaml index 03848ed45..bddee44ec 100644 --- a/samples/client/flutter/restaurant_finder/app/pubspec.yaml +++ b/samples/client/flutter/restaurant_finder/app/pubspec.yaml @@ -29,7 +29,6 @@ dependencies: genui_a2a: ^0.8.0 logging: ^1.3.0 - dev_dependencies: flutter_test: sdk: flutter diff --git a/samples/client/flutter/restaurant_finder/app/web/index.html b/samples/client/flutter/restaurant_finder/app/web/index.html index 8ef603914..1e39b9a3a 100644 --- a/samples/client/flutter/restaurant_finder/app/web/index.html +++ b/samples/client/flutter/restaurant_finder/app/web/index.html @@ -14,10 +14,10 @@ limitations under the License. --> - + - - - - - - - - - - - - - - - - - - app - - - - + + + + + + + + + app + + + + - - + + diff --git a/samples/client/flutter/restaurant_finder/app/web/manifest.json b/samples/client/flutter/restaurant_finder/app/web/manifest.json index 5620a335a..85243e14c 100644 --- a/samples/client/flutter/restaurant_finder/app/web/manifest.json +++ b/samples/client/flutter/restaurant_finder/app/web/manifest.json @@ -1,35 +1,35 @@ { - "name": "app", - "short_name": "app", - "start_url": ".", - "display": "standalone", - "background_color": "#0175C2", - "theme_color": "#0175C2", - "description": "A new Flutter project.", - "orientation": "portrait-primary", - "prefer_related_applications": false, - "icons": [ - { - "src": "icons/Icon-192.png", - "sizes": "192x192", - "type": "image/png" - }, - { - "src": "icons/Icon-512.png", - "sizes": "512x512", - "type": "image/png" - }, - { - "src": "icons/Icon-maskable-192.png", - "sizes": "192x192", - "type": "image/png", - "purpose": "maskable" - }, - { - "src": "icons/Icon-maskable-512.png", - "sizes": "512x512", - "type": "image/png", - "purpose": "maskable" - } - ] + "name": "app", + "short_name": "app", + "start_url": ".", + "display": "standalone", + "background_color": "#0175C2", + "theme_color": "#0175C2", + "description": "A new Flutter project.", + "orientation": "portrait-primary", + "prefer_related_applications": false, + "icons": [ + { + "src": "icons/Icon-192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "icons/Icon-512.png", + "sizes": "512x512", + "type": "image/png" + }, + { + "src": "icons/Icon-maskable-192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "maskable" + }, + { + "src": "icons/Icon-maskable-512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "maskable" + } + ] } diff --git a/samples/client/flutter/restaurant_finder/e2e_test/README.md b/samples/client/flutter/restaurant_finder/e2e_test/README.md index 4cc22887d..d752927ef 100644 --- a/samples/client/flutter/restaurant_finder/e2e_test/README.md +++ b/samples/client/flutter/restaurant_finder/e2e_test/README.md @@ -6,6 +6,6 @@ Tests that checks the following entities work well together: 2. Python restaurant finder agent 3. AI model -This tests are in separate package and +This tests are in separate package and run in a separate CI/CD pipeline, because they require API key. diff --git a/samples/client/flutter/restaurant_finder/e2e_test/pubspec.yaml b/samples/client/flutter/restaurant_finder/e2e_test/pubspec.yaml index c83447bd0..6518e642b 100644 --- a/samples/client/flutter/restaurant_finder/e2e_test/pubspec.yaml +++ b/samples/client/flutter/restaurant_finder/e2e_test/pubspec.yaml @@ -31,7 +31,6 @@ dependencies: restaurant_finder_client: path: ../app - dev_dependencies: flutter_test: sdk: flutter diff --git a/samples/client/lit/custom-components-example/README.md b/samples/client/lit/custom-components-example/README.md index b161f8236..8ef49ae8a 100644 --- a/samples/client/lit/custom-components-example/README.md +++ b/samples/client/lit/custom-components-example/README.md @@ -11,6 +11,7 @@ This is a UI to generate and visualize A2UI responses. This sample depends on the Lit renderer. Before running this sample, you need to build the renderer. 1. **Build the renderer:** + ```bash cd ../../../renderers/lit npm install @@ -18,6 +19,7 @@ This sample depends on the Lit renderer. Before running this sample, you need to ``` 2. **Run this sample:** + ```bash cd - # back to the sample directory npm install @@ -26,7 +28,7 @@ This sample depends on the Lit renderer. Before running this sample, you need to 3. **Run the servers:** - Run the [A2A server](../../../agent/adk/custom-components-example/) - By default, the server uses the `McpAppsCustomComponent` which wraps MCP Apps in a secure, isolated double-iframe sandbox (`sandbox.html`) communicating strictly via JSON-RPC. - - Optionally run the server using `USE_MCP_SANDBOX=false uv run .` to bypass this security and use the standard `WebFrame` element. + - Optionally run the server using `USE_MCP_SANDBOX=false uv run .` to bypass this security and use the standard `WebFrame` element. - **Observing the difference**: Search for "Alex Jordan" in the UI and click the Location button to open the floor plan. If you inspect the DOM using your browser's Developer Tools, you will see that `McpAppsCustomComponent` securely points the iframe `src` to the local proxy (`/sandbox.html`). In contrast, `WebFrame` directly injects the untrusted HTML via a data blob/srcdoc, lacking defense-in-depth origin isolation. - Run the dev server: `npm run dev` @@ -36,11 +38,11 @@ After starting the dev server, you can open http://localhost:5173/ to view the s This sample showcases several custom components that go beyond standard A2UI rendering: -- **MCP Apps (`mcp-apps-component.ts`)**: Sandboxed UI widgets using the MCP protocol, communicating securely via a JSON-RPC channel. -- **Secure iFrame Web Frame (`web-frame.ts`)**: Powerful component that allows rendering raw HTML in an isolated context (used for the Office Floor Plan). -- **Org Chart (`org-chart.ts`)**: A custom tree structure visualization component. +- **MCP Apps (`mcp-apps-component.ts`)**: Sandboxed UI widgets using the MCP protocol, communicating securely via a JSON-RPC channel. +- **Secure iFrame Web Frame (`web-frame.ts`)**: Powerful component that allows rendering raw HTML in an isolated context (used for the Office Floor Plan). +- **Org Chart (`org-chart.ts`)**: A custom tree structure visualization component. ## Mix and Match A2UI Surfaces -This sample demonstrates how standard A2UI surfaces (such as contact profile cards using standard `Card` and list items) can live on the same canvas as custom extensions. -The A2UI renderer library seamlessly manages the standard component catalog, while custom components (like the Org Chart or iframe-based floor plans) hook into the same event lifecycle. You can swap between standard profile views and rich custom widgets using a unified routing layer. \ No newline at end of file +This sample demonstrates how standard A2UI surfaces (such as contact profile cards using standard `Card` and list items) can live on the same canvas as custom extensions. +The A2UI renderer library seamlessly manages the standard component catalog, while custom components (like the Org Chart or iframe-based floor plans) hook into the same event lifecycle. You can swap between standard profile views and rich custom widgets using a unified routing layer. diff --git a/samples/client/lit/custom-components-example/README_CUSTOM_COMPONENTS.md b/samples/client/lit/custom-components-example/README_CUSTOM_COMPONENTS.md index f94b77c76..5b2dc4dfd 100644 --- a/samples/client/lit/custom-components-example/README_CUSTOM_COMPONENTS.md +++ b/samples/client/lit/custom-components-example/README_CUSTOM_COMPONENTS.md @@ -9,55 +9,66 @@ This sample demonstrates a powerful pattern where the **Client** controls the ca 1. **Component Definition**: This client defines custom components (`OrgChart`, `WebFrame`) in `ui/custom-components/`. 2. **Schema Generation**: Each custom component has an associated JSON schema. 3. **Handshake**: When connecting to the agent, the client sends these schemas in the `metadata.inlineCatalog` field of the initial request. -4. **Dynamic Support**: This allows *any* A2UI agent (that supports inline catalogs) to immediately start using these components without prior knowledge. +4. **Dynamic Support**: This allows _any_ A2UI agent (that supports inline catalogs) to immediately start using these components without prior knowledge. ## Custom Components Implemented ### 1. `OrgChart` -*Located in: `ui/custom-components/org-chart.ts`* + +_Located in: `ui/custom-components/org-chart.ts`_ A visual tree illustrating the organizational hierarchy. -- **Implementation**: A standard LitElement component. -- **Interaction**: Emits `chart_node_click` events when nodes are clicked, which are sent back to the agent as A2UI Actions. + +- **Implementation**: A standard LitElement component. +- **Interaction**: Emits `chart_node_click` events when nodes are clicked, which are sent back to the agent as A2UI Actions. ### 2. `WebFrame` (Interactive Iframe) -*Located in: `ui/custom-components/web-frame.ts`* + +_Located in: `ui/custom-components/web-frame.ts`_ A tailored iframe wrapper for embedding external content or static HTML tools. -- **Use Case**: Used here to render the "Office Floor Plan" map. -- **Security**: Uses `sandbox` attributes to restrict script execution while allowing necessary interactions. -- **Bridge**: Includes a `postMessage` bridge to allow the embedded content (the map) to trigger A2UI actions in the main application. + +- **Use Case**: Used here to render the "Office Floor Plan" map. +- **Security**: Uses `sandbox` attributes to restrict script execution while allowing necessary interactions. +- **Bridge**: Includes a `postMessage` bridge to allow the embedded content (the map) to trigger A2UI actions in the main application. ## Multiple Surfaces The client is designed to render multiple A2UI "Surfaces" simultaneously. Instead of a single chat stream, the `contact.ts` shell manages: -- **Main Profile (`contact-card`)**: The primary view. -- **Side Panel (`org-chart-view`)**: A persistent side view for context. -- **Overlay (`location-surface`)**: A temporary surface for specific tasks like map viewing. +- **Main Profile (`contact-card`)**: The primary view. +- **Side Panel (`org-chart-view`)**: A persistent side view for context. +- **Overlay (`location-surface`)**: A temporary surface for specific tasks like map viewing. ## How to Run in Tandem To see this full experience, you must run this client with the specific `contact_multiple_surfaces` agent. ### 1. Start the Agent + The agent serves the backend logic and the static assets (like the floor plan HTML). + ```bash cd ../../../agent/adk/contact_multiple_surfaces uv run . ``` -*Runs on port 10004.* + +_Runs on port 10004._ ### 2. Start this Client + The client connects to the agent and renders the UI. + ```bash # In this directory (samples/client/lit/contact) npm install npm run dev ``` -*The client acts as a shell, connecting to localhost:10004 by default.* + +_The client acts as a shell, connecting to localhost:10004 by default._ ## Configuration The connection to the agent is configured in `middleware/a2a.ts`. If you need to change the agent port, update the URL in that file: + ```typescript -const agentUrl = "http://localhost:10004"; +const agentUrl = 'http://localhost:10004'; ``` diff --git a/samples/client/lit/custom-components-example/client.ts b/samples/client/lit/custom-components-example/client.ts index ccd31027d..770f20874 100644 --- a/samples/client/lit/custom-components-example/client.ts +++ b/samples/client/lit/custom-components-example/client.ts @@ -14,23 +14,21 @@ * limitations under the License. */ -import { v0_8 } from "@a2ui/lit"; -import { registerContactComponents } from "./ui/custom-components/register-components.js"; +import {v0_8} from '@a2ui/lit'; +import {registerContactComponents} from './ui/custom-components/register-components.js'; type A2TextPayload = { - kind: "text"; + kind: 'text'; text: string; }; type A2DataPayload = { - kind: "data"; + kind: 'data'; data: v0_8.Types.ServerToClientMessage; }; -type A2AServerPayload = - | Array - | { error: string }; +type A2AServerPayload = Array | {error: string}; -import { componentRegistry } from "@a2ui/lit/ui"; +import {componentRegistry} from '@a2ui/lit/ui'; export class A2UIClient { #ready: Promise = Promise.resolve(); @@ -42,51 +40,51 @@ export class A2UIClient { async send( message: v0_8.Types.A2UIClientEventMessage, - onChunk?: (messages: v0_8.Types.ServerToClientMessage[]) => void + onChunk?: (messages: v0_8.Types.ServerToClientMessage[]) => void, ): Promise { const catalog = componentRegistry.getInlineCatalog(); const finalMessage = { ...message, metadata: { - "a2uiClientCapabilities": { - "inlineCatalogs": [catalog], + a2uiClientCapabilities: { + inlineCatalogs: [catalog], }, }, }; - const response = await fetch("/a2a", { + const response = await fetch('/a2a', { body: JSON.stringify({ event: finalMessage, - contextId: this.#contextId + contextId: this.#contextId, }), - method: "POST", + method: 'POST', }); if (!response.ok) { - const error = (await response.json()) as { error: string }; + const error = (await response.json()) as {error: string}; throw new Error(error.error); } - const contentType = response.headers.get("content-type"); + const contentType = response.headers.get('content-type'); const messages: v0_8.Types.ServerToClientMessage[] = []; - if (contentType?.includes("text/event-stream")) { + if (contentType?.includes('text/event-stream')) { const reader = response.body?.getReader(); - if (!reader) throw new Error("No response body"); + if (!reader) throw new Error('No response body'); const decoder = new TextDecoder(); - let buffer = ""; + let buffer = ''; while (true) { - const { done, value } = await reader.read(); + const {done, value} = await reader.read(); if (done) break; - buffer += decoder.decode(value, { stream: true }); - const lines = buffer.split("\n\n"); - buffer = lines.pop() || ""; + buffer += decoder.decode(value, {stream: true}); + const lines = buffer.split('\n\n'); + buffer = lines.pop() || ''; for (const line of lines) { - if (line.startsWith("data: ")) { - const jsonStr = line.replace(/^data:\s*/, ""); + if (line.startsWith('data: ')) { + const jsonStr = line.replace(/^data:\s*/, ''); try { const responseData = JSON.parse(jsonStr); if (responseData.error) { @@ -103,7 +101,7 @@ export class A2UIClient { } } } catch (e) { - console.error("Error parsing SSE data:", e, jsonStr); + console.error('Error parsing SSE data:', e, jsonStr); } } } @@ -112,7 +110,7 @@ export class A2UIClient { } const responseData = (await response.json()) as any; - if (responseData && typeof responseData === 'object' && "error" in responseData) { + if (responseData && typeof responseData === 'object' && 'error' in responseData) { throw new Error(responseData.error); } else { if (responseData.contextId) { @@ -135,19 +133,21 @@ export class A2UIClient { } else { items = Array.isArray(data) ? data - : (data.kind === "message" && Array.isArray(data.parts) ? data.parts : [data]); + : data.kind === 'message' && Array.isArray(data.parts) + ? data.parts + : [data]; } const messages: v0_8.Types.ServerToClientMessage[] = []; for (const item of items) { - if (item.kind === "message" && Array.isArray(item.parts)) { + if (item.kind === 'message' && Array.isArray(item.parts)) { for (const part of item.parts) { if (part.data) { messages.push(part.data); } } } else { - if (item.kind === "text") continue; + if (item.kind === 'text') continue; if (item.data) { messages.push(item.data); } diff --git a/samples/client/lit/custom-components-example/contact.ts b/samples/client/lit/custom-components-example/contact.ts index 283592062..dacd5427a 100644 --- a/samples/client/lit/custom-components-example/contact.ts +++ b/samples/client/lit/custom-components-example/contact.ts @@ -14,50 +14,38 @@ * limitations under the License. */ -import { SignalWatcher } from "@lit-labs/signals"; -import { provide } from "@lit/context"; -import { - LitElement, - html, - css, - nothing, - HTMLTemplateResult, - unsafeCSS, -} from "lit"; -import { customElement, state } from "lit/decorators.js"; -import { theme as uiTheme } from "./theme/theme.js"; -import { A2UIClient } from "./client.js"; -import { - SnackbarAction, - SnackbarMessage, - SnackbarUUID, - SnackType, -} from "./types/types.js"; -import { type Snackbar } from "./ui/snackbar.js"; -import { repeat } from "lit/directives/repeat.js"; -import { v0_8 } from "@a2ui/lit"; -import * as UI from "@a2ui/lit/ui"; +import {SignalWatcher} from '@lit-labs/signals'; +import {provide} from '@lit/context'; +import {LitElement, html, css, nothing, HTMLTemplateResult, unsafeCSS} from 'lit'; +import {customElement, state} from 'lit/decorators.js'; +import {theme as uiTheme} from './theme/theme.js'; +import {A2UIClient} from './client.js'; +import {SnackbarAction, SnackbarMessage, SnackbarUUID, SnackType} from './types/types.js'; +import {type Snackbar} from './ui/snackbar.js'; +import {repeat} from 'lit/directives/repeat.js'; +import {v0_8} from '@a2ui/lit'; +import * as UI from '@a2ui/lit/ui'; // Demo elements. -import "./ui/ui.js"; -import { registerContactComponents } from "./ui/custom-components/register-components.js"; -import { Context } from "@a2ui/lit/ui"; +import './ui/ui.js'; +import {registerContactComponents} from './ui/custom-components/register-components.js'; +import {Context} from '@a2ui/lit/ui'; // @ts-ignore -import { renderMarkdown } from "@a2ui/markdown-it"; +import {renderMarkdown} from '@a2ui/markdown-it'; // Register custom components for the contact app registerContactComponents(); -@customElement("a2ui-contact") +@customElement('a2ui-contact') export class A2UIContactFinder extends SignalWatcher(LitElement) { connectedCallback() { super.connectedCallback(); } - @provide({ context: UI.Context.themeContext }) + @provide({context: UI.Context.themeContext}) accessor theme: v0_8.Types.Theme = uiTheme; - @provide({ context: UI.Context.markdown }) + @provide({context: UI.Context.markdown}) accessor markdownRenderer: v0_8.Types.MarkdownRenderer = async (text, options) => { return renderMarkdown(text, options); }; @@ -232,11 +220,7 @@ export class A2UIContactFinder extends SignalWatcher(LitElement) { }> = []; render() { - return [ - this.#maybeRenderForm(), - this.#maybeRenderData(), - this.#maybeRenderError(), - ]; + return [this.#maybeRenderForm(), this.#maybeRenderData(), this.#maybeRenderError()]; } #maybeRenderError() { @@ -255,7 +239,7 @@ export class A2UIContactFinder extends SignalWatcher(LitElement) { return; } const data = new FormData(evt.target); - const body = data.get("body") ?? null; + const body = data.get('body') ?? null; if (!body) { return; } @@ -265,9 +249,7 @@ export class A2UIContactFinder extends SignalWatcher(LitElement) { await this.#sendAndProcessMessage(message); }} > -

- Contact Finder -

+

Contact Finder

progress_activity Rendering UI... @@ -318,68 +298,62 @@ export class A2UIContactFinder extends SignalWatcher(LitElement) { surfaces, ([surfaceId]) => surfaceId, ([surfaceId, surface]) => { - return html` -
- - - ) => { - const [target] = evt.composedPath(); - if (!(target instanceof HTMLElement)) { - return; - } - - const context: v0_8.Types.A2UIClientEventMessage["userAction"]["context"] = - {}; - if (evt.detail.action.context) { - const srcContext = evt.detail.action.context; - for (const item of srcContext) { - if (item.value.literalBoolean) { - context[item.key] = item.value.literalBoolean; - } else if (item.value.literalNumber) { - context[item.key] = item.value.literalNumber; - } else if (item.value.literalString) { - context[item.key] = item.value.literalString; - } else if (item.value.path) { - const path = this.#processor.resolvePath( - item.value.path, - evt.detail.dataContextPath - ); - const value = this.#processor.getData( - evt.detail.sourceComponent, - path, - surfaceId - ); - context[item.key] = value; - } + return html`
+ ) => { + const [target] = evt.composedPath(); + if (!(target instanceof HTMLElement)) { + return; } - } - - const message: v0_8.Types.A2UIClientEventMessage = { - userAction: { - surfaceId: surfaceId, - name: "ACTION: " + evt.detail.action.name, - sourceComponentId: target.id, - timestamp: new Date().toISOString(), - context, - }, - }; - + const context: v0_8.Types.A2UIClientEventMessage['userAction']['context'] = {}; + if (evt.detail.action.context) { + const srcContext = evt.detail.action.context; + for (const item of srcContext) { + if (item.value.literalBoolean) { + context[item.key] = item.value.literalBoolean; + } else if (item.value.literalNumber) { + context[item.key] = item.value.literalNumber; + } else if (item.value.literalString) { + context[item.key] = item.value.literalString; + } else if (item.value.path) { + const path = this.#processor.resolvePath( + item.value.path, + evt.detail.dataContextPath, + ); + const value = this.#processor.getData( + evt.detail.sourceComponent, + path, + surfaceId, + ); + context[item.key] = value; + } + } + } - await this.#sendAndProcessMessage(message); - }} + const message: v0_8.Types.A2UIClientEventMessage = { + userAction: { + surfaceId: surfaceId, + name: 'ACTION: ' + evt.detail.action.name, + sourceComponentId: target.id, + timestamp: new Date().toISOString(), + context, + }, + }; + + await this.#sendAndProcessMessage(message); + }} .surfaceId=${surfaceId} - .processor=${this.#processor} .enableCustomElements=${true} >
`; - } + }, )} - `; + `; } async #sendAndProcessMessage(request: v0_8.Types.A2UIClientEventMessage) { @@ -398,11 +372,11 @@ export class A2UIContactFinder extends SignalWatcher(LitElement) { } async #sendMessage( - message: v0_8.Types.A2UIClientEventMessage + message: v0_8.Types.A2UIClientEventMessage, ): Promise { try { this.#requesting = true; - const response = await this.#a2uiClient.send(message, (chunkMessages) => { + const response = await this.#a2uiClient.send(message, chunkMessages => { this.#processor.processMessages(chunkMessages); this.renderVersion++; this.requestUpdate(); @@ -426,7 +400,7 @@ export class A2UIContactFinder extends SignalWatcher(LitElement) { actions: SnackbarAction[] = [], persistent = false, id = globalThis.crypto.randomUUID(), - replaceAll = false + replaceAll = false, ) { if (!this.#snackbar) { this.#pendingSnackbarMessages.push({ @@ -450,7 +424,7 @@ export class A2UIContactFinder extends SignalWatcher(LitElement) { persistent, actions, }, - replaceAll + replaceAll, ); } diff --git a/samples/client/lit/custom-components-example/events/events.ts b/samples/client/lit/custom-components-example/events/events.ts index 2b7811ab9..95ec43396 100644 --- a/samples/client/lit/custom-components-example/events/events.ts +++ b/samples/client/lit/custom-components-example/events/events.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { HTMLTemplateResult } from "lit"; +import {HTMLTemplateResult} from 'lit'; const eventInit = { bubbles: true, @@ -23,13 +23,13 @@ const eventInit = { }; export class SnackbarActionEvent extends Event { - static eventName = "snackbaraction"; + static eventName = 'snackbaraction'; constructor( public readonly action: string, public readonly value?: HTMLTemplateResult | string, - public readonly callback?: () => void + public readonly callback?: () => void, ) { - super(SnackbarActionEvent.eventName, { ...eventInit }); + super(SnackbarActionEvent.eventName, {...eventInit}); } } diff --git a/samples/client/lit/custom-components-example/index.html b/samples/client/lit/custom-components-example/index.html index a69208e02..c907d7169 100644 --- a/samples/client/lit/custom-components-example/index.html +++ b/samples/client/lit/custom-components-example/index.html @@ -1,4 +1,4 @@ - + - + + + + + A2UI Org Chart Test (Contact Sample) + + + - // Register custom components - registerContactComponents(); - console.log('Custom components registered in Contact Sample'); - - + +

A2UI Org Chart Test (Contact Sample)

- -

A2UI Org Chart Test (Contact Sample)

+
+ +
-
- -
+ - - - \ No newline at end of file + + + diff --git a/samples/client/lit/custom-components-example/ui/custom-components/test/override-test.html b/samples/client/lit/custom-components-example/ui/custom-components/test/override-test.html index 7edf0291c..67feacf2f 100644 --- a/samples/client/lit/custom-components-example/ui/custom-components/test/override-test.html +++ b/samples/client/lit/custom-components-example/ui/custom-components/test/override-test.html @@ -14,30 +14,28 @@ limitations under the License. --> - + - - - - A2UI Component Override Test - - - - - -

Component Override Test

-
- - - \ No newline at end of file + + + A2UI Component Override Test + + + + + +

Component Override Test

+
+ + diff --git a/samples/client/lit/custom-components-example/ui/custom-components/test/override-test.ts b/samples/client/lit/custom-components-example/ui/custom-components/test/override-test.ts index b1a1bd06b..4f2de69a7 100644 --- a/samples/client/lit/custom-components-example/ui/custom-components/test/override-test.ts +++ b/samples/client/lit/custom-components-example/ui/custom-components/test/override-test.ts @@ -14,27 +14,27 @@ * limitations under the License. */ -import { componentRegistry, Root } from "@a2ui/lit/ui"; -import { html, css } from "lit"; -import { property } from "lit/decorators.js"; +import {componentRegistry, Root} from '@a2ui/lit/ui'; +import {html, css} from 'lit'; +import {property} from 'lit/decorators.js'; // 1. Define the override -import { PremiumTextField } from "../premium-text-field.js"; +import {PremiumTextField} from '../premium-text-field.js'; // 2. Register it as "TextField" -componentRegistry.register("TextField", PremiumTextField, "premium-text-field"); -console.log("Registered PremiumTextField override"); +componentRegistry.register('TextField', PremiumTextField, 'premium-text-field'); +console.log('Registered PremiumTextField override'); // 3. Render a standard TextField component node -const container = document.getElementById("app"); +const container = document.getElementById('app'); if (container) { - const root = document.createElement("a2ui-root") as Root; + const root = document.createElement('a2ui-root') as Root; const textFieldComponent = { - type: "TextField", - id: "tf-1", + type: 'TextField', + id: 'tf-1', properties: { - label: "Enter your name", - text: "John Doe", + label: 'Enter your name', + text: 'John Doe', }, }; diff --git a/samples/client/lit/custom-components-example/ui/custom-components/web-frame.ts b/samples/client/lit/custom-components-example/ui/custom-components/web-frame.ts index 6eee51cf7..d5bde665d 100644 --- a/samples/client/lit/custom-components-example/ui/custom-components/web-frame.ts +++ b/samples/client/lit/custom-components-example/ui/custom-components/web-frame.ts @@ -14,18 +14,17 @@ * limitations under the License. */ -import { html, css, PropertyValues, nothing } from "lit"; -import { customElement, property, query } from "lit/decorators.js"; -import { ifDefined } from "lit/directives/if-defined.js"; -import { Root } from "@a2ui/lit/ui"; -import { v0_8 } from "@a2ui/lit"; - +import {html, css, PropertyValues, nothing} from 'lit'; +import {customElement, property, query} from 'lit/decorators.js'; +import {ifDefined} from 'lit/directives/if-defined.js'; +import {Root} from '@a2ui/lit/ui'; +import {v0_8} from '@a2ui/lit'; interface WebFrameConfig { [key: string]: unknown; } -@customElement("a2ui-web-frame") +@customElement('a2ui-web-frame') export class WebFrame extends Root { static override styles = [ ...Root.styles, @@ -62,7 +61,7 @@ export class WebFrame extends Root { display: flex; align-items: center; justify-content: center; - box-shadow: 0 2px 4px rgba(0,0,0,0.1); + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); } .controls button:hover { background: #f0f0f0; @@ -72,33 +71,33 @@ export class WebFrame extends Root { /* --- Properties (Server Contract) --- */ - @property({ type: String }) - accessor url: string = ""; + @property({type: String}) + accessor url: string = ''; - @property({ type: String }) - accessor html: string = ""; + @property({type: String}) + accessor html: string = ''; - @property({ type: Number }) + @property({type: Number}) accessor height: number | undefined = undefined; - @property({ type: String }) - accessor interactionMode: "readOnly" | "interactive" = "readOnly"; + @property({type: String}) + accessor interactionMode: 'readOnly' | 'interactive' = 'readOnly'; - @property({ type: Array }) + @property({type: Array}) accessor allowedEvents: string[] = []; // --- Internal State --- - @query("iframe") + @query('iframe') accessor iframe!: HTMLIFrameElement; // --- Security Constants --- static readonly TRUSTED_DOMAINS = [ - "localhost", - "127.0.0.1", - "openstreetmap.org", - "youtube.com", - "maps.google.com" + 'localhost', + '127.0.0.1', + 'openstreetmap.org', + 'youtube.com', + 'maps.google.com', ]; override render() { @@ -131,9 +130,9 @@ export class WebFrame extends Root { // 1. If HTML is provided, it's treated as Trusted (but isolated) if (this.html) { if (this.interactionMode === 'interactive') { - return "allow-scripts allow-forms allow-popups allow-modals"; + return 'allow-scripts allow-forms allow-popups allow-modals'; } - return "allow-scripts"; // ReadOnly but scripts allowed for rendering + return 'allow-scripts'; // ReadOnly but scripts allowed for rendering } // 2. Parse Domain from URL @@ -141,35 +140,36 @@ export class WebFrame extends Root { const urlObj = new URL(this.url, window.location.href); // Handle relative URLs too const hostname = urlObj.hostname; - const isTrusted = WebFrame.TRUSTED_DOMAINS.some(d => hostname === d || hostname.endsWith(`.${d}`)); + const isTrusted = WebFrame.TRUSTED_DOMAINS.some( + d => hostname === d || hostname.endsWith(`.${d}`), + ); if (!isTrusted) { // Untrusted: Strict Lockdown - return ""; + return ''; } // Trusted // Always allow same-origin for trusted domains to avoid issues with local assets or CORS checks if (this.interactionMode === 'interactive') { - return "allow-scripts allow-forms allow-popups allow-modals allow-same-origin"; + return 'allow-scripts allow-forms allow-popups allow-modals allow-same-origin'; } else { - return "allow-scripts allow-same-origin"; + return 'allow-scripts allow-same-origin'; } - } catch (e) { // Invalid URL -> Lockdown - return ""; + return ''; } } // --- Event Bridge --- firstUpdated() { - window.addEventListener("message", this.#onMessage); + window.addEventListener('message', this.#onMessage); } disconnectedCallback() { - window.removeEventListener("message", this.#onMessage); + window.removeEventListener('message', this.#onMessage); super.disconnectedCallback(); } @@ -179,14 +179,17 @@ export class WebFrame extends Root { // Spec Protocol: { type: 'a2ui_action', action: '...', data: ... } if (data && data.type === 'a2ui_action') { - const { action, data: actionData } = data; // 'data' property in message payload + const {action, data: actionData} = data; // 'data' property in message payload // 1. Validate Action if (this.allowedEvents.includes(action)) { // 2. Dispatch this.#dispatchAgentAction(action, actionData); } else { - console.warn(`[WebFrame] Action '${action}' blocked. Not in allowedEvents:`, this.allowedEvents); + console.warn( + `[WebFrame] Action '${action}' blocked. Not in allowedEvents:`, + this.allowedEvents, + ); } } // Legacy support for 'emit' temporarily if we want to be safe, but spec implies replacement. @@ -194,15 +197,15 @@ export class WebFrame extends Root { }; #dispatchAgentAction(actionName: string, params: any) { - const context: v0_8.Types.Action["context"] = []; + const context: v0_8.Types.Action['context'] = []; if (params && typeof params === 'object') { for (const [key, value] of Object.entries(params)) { - if (typeof value === "string") { - context.push({ key, value: { literalString: value } }); - } else if (typeof value === "number") { - context.push({ key, value: { literalNumber: value } }); - } else if (typeof value === "boolean") { - context.push({ key, value: { literalBoolean: value } }); + if (typeof value === 'string') { + context.push({key, value: {literalString: value}}); + } else if (typeof value === 'number') { + context.push({key, value: {literalNumber: value}}); + } else if (typeof value === 'boolean') { + context.push({key, value: {literalBoolean: value}}); } } } @@ -212,8 +215,8 @@ export class WebFrame extends Root { context, }; - const eventPayload: v0_8.Events.StateEventDetailMap["a2ui.action"] = { - eventType: "a2ui.action", + const eventPayload: v0_8.Events.StateEventDetailMap['a2ui.action'] = { + eventType: 'a2ui.action', action, sourceComponentId: this.id, dataContextPath: this.dataContextPath, @@ -228,7 +231,7 @@ export class WebFrame extends Root { // We assume the iframe content knows how to handle 'zoom' message if it supports it. #zoom(factor: number) { if (this.iframe && this.iframe.contentWindow) { - this.iframe.contentWindow.postMessage({ type: 'zoom', payload: { factor } }, '*'); + this.iframe.contentWindow.postMessage({type: 'zoom', payload: {factor}}, '*'); } } } diff --git a/samples/client/lit/custom-components-example/ui/shared-constants.ts b/samples/client/lit/custom-components-example/ui/shared-constants.ts index 2213f62d6..e9ef90f04 100644 --- a/samples/client/lit/custom-components-example/ui/shared-constants.ts +++ b/samples/client/lit/custom-components-example/ui/shared-constants.ts @@ -14,6 +14,6 @@ * limitations under the License. */ -export const SANDBOX_BASE_PATH = "shared/mcp_apps_inner_iframe/"; +export const SANDBOX_BASE_PATH = 'shared/mcp_apps_inner_iframe/'; export const SANDBOX_ENTRY_NAME = `${SANDBOX_BASE_PATH}sandbox`; export const SANDBOX_IFRAME_PATH = `/${SANDBOX_ENTRY_NAME}.html`; diff --git a/samples/client/lit/custom-components-example/ui/snackbar.ts b/samples/client/lit/custom-components-example/ui/snackbar.ts index 03ce8ab4a..68bef22b1 100644 --- a/samples/client/lit/custom-components-example/ui/snackbar.ts +++ b/samples/client/lit/custom-components-example/ui/snackbar.ts @@ -13,22 +13,22 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { LitElement, html, css, nothing, unsafeCSS } from "lit"; -import { customElement, property } from "lit/decorators.js"; -import { SnackbarMessage, SnackbarUUID, SnackType } from "../types/types"; -import { repeat } from "lit/directives/repeat.js"; -import { SnackbarActionEvent } from "../events/events"; -import { classMap } from "lit/directives/class-map.js"; -import { v0_8 } from "@a2ui/lit"; +import {LitElement, html, css, nothing, unsafeCSS} from 'lit'; +import {customElement, property} from 'lit/decorators.js'; +import {SnackbarMessage, SnackbarUUID, SnackType} from '../types/types'; +import {repeat} from 'lit/directives/repeat.js'; +import {SnackbarActionEvent} from '../events/events'; +import {classMap} from 'lit/directives/class-map.js'; +import {v0_8} from '@a2ui/lit'; const DEFAULT_TIMEOUT = 8000; -@customElement("ui-snackbar") +@customElement('ui-snackbar') export class Snackbar extends LitElement { - @property({ reflect: true, type: Boolean }) + @property({reflect: true, type: Boolean}) accessor active = false; - @property({ reflect: true, type: Boolean }) + @property({reflect: true, type: Boolean}) accessor error = false; @property() @@ -61,8 +61,7 @@ export class Snackbar extends LitElement { z-index: 1800; scrollbar-width: none; overflow-x: scroll; - font: 400 var(--bb-body-medium) / var(--bb-body-line-height-medium) - var(--bb-font-family); + font: 400 var(--bb-body-medium) / var(--bb-body-line-height-medium) var(--bb-font-family); } :host([active]) { @@ -109,8 +108,7 @@ export class Snackbar extends LitElement { margin-right: var(--bb-grid-size-3); & button { - font: 500 var(--bb-body-medium) / var(--bb-body-line-height-medium) - var(--bb-font-family); + font: 500 var(--bb-body-medium) / var(--bb-body-line-height-medium) var(--bb-font-family); padding: 0; background: transparent; border: none; @@ -168,9 +166,7 @@ export class Snackbar extends LitElement { ]; show(message: SnackbarMessage, replaceAll = false) { - const existingMessage = this.#messages.findIndex( - (msg) => msg.id === message.id - ); + const existingMessage = this.#messages.findIndex(msg => msg.id === message.id); if (existingMessage === -1) { if (replaceAll) { this.#messages.length = 0; @@ -182,13 +178,13 @@ export class Snackbar extends LitElement { } window.clearTimeout(this.#timeout); - if (!this.#messages.every((msg) => msg.persistent)) { + if (!this.#messages.every(msg => msg.persistent)) { this.#timeout = window.setTimeout(() => { this.hide(); }, this.timeout); } - this.error = this.#messages.some((msg) => msg.type === SnackType.ERROR); + this.error = this.#messages.some(msg => msg.type === SnackType.ERROR); this.active = true; this.requestUpdate(); @@ -197,7 +193,7 @@ export class Snackbar extends LitElement { hide(id?: SnackbarUUID) { if (id) { - const idx = this.#messages.findIndex((msg) => msg.id === id); + const idx = this.#messages.findIndex(msg => msg.id === id); if (idx !== -1) { this.#messages.splice(idx, 1); } @@ -206,7 +202,7 @@ export class Snackbar extends LitElement { } this.active = this.#messages.length !== 0; - this.updateComplete.then((avoidedUpdate) => { + this.updateComplete.then(avoidedUpdate => { if (!avoidedUpdate) { return; } @@ -217,81 +213,74 @@ export class Snackbar extends LitElement { render() { let rotate = false; - let icon = ""; + let icon = ''; for (let i = this.#messages.length - 1; i >= 0; i--) { - if ( - !this.#messages[i].type || - this.#messages[i].type === SnackType.NONE - ) { + if (!this.#messages[i].type || this.#messages[i].type === SnackType.NONE) { continue; } icon = this.#messages[i].type; if (this.#messages[i].type === SnackType.PENDING) { - icon = "progress_activity"; + icon = 'progress_activity'; rotate = true; } break; } return html` ${icon - ? html`${icon}` - : nothing} + : nothing}
${repeat( - this.#messages, - (message) => message.id, - (message) => { - return html`
${message.message}
`; - } - )} + this.#messages, + message => message.id, + message => { + return html`
${message.message}
`; + }, + )}
${repeat( - this.#messages, - (message) => message.id, - (message) => { - if (!message.actions) { - return nothing; - } + this.#messages, + message => message.id, + message => { + if (!message.actions) { + return nothing; + } - return html`${repeat( - message.actions, - (action) => action.value, - (action) => { - return html``; - } - )}`; - } - )} + }, + )}`; + }, + )}
`; diff --git a/samples/client/lit/custom-components-example/ui/ui.ts b/samples/client/lit/custom-components-example/ui/ui.ts index 0a06940f1..2f86cdd65 100644 --- a/samples/client/lit/custom-components-example/ui/ui.ts +++ b/samples/client/lit/custom-components-example/ui/ui.ts @@ -14,4 +14,4 @@ * limitations under the License. */ -export { Snackbar } from "./snackbar"; +export {Snackbar} from './snackbar'; diff --git a/samples/client/lit/custom-components-example/vite.config.ts b/samples/client/lit/custom-components-example/vite.config.ts index 001035e06..4fa6e79d6 100644 --- a/samples/client/lit/custom-components-example/vite.config.ts +++ b/samples/client/lit/custom-components-example/vite.config.ts @@ -14,12 +14,12 @@ * limitations under the License. */ -import { config } from "dotenv"; -import { UserConfig } from "vite"; -import * as Middleware from "./middleware"; -import { dirname, resolve } from "node:path"; -import { fileURLToPath } from "node:url"; -import { SANDBOX_ENTRY_NAME, SANDBOX_BASE_PATH, SANDBOX_IFRAME_PATH } from "./ui/shared-constants.js"; +import {config} from 'dotenv'; +import {UserConfig} from 'vite'; +import * as Middleware from './middleware'; +import {dirname, resolve} from 'node:path'; +import {fileURLToPath} from 'node:url'; +import {SANDBOX_ENTRY_NAME, SANDBOX_BASE_PATH, SANDBOX_IFRAME_PATH} from './ui/shared-constants.js'; const __dirname = dirname(fileURLToPath(import.meta.url)); @@ -27,7 +27,7 @@ export default async () => { config(); const entry: Record = { - contact: resolve(__dirname, "index.html"), + contact: resolve(__dirname, 'index.html'), [SANDBOX_ENTRY_NAME]: resolve(__dirname, `../..${SANDBOX_IFRAME_PATH}`), }; @@ -41,44 +41,47 @@ export default async () => { if (req.url?.startsWith(`/${SANDBOX_BASE_PATH}`)) { let targetPath = req.url.slice(1); // Normalize .js requests from HTML back to source .ts files for Vite bundling - if (targetPath.endsWith(".js")) { - targetPath = targetPath.slice(0, -3) + ".ts"; + if (targetPath.endsWith('.js')) { + targetPath = targetPath.slice(0, -3) + '.ts'; } req.url = '/@fs' + resolve(__dirname, '../../' + targetPath); } next(); }); - } - } + }, + }, ], build: { rollupOptions: { input: entry, }, - target: "es2021", + target: 'es2021', }, define: {}, resolve: { - dedupe: ["lit"], + dedupe: ['lit'], alias: { - "@a2ui/markdown-it": resolve(__dirname, "../../../../renderers/markdown/markdown-it/dist/src/markdown.js"), - "sandbox.js": resolve(__dirname, "../../" + SANDBOX_ENTRY_NAME + ".ts"), - "@modelcontextprotocol/ext-apps/app-bridge": resolve(__dirname, "../node_modules/@modelcontextprotocol/ext-apps/dist/src/app-bridge.js"), - } + '@a2ui/markdown-it': resolve( + __dirname, + '../../../../renderers/markdown/markdown-it/dist/src/markdown.js', + ), + 'sandbox.js': resolve(__dirname, '../../' + SANDBOX_ENTRY_NAME + '.ts'), + '@modelcontextprotocol/ext-apps/app-bridge': resolve( + __dirname, + '../node_modules/@modelcontextprotocol/ext-apps/dist/src/app-bridge.js', + ), + }, }, optimizeDeps: { esbuildOptions: { - target: "es2021", - } + target: 'es2021', + }, }, server: { host: true, // Listen on all network interfaces (0.0.0.0), enabling both localhost and 127.0.0.1 simultaneously fs: { - allow: [ - "../../", - "./", - ] - } + allow: ['../../', './'], + }, }, } satisfies UserConfig; }; diff --git a/samples/client/lit/mcp-apps-in-a2ui-sample/README.md b/samples/client/lit/mcp-apps-in-a2ui-sample/README.md index 4dbd6071f..34f5c43c3 100644 --- a/samples/client/lit/mcp-apps-in-a2ui-sample/README.md +++ b/samples/client/lit/mcp-apps-in-a2ui-sample/README.md @@ -16,23 +16,30 @@ This sample demonstrates how a sandboxed MCP (Model Context Protocol) applicatio ## Rationale for Implementation Pattern This sample follows the pattern established in the `custom-components-example` (specifically the floor plan map): + - It uses **raw postMessage** communication in the iframe instead of loading external scripts like `AppBridge` from `unpkg.com`. This makes it robust against network issues and CSP restrictions in the sandbox. - It relies on the A2UI host to act as the orchestrator, translating MCP tool calls into A2UI actions. ## How to Run ### 1. Run the Agent + Navigate to the agent sample directory and run the agent: + ```bash cd samples/agent/adk/mcp-apps-in-a2ui-sample uv run agent.py ``` + The agent will start on `http://localhost:8000`. ### 2. Run the Client + Navigate to the client sample directory and start the dev server: + ```bash cd samples/client/lit/mcp-apps-in-a2ui-sample npm run dev ``` + The client will start (typically on `http://localhost:5173/`). Open this URL in your browser to interact with the sample. diff --git a/samples/client/lit/mcp-apps-in-a2ui-sample/client.ts b/samples/client/lit/mcp-apps-in-a2ui-sample/client.ts index 1c2312cde..bdf42554b 100644 --- a/samples/client/lit/mcp-apps-in-a2ui-sample/client.ts +++ b/samples/client/lit/mcp-apps-in-a2ui-sample/client.ts @@ -14,23 +14,21 @@ * limitations under the License. */ -import { v0_8 } from "@a2ui/lit"; -import { registerMcpComponents } from "./ui/custom-components/register-components.js"; -import { componentRegistry } from "@a2ui/lit/ui"; +import {v0_8} from '@a2ui/lit'; +import {registerMcpComponents} from './ui/custom-components/register-components.js'; +import {componentRegistry} from '@a2ui/lit/ui'; type A2TextPayload = { - kind: "text"; + kind: 'text'; text: string; }; type A2DataPayload = { - kind: "data"; + kind: 'data'; data: v0_8.Types.ServerToClientMessage; }; -type A2AServerPayload = - | Array - | { error: string }; +type A2AServerPayload = Array | {error: string}; export class A2UIClient { #ready: Promise = Promise.resolve(); @@ -39,33 +37,33 @@ export class A2UIClient { } async send( - message: v0_8.Types.A2UIClientEventMessage + message: v0_8.Types.A2UIClientEventMessage, ): Promise { const catalog = componentRegistry.getInlineCatalog(); const finalMessage = { ...message, metadata: { - "a2uiClientCapabilities": { - "inlineCatalogs": [catalog], + a2uiClientCapabilities: { + inlineCatalogs: [catalog], }, }, }; - const response = await fetch("/a2a", { + const response = await fetch('/a2a', { body: JSON.stringify(finalMessage), - method: "POST", + method: 'POST', }); if (response.ok) { const data = (await response.json()) as A2AServerPayload; const messages: v0_8.Types.ServerToClientMessage[] = []; - if ("error" in data) { + if ('error' in data) { throw new Error(data.error); } else { for (const item of data) { if (typeof item === 'object' && item !== null && 'kind' in item) { - if (item.kind === "text") continue; - if (item.kind === "data") { + if (item.kind === 'text') continue; + if (item.kind === 'data') { messages.push(item.data); } } else { diff --git a/samples/client/lit/mcp-apps-in-a2ui-sample/index.html b/samples/client/lit/mcp-apps-in-a2ui-sample/index.html index 461c59b7e..37313cbe4 100644 --- a/samples/client/lit/mcp-apps-in-a2ui-sample/index.html +++ b/samples/client/lit/mcp-apps-in-a2ui-sample/index.html @@ -1,4 +1,4 @@ - + - - - - How A2UI Works - Message Flow - - - - - -
-
- -
-

How A2UI Works

-
Message Flow Between Client, Server, and Remote Agent
-
-
- - arrow_back - Back to Demo - -
- -
-

The A2UI Message Flow

-

This page shows a representative message sequence from when a user asks for flashcards about ATP.

-

A2UI enables agents to generate rich UI components as declarative JSON data, not executable code. The client application renders these using its own native components.

- -
-
Browser Client
- -
Local API Server
(api-server.ts)
- -
Remote Agent
(Vertex AI Agent Engine)
- -
Local API Server
- -
Browser Renders
-
-
+ + + + How A2UI Works - Message Flow + + + + + +
+
+ +
+

How A2UI Works

+
Message Flow Between Client, Server, and Remote Agent
+
-
+ +
+

The A2UI Message Flow

+

+ This page shows a representative message sequence from when a user asks for + flashcards about ATP. +

+

+ A2UI enables agents to generate rich UI components as + declarative JSON data, not executable code. The client application renders + these using its own native components. +

+ +
+
Browser Client
+ +
+ Local API Server
(api-server.ts)
-
-
- Request Payload - JSON -
-
+ +
+ Remote Agent
(Vertex AI Agent Engine)
+ +
Local API Server
+ +
Browser Renders
- -
-
-
2
- Server → Client - /api/chat (Intent Classification) -
-
-
- Gemini classifies the intent as "flashcards". This tells the orchestrator to fetch A2UI content from the specialized agent. +
+ +
+
+
1
+ Client → Server + /api/chat (Intent Detection)
-
-
- Response - JSON +
+
+ User types "Create flashcards for bond energy misconceptions". The + orchestrator first sends this to Gemini to detect the user's intent. +
+
+
+ Request Payload + JSON +
+
-
-
- -
-
-
3
- Client → Server - /a2ui-agent/a2a/query -
-
-
- The browser requests A2UI content generation. It specifies format: flashcards and includes context about what the user asked for. + +
+
+
2
+ Server → Client + /api/chat (Intent Classification)
-
-
- Request Payload - JSON +
+
+ Gemini classifies the intent as "flashcards". This tells the + orchestrator to fetch A2UI content from the specialized agent. +
+
+
+ Response + JSON +
+
-
-
- -
-
-
4
- Server → Remote Agent - Deployed to Vertex AI Agent Engine -
-
-
- The server forwards the request to a remote A2UI-generating agent deployed to Vertex AI Agent Engine. This agent is a separate service that specializes in generating A2UI content. + +
+
+
3
+ Client → Server + /a2ui-agent/a2a/query
-
-
- Request to Remote Agent (via Agent Engine API) - JSON +
+
+ The browser requests A2UI content generation. It specifies + format: flashcards and includes context about what the user asked for. +
+
+
+ Request Payload + JSON +
+
-
-
-
- Key Point: This is the A2A (Agent-to-Agent) pattern described in the blog. The orchestrator delegates UI generation to a specialized remote agent deployed on Vertex AI Agent Engine - demonstrating cross-boundary agent collaboration.
-
- -
-
-
5
- Remote Agent → Server - Response from Vertex AI Agent Engine -
-
-
- The remote agent (deployed on Agent Engine) returns A2UI JSON - a declarative description of UI components. This is the core of what A2UI provides. + +
+
+
4
+ Server → Remote Agent + Deployed to Vertex AI Agent Engine
-
-
- A2UI Messages (Parsed) - JSON • Scroll to see full payload +
+
+ The server forwards the request to a + remote A2UI-generating agent deployed to Vertex AI Agent Engine. This + agent is a separate service that specializes in generating A2UI content. +
+
+
+ Request to Remote Agent (via Agent Engine API) + JSON +
+
+
+
+ Key Point: This is the A2A (Agent-to-Agent) pattern described in the + blog. The orchestrator delegates UI generation to a specialized remote agent deployed on + Vertex AI Agent Engine - demonstrating cross-boundary agent collaboration.
-
-
-
-

Understanding the A2UI Structure

-

beginRendering - Declares the surface and root component ID
- surfaceUpdate - Contains the flat list of components with ID references
- Components - Text, Column, Row, Flashcard - these are catalog components the client knows how to render

-
- -
-
-
6
- Server → Client - /a2ui-agent/a2a/query -
-
-
- The server sends the A2UI payload to the browser. The @a2ui/web-lib renderer processes this JSON and creates native web components. + +
+
+
5
+ Remote Agent → Server + Response from Vertex AI Agent Engine
-
-
- Final Response to Browser - JSON +
+
+ The remote agent (deployed on Agent Engine) returns A2UI JSON - a + declarative description of UI components. This is the core of what A2UI provides. +
+
+
+ A2UI Messages (Parsed) + JSON • Scroll to see full payload +
+
+
+
+

Understanding the A2UI Structure

+

+ beginRendering - Declares the surface and root component ID
+ surfaceUpdate - Contains the flat list of components with ID + references
+ Components - Text, Column, Row, Flashcard - these are catalog + components the client knows how to render +

-
-
- Result: The browser's A2UI renderer creates <a2ui-surface>, which contains <a2ui-flashcard> components styled to match the host application's theme. +
+ + +
+
+
6
+ Server → Client + /a2ui-agent/a2a/query +
+
+
+ The server sends the A2UI payload to the browser. The + @a2ui/web-lib renderer processes this JSON and creates native web + components. +
+
+
+ Final Response to Browser + JSON +
+
+
+
+ Result: The browser's A2UI renderer creates <a2ui-surface>, which + contains <a2ui-flashcard> components styled to match the host application's theme. +
-
- - - + msg5: [ + { + beginRendering: { + surfaceId: 'learningContent', + root: 'mainColumn', + }, + }, + { + surfaceUpdate: { + surfaceId: 'learningContent', + components: [ + { + id: 'mainColumn', + component: { + Column: { + children: {explicitList: ['headerText', 'flashcardRow']}, + distribution: 'start', + alignment: 'stretch', + }, + }, + }, + { + id: 'headerText', + component: { + Text: { + text: {literalString: 'Study Flashcards: ATP & Bond Energy'}, + usageHint: 'h3', + }, + }, + }, + { + id: 'flashcardRow', + component: { + Row: { + children: {explicitList: ['card1', 'card2', 'card3']}, + distribution: 'start', + alignment: 'stretch', + }, + }, + }, + { + id: 'card1', + component: { + Flashcard: { + front: {literalString: 'Why does ATP hydrolysis release energy?'}, + back: { + literalString: + 'ATP hydrolysis releases energy because the products (ADP + Pi) are MORE STABLE than ATP. The phosphate groups in ATP repel each other due to negative charges. When the terminal phosphate is removed, this electrostatic strain is relieved, and the products achieve better resonance stabilization.', + }, + category: {literalString: 'Biochemistry'}, + }, + }, + }, + { + id: 'card2', + component: { + Flashcard: { + front: {literalString: 'Does breaking a chemical bond release energy?'}, + back: { + literalString: + "NO - this is a common MCAT trap! Breaking ANY bond REQUIRES energy input (it's endothermic). Energy is only released when NEW bonds FORM. In ATP hydrolysis, the energy released comes from forming more stable bonds in the products.", + }, + category: {literalString: 'Common Trap'}, + }, + }, + }, + { + id: 'card3', + component: { + Flashcard: { + front: { + literalString: "What's wrong with saying 'ATP stores energy in its bonds'?", + }, + back: { + literalString: + "Bonds don't 'store' energy like batteries. Think of it like holding a plank position at the gym - you're not storing energy, you're in a high-energy unstable state. When you release to rest (ATP → ADP + Pi), you move to a more stable, lower-energy state.", + }, + category: {literalString: 'MCAT Concept'}, + }, + }, + }, + ], + }, + }, + ], + msg6: { + format: 'flashcards', + surfaceId: 'learningContent', + source: { + url: 'https://openstax.org/books/biology-ap-courses/pages/6-2-potential-kinetic-free-and-activation-energy', + title: 'OpenStax Biology for AP Courses', + provider: 'OpenStax', + }, + a2uiMessageCount: 2, + a2uiPayload: '[ ... see Message 5 for full A2UI content ... ]', + }, + }; + + // Simple JSON syntax highlighter + function highlightJSON(obj) { + const json = JSON.stringify(obj, null, 2); + return json + .replace(/"([^"]+)":/g, '"$1":') + .replace(/: "([^"]*)"/g, ': "$1"') + .replace(/: (\d+)/g, ': $1') + .replace(/: (true|false)/g, ': $1') + .replace(/: (null)/g, ': $1'); + } + + // Populate JSON containers + document.getElementById('json1').innerHTML = highlightJSON(exampleMessages.msg1); + document.getElementById('json2').innerHTML = highlightJSON(exampleMessages.msg2); + document.getElementById('json3').innerHTML = highlightJSON(exampleMessages.msg3); + document.getElementById('json4').innerHTML = highlightJSON(exampleMessages.msg4); + document.getElementById('json5').innerHTML = highlightJSON(exampleMessages.msg5); + document.getElementById('json6').innerHTML = highlightJSON(exampleMessages.msg6); + + diff --git a/samples/client/lit/personalized_learning/api-server.ts b/samples/client/lit/personalized_learning/api-server.ts index b844fe139..855aff2a1 100644 --- a/samples/client/lit/personalized_learning/api-server.ts +++ b/samples/client/lit/personalized_learning/api-server.ts @@ -32,13 +32,13 @@ * GENAI_MODEL - Gemini model to use (default: gemini-2.5-flash) */ -import { createServer } from "http"; -import { execSync } from "child_process"; -import { writeFileSync, readFileSync, existsSync } from "fs"; -import { join } from "path"; -import { config } from "dotenv"; -import { initializeApp, applicationDefault } from "firebase-admin/app"; -import { getAuth } from "firebase-admin/auth"; +import {createServer} from 'http'; +import {execSync} from 'child_process'; +import {writeFileSync, readFileSync, existsSync} from 'fs'; +import {join} from 'path'; +import {config} from 'dotenv'; +import {initializeApp, applicationDefault} from 'firebase-admin/app'; +import {getAuth} from 'firebase-admin/auth'; // Load environment variables config(); @@ -46,19 +46,21 @@ config(); // ============================================================================= // FIREBASE ADMIN - Server-side authentication // ============================================================================= -initializeApp({ credential: applicationDefault() }); +initializeApp({credential: applicationDefault()}); // Local dev mode: skip auth when Firebase is not configured (matches client behavior) const IS_LOCAL_DEV_MODE = !process.env.VITE_FIREBASE_API_KEY; if (IS_LOCAL_DEV_MODE) { - console.warn("[API Server] ⚠️ LOCAL DEV MODE: Authentication disabled (VITE_FIREBASE_API_KEY not set)"); + console.warn( + '[API Server] ⚠️ LOCAL DEV MODE: Authentication disabled (VITE_FIREBASE_API_KEY not set)', + ); } // Access control - reads from environment variables (shared with src/firebase-auth.ts) // Uses VITE_ prefix so the same .env works for both client and server -const ALLOWED_DOMAIN = process.env.VITE_ALLOWED_DOMAIN ?? "google.com"; -const ALLOWED_EMAILS: string[] = (process.env.VITE_ALLOWED_EMAILS ?? "") - .split(",") +const ALLOWED_DOMAIN = process.env.VITE_ALLOWED_DOMAIN ?? 'google.com'; +const ALLOWED_EMAILS: string[] = (process.env.VITE_ALLOWED_EMAILS ?? '') + .split(',') .map((e: string) => e.trim().toLowerCase()) .filter((e: string) => e.length > 0); @@ -78,26 +80,26 @@ async function authenticateRequest(req: any, res: any): Promise { } const authHeader = req.headers.authorization; - if (!authHeader?.startsWith("Bearer ")) { - res.writeHead(401, { "Content-Type": "application/json" }); - res.end(JSON.stringify({ error: "Missing or malformed Authorization header" })); + if (!authHeader?.startsWith('Bearer ')) { + res.writeHead(401, {'Content-Type': 'application/json'}); + res.end(JSON.stringify({error: 'Missing or malformed Authorization header'})); return false; } try { - const token = authHeader.split("Bearer ")[1]; + const token = authHeader.split('Bearer ')[1]; const decoded = await getAuth().verifyIdToken(token); if (!isAllowedEmail(decoded.email)) { - console.error("[API Server] Access denied for:", decoded.email); - res.writeHead(403, { "Content-Type": "application/json" }); - res.end(JSON.stringify({ error: "Email not authorized" })); + console.error('[API Server] Access denied for:', decoded.email); + res.writeHead(403, {'Content-Type': 'application/json'}); + res.end(JSON.stringify({error: 'Email not authorized'})); return false; } return true; } catch (err) { const message = err instanceof Error ? err.message : String(err); - console.error("[API Server] Auth failed:", message); - res.writeHead(403, { "Content-Type": "application/json" }); - res.end(JSON.stringify({ error: "Invalid or expired token" })); + console.error('[API Server] Auth failed:', message); + res.writeHead(403, {'Content-Type': 'application/json'}); + res.end(JSON.stringify({error: 'Invalid or expired token'})); return false; } } @@ -105,20 +107,20 @@ async function authenticateRequest(req: any, res: any): Promise { // ============================================================================= // MESSAGE LOG - Captures all request/response traffic for demo purposes // ============================================================================= -const LOG_FILE = "./demo-message-log.json"; +const LOG_FILE = './demo-message-log.json'; let messageLog: Array<{ sequence: number; timestamp: string; - direction: "CLIENT_TO_SERVER" | "SERVER_TO_AGENT" | "AGENT_TO_SERVER" | "SERVER_TO_CLIENT"; + direction: 'CLIENT_TO_SERVER' | 'SERVER_TO_AGENT' | 'AGENT_TO_SERVER' | 'SERVER_TO_CLIENT'; endpoint: string; data: unknown; }> = []; let sequenceCounter = 0; function logMessage( - direction: "CLIENT_TO_SERVER" | "SERVER_TO_AGENT" | "AGENT_TO_SERVER" | "SERVER_TO_CLIENT", + direction: 'CLIENT_TO_SERVER' | 'SERVER_TO_AGENT' | 'AGENT_TO_SERVER' | 'SERVER_TO_CLIENT', endpoint: string, - data: unknown + data: unknown, ) { const entry = { sequence: ++sequenceCounter, @@ -137,22 +139,22 @@ function logMessage( function resetLog() { messageLog = []; sequenceCounter = 0; - writeFileSync(LOG_FILE, "[]"); + writeFileSync(LOG_FILE, '[]'); console.log(`[LOG] Reset log file: ${LOG_FILE}`); } // Reset log on server start resetLog(); -const PORT = parseInt(process.env.API_PORT || "8080"); +const PORT = parseInt(process.env.API_PORT || '8080'); const PROJECT = process.env.GOOGLE_CLOUD_PROJECT; // Use us-central1 region for consistency with Agent Engine -const LOCATION = process.env.GOOGLE_CLOUD_LOCATION || "us-central1"; -const MODEL = process.env.GENAI_MODEL || "gemini-2.5-flash"; +const LOCATION = process.env.GOOGLE_CLOUD_LOCATION || 'us-central1'; +const MODEL = process.env.GENAI_MODEL || 'gemini-2.5-flash'; // Validate required environment variables if (!PROJECT) { - console.error("ERROR: GOOGLE_CLOUD_PROJECT environment variable is required"); + console.error('ERROR: GOOGLE_CLOUD_PROJECT environment variable is required'); process.exit(1); } @@ -160,62 +162,92 @@ if (!PROJECT) { // See QUICKSTART.md for deployment instructions // Note: Agent Engine is deployed in us-central1 (not global like Gemini API) const AGENT_ENGINE_CONFIG = { - projectNumber: process.env.AGENT_ENGINE_PROJECT_NUMBER || "", - location: process.env.AGENT_ENGINE_LOCATION || "us-central1", - resourceId: process.env.AGENT_ENGINE_RESOURCE_ID || "", + projectNumber: process.env.AGENT_ENGINE_PROJECT_NUMBER || '', + location: process.env.AGENT_ENGINE_LOCATION || 'us-central1', + resourceId: process.env.AGENT_ENGINE_RESOURCE_ID || '', }; if (!AGENT_ENGINE_CONFIG.projectNumber || !AGENT_ENGINE_CONFIG.resourceId) { - console.warn("WARNING: AGENT_ENGINE_PROJECT_NUMBER and AGENT_ENGINE_RESOURCE_ID not set."); - console.warn(" Agent Engine features will not work. See QUICKSTART.md for setup."); + console.warn('WARNING: AGENT_ENGINE_PROJECT_NUMBER and AGENT_ENGINE_RESOURCE_ID not set.'); + console.warn(' Agent Engine features will not work. See QUICKSTART.md for setup.'); } // ============================================================================= // OpenStax Source Attribution - Maps topics to specific textbook sections // ============================================================================= -const OPENSTAX_BASE = "https://openstax.org/books/biology-ap-courses/pages/"; +const OPENSTAX_BASE = 'https://openstax.org/books/biology-ap-courses/pages/'; -const OPENSTAX_SECTIONS: Record = { +const OPENSTAX_SECTIONS: Record = { // ATP and Energy - "atp": { slug: "6-4-atp-adenosine-triphosphate", title: "ATP: Adenosine Triphosphate" }, - "bond energy": { slug: "6-4-atp-adenosine-triphosphate", title: "ATP: Adenosine Triphosphate" }, - "energy currency": { slug: "6-4-atp-adenosine-triphosphate", title: "ATP: Adenosine Triphosphate" }, - "hydrolysis": { slug: "6-4-atp-adenosine-triphosphate", title: "ATP: Adenosine Triphosphate" }, + atp: {slug: '6-4-atp-adenosine-triphosphate', title: 'ATP: Adenosine Triphosphate'}, + 'bond energy': {slug: '6-4-atp-adenosine-triphosphate', title: 'ATP: Adenosine Triphosphate'}, + 'energy currency': { + slug: '6-4-atp-adenosine-triphosphate', + title: 'ATP: Adenosine Triphosphate', + }, + hydrolysis: {slug: '6-4-atp-adenosine-triphosphate', title: 'ATP: Adenosine Triphosphate'}, // Thermodynamics - "thermodynamics": { slug: "6-3-the-laws-of-thermodynamics", title: "The Laws of Thermodynamics" }, - "gibbs": { slug: "6-2-potential-kinetic-free-and-activation-energy", title: "Potential, Kinetic, Free, and Activation Energy" }, - "free energy": { slug: "6-2-potential-kinetic-free-and-activation-energy", title: "Potential, Kinetic, Free, and Activation Energy" }, + thermodynamics: {slug: '6-3-the-laws-of-thermodynamics', title: 'The Laws of Thermodynamics'}, + gibbs: { + slug: '6-2-potential-kinetic-free-and-activation-energy', + title: 'Potential, Kinetic, Free, and Activation Energy', + }, + 'free energy': { + slug: '6-2-potential-kinetic-free-and-activation-energy', + title: 'Potential, Kinetic, Free, and Activation Energy', + }, // Metabolism - "metabolism": { slug: "6-1-energy-and-metabolism", title: "Energy and Metabolism" }, - "enzymes": { slug: "6-5-enzymes", title: "Enzymes" }, - "glycolysis": { slug: "7-2-glycolysis", title: "Glycolysis" }, - "krebs": { slug: "7-3-oxidation-of-pyruvate-and-the-citric-acid-cycle", title: "Oxidation of Pyruvate and the Citric Acid Cycle" }, - "citric acid": { slug: "7-3-oxidation-of-pyruvate-and-the-citric-acid-cycle", title: "Oxidation of Pyruvate and the Citric Acid Cycle" }, - "oxidative phosphorylation": { slug: "7-4-oxidative-phosphorylation", title: "Oxidative Phosphorylation" }, - "electron transport": { slug: "7-4-oxidative-phosphorylation", title: "Oxidative Phosphorylation" }, + metabolism: {slug: '6-1-energy-and-metabolism', title: 'Energy and Metabolism'}, + enzymes: {slug: '6-5-enzymes', title: 'Enzymes'}, + glycolysis: {slug: '7-2-glycolysis', title: 'Glycolysis'}, + krebs: { + slug: '7-3-oxidation-of-pyruvate-and-the-citric-acid-cycle', + title: 'Oxidation of Pyruvate and the Citric Acid Cycle', + }, + 'citric acid': { + slug: '7-3-oxidation-of-pyruvate-and-the-citric-acid-cycle', + title: 'Oxidation of Pyruvate and the Citric Acid Cycle', + }, + 'oxidative phosphorylation': { + slug: '7-4-oxidative-phosphorylation', + title: 'Oxidative Phosphorylation', + }, + 'electron transport': { + slug: '7-4-oxidative-phosphorylation', + title: 'Oxidative Phosphorylation', + }, // Photosynthesis - "photosynthesis": { slug: "8-1-overview-of-photosynthesis", title: "Overview of Photosynthesis" }, - "light reactions": { slug: "8-2-the-light-dependent-reaction-of-photosynthesis", title: "The Light-Dependent Reactions" }, - "calvin cycle": { slug: "8-3-using-light-to-make-organic-molecules", title: "Using Light to Make Organic Molecules" }, + photosynthesis: {slug: '8-1-overview-of-photosynthesis', title: 'Overview of Photosynthesis'}, + 'light reactions': { + slug: '8-2-the-light-dependent-reaction-of-photosynthesis', + title: 'The Light-Dependent Reactions', + }, + 'calvin cycle': { + slug: '8-3-using-light-to-make-organic-molecules', + title: 'Using Light to Make Organic Molecules', + }, // Cell structure - "cell membrane": { slug: "5-1-components-and-structure", title: "Cell Membrane Components and Structure" }, - "transport": { slug: "5-2-passive-transport", title: "Passive Transport" }, + 'cell membrane': { + slug: '5-1-components-and-structure', + title: 'Cell Membrane Components and Structure', + }, + transport: {slug: '5-2-passive-transport', title: 'Passive Transport'}, // Reproduction - "reproductive system": { slug: "34-1-reproduction-methods", title: "Reproduction Methods" }, - "reproductive": { slug: "34-1-reproduction-methods", title: "Reproduction Methods" }, - "reproduction": { slug: "34-1-reproduction-methods", title: "Reproduction Methods" }, + 'reproductive system': {slug: '34-1-reproduction-methods', title: 'Reproduction Methods'}, + reproductive: {slug: '34-1-reproduction-methods', title: 'Reproduction Methods'}, + reproduction: {slug: '34-1-reproduction-methods', title: 'Reproduction Methods'}, // Default - intentionally empty to avoid wrong citations - "default": { slug: "", title: "Biology Content" }, + default: {slug: '', title: 'Biology Content'}, }; -function getOpenStaxSource(topic: string): { provider: string; title: string; url: string } { +function getOpenStaxSource(topic: string): {provider: string; title: string; url: string} { const topicLower = topic.toLowerCase(); // Find matching section for (const [keyword, section] of Object.entries(OPENSTAX_SECTIONS)) { - if (keyword !== "default" && topicLower.includes(keyword)) { + if (keyword !== 'default' && topicLower.includes(keyword)) { return { - provider: "OpenStax Biology for AP Courses", + provider: 'OpenStax Biology for AP Courses', title: section.title, url: OPENSTAX_BASE + section.slug, }; @@ -223,9 +255,9 @@ function getOpenStaxSource(topic: string): { provider: string; title: string; ur } // Default fallback - const defaultSection = OPENSTAX_SECTIONS["default"]; + const defaultSection = OPENSTAX_SECTIONS['default']; return { - provider: "OpenStax Biology for AP Courses", + provider: 'OpenStax Biology for AP Courses', title: defaultSection.title, url: OPENSTAX_BASE + defaultSection.slug, }; @@ -235,7 +267,7 @@ function getOpenStaxSource(topic: string): { provider: string; title: string; ur let genai: any = null; async function initGenAI() { - const { GoogleGenAI } = await import("@google/genai"); + const {GoogleGenAI} = await import('@google/genai'); // Use VertexAI with Application Default Credentials genai = new GoogleGenAI({ vertexai: true, @@ -248,7 +280,7 @@ async function initGenAI() { interface ChatMessage { role: string; - parts: { text: string }[]; + parts: {text: string}[]; } interface ChatRequest { @@ -261,7 +293,7 @@ interface ChatRequest { // ============================================================================= // ACCESS TOKEN CACHING // ============================================================================= -let cachedAccessToken: { token: string; expiresAt: number } | null = null; +let cachedAccessToken: {token: string; expiresAt: number} | null = null; // Get Google Cloud access token with caching // In Cloud Run, use the metadata server. Locally, use gcloud CLI. @@ -275,9 +307,10 @@ async function getAccessToken(): Promise { // Try metadata server first (Cloud Run environment) try { - const metadataUrl = "http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token"; + const metadataUrl = + 'http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token'; const response = await fetch(metadataUrl, { - headers: { "Metadata-Flavor": "Google" }, + headers: {'Metadata-Flavor': 'Google'}, }); if (response.ok) { const data = await response.json(); @@ -286,7 +319,7 @@ async function getAccessToken(): Promise { token: data.access_token, expiresAt: now + 3480000, // 58 minutes }; - console.log("[API Server] Cached access token from metadata server"); + console.log('[API Server] Cached access token from metadata server'); return data.access_token; } } catch { @@ -295,25 +328,25 @@ async function getAccessToken(): Promise { // Fall back to gcloud CLI (local development) try { - const token = execSync("gcloud auth print-access-token", { - encoding: "utf-8", + const token = execSync('gcloud auth print-access-token', { + encoding: 'utf-8', }).trim(); // Cache the token cachedAccessToken = { token: token, expiresAt: now + 3480000, // 58 minutes }; - console.log("[API Server] Cached access token from gcloud CLI"); + console.log('[API Server] Cached access token from gcloud CLI'); return token; } catch (error) { - console.error("[API Server] Failed to get access token:", error); - throw new Error("Failed to get Google Cloud access token. Run: gcloud auth login"); + console.error('[API Server] Failed to get access token:', error); + throw new Error('Failed to get Google Cloud access token. Run: gcloud auth login'); } } // Query Agent Engine for A2UI content using streamQuery -async function queryAgentEngine(format: string, context: string = ""): Promise { - const { projectNumber, location, resourceId } = AGENT_ENGINE_CONFIG; +async function queryAgentEngine(format: string, context: string = ''): Promise { + const {projectNumber, location, resourceId} = AGENT_ENGINE_CONFIG; // Use :streamQuery endpoint with stream_query method for ADK agents const url = `https://${location}-aiplatform.googleapis.com/v1/projects/${projectNumber}/locations/${location}/reasoningEngines/${resourceId}:streamQuery`; @@ -322,8 +355,8 @@ async function queryAgentEngine(format: string, context: string = ""): Promise { - try { return JSON.parse(chunk); } catch { return chunk.substring(0, 200); } - }), - note: "Showing first 5 chunks only", + rawChunks: responseText + .trim() + .split('\n') + .slice(0, 5) + .map(chunk => { + try { + return JSON.parse(chunk); + } catch { + return chunk.substring(0, 200); + } + }), + note: 'Showing first 5 chunks only', }); // Extract text from all chunks - const chunks = responseText.trim().split("\n").filter((line: string) => line.trim()); - let fullText = ""; + const chunks = responseText + .trim() + .split('\n') + .filter((line: string) => line.trim()); + let fullText = ''; - let functionResponseResult = ""; // Prioritize function_response over text - let textParts = ""; + let functionResponseResult = ''; // Prioritize function_response over text + let textParts = ''; for (const chunk of chunks) { try { const parsed = JSON.parse(chunk); - console.log("[API Server] Parsed chunk keys:", Object.keys(parsed)); + console.log('[API Server] Parsed chunk keys:', Object.keys(parsed)); // Extract from content.parts - can contain text, function_call, or function_response if (parsed.content?.parts) { @@ -395,23 +439,23 @@ async function queryAgentEngine(format: string, context: string = ""): Promise { + const extractA2UIWithSource = ( + text: string, + ): {a2ui: unknown[] | null; source?: {url: string; title: string; provider: string}} => { // Try parsing as raw JSON array (legacy format) - if (text.startsWith("[")) { + if (text.startsWith('[')) { try { const parsed = JSON.parse(text); - if (Array.isArray(parsed)) return { a2ui: parsed }; + if (Array.isArray(parsed)) return {a2ui: parsed}; } catch {} } // Try parsing as object with a2ui and source (new format) - if (text.startsWith("{")) { + if (text.startsWith('{')) { try { const wrapper = JSON.parse(text); // New format: {a2ui: [...], source: {...}} if (wrapper.a2ui && Array.isArray(wrapper.a2ui)) { - return { a2ui: wrapper.a2ui, source: wrapper.source || undefined }; + return {a2ui: wrapper.a2ui, source: wrapper.source || undefined}; } // Legacy format: {"result": "..."} if (wrapper.result) { - const inner = typeof wrapper.result === 'string' - ? JSON.parse(wrapper.result) - : wrapper.result; + const inner = + typeof wrapper.result === 'string' ? JSON.parse(wrapper.result) : wrapper.result; // Check if inner is the new format if (inner && inner.a2ui && Array.isArray(inner.a2ui)) { - return { a2ui: inner.a2ui, source: inner.source || undefined }; + return {a2ui: inner.a2ui, source: inner.source || undefined}; } - if (Array.isArray(inner)) return { a2ui: inner }; + if (Array.isArray(inner)) return {a2ui: inner}; } } catch {} } @@ -459,7 +504,7 @@ async function queryAgentEngine(format: string, context: string = ""): Promise { - const { systemPrompt, intentGuidance, messages, userMessage } = request; +async function handleChatRequest(request: ChatRequest): Promise<{text: string}> { + const {systemPrompt, intentGuidance, messages, userMessage} = request; // Build the full system instruction const fullSystemPrompt = `${systemPrompt}\n\n${intentGuidance}`; // Convert messages to Gemini format - const contents = messages.map((m) => ({ - role: m.role === "assistant" ? "model" : "user", + const contents = messages.map(m => ({ + role: m.role === 'assistant' ? 'model' : 'user', parts: m.parts, })); // Add the current user message contents.push({ - role: "user", - parts: [{ text: userMessage }], + role: 'user', + parts: [{text: userMessage}], }); try { @@ -686,9 +737,9 @@ async function handleChatRequest(request: ChatRequest): Promise<{ text: string } }); const text = response.text || "I apologize, I couldn't generate a response."; - return { text }; + return {text}; } catch (error) { - console.error("[API Server] Error calling Gemini:", error); + console.error('[API Server] Error calling Gemini:', error); throw error; } } @@ -707,11 +758,13 @@ interface CombinedChatRequest { interface CombinedChatResponse { intent: string; text: string; - keywords?: string; // Comma-separated keywords for content-generating intents + keywords?: string; // Comma-separated keywords for content-generating intents } -async function handleCombinedChatRequest(request: CombinedChatRequest): Promise { - const { systemPrompt, messages, userMessage, recentContext } = request; +async function handleCombinedChatRequest( + request: CombinedChatRequest, +): Promise { + const {systemPrompt, messages, userMessage, recentContext} = request; const combinedSystemPrompt = `${systemPrompt} @@ -758,8 +811,8 @@ The keywords help the content retrieval system find the right OpenStax textbook Then provide an appropriate conversational response following your tutor persona.`; // Convert messages to Gemini format - const contents = messages.map((m) => ({ - role: m.role === "assistant" ? "model" : "user", + const contents = messages.map(m => ({ + role: m.role === 'assistant' ? 'model' : 'user', parts: m.parts, })); @@ -770,8 +823,8 @@ Then provide an appropriate conversational response following your tutor persona } contents.push({ - role: "user", - parts: [{ text: contextualMessage }], + role: 'user', + parts: [{text: contextualMessage}], }); try { @@ -780,138 +833,138 @@ Then provide an appropriate conversational response following your tutor persona contents, config: { systemInstruction: combinedSystemPrompt, - responseMimeType: "application/json", + responseMimeType: 'application/json', }, }); - const responseText = response.text?.trim() || ""; - console.log("[API Server] Combined response:", responseText.substring(0, 200)); + const responseText = response.text?.trim() || ''; + console.log('[API Server] Combined response:', responseText.substring(0, 200)); try { const parsed = JSON.parse(responseText); const result: CombinedChatResponse = { - intent: parsed.intent || "general", + intent: parsed.intent || 'general', text: parsed.text || "I apologize, I couldn't generate a response.", }; // Include keywords if present (for content-generating intents) if (parsed.keywords) { result.keywords = parsed.keywords; - console.log("[API Server] Keywords for content retrieval:", parsed.keywords); + console.log('[API Server] Keywords for content retrieval:', parsed.keywords); } return result; } catch (parseError) { - console.error("[API Server] Failed to parse combined response:", parseError); + console.error('[API Server] Failed to parse combined response:', parseError); // Fallback: return general intent with raw text return { - intent: "general", + intent: 'general', text: responseText || "I apologize, I couldn't generate a response.", }; } } catch (error) { - console.error("[API Server] Error calling Gemini for combined request:", error); + console.error('[API Server] Error calling Gemini for combined request:', error); throw error; } } function parseBody(req: any): Promise { return new Promise((resolve, reject) => { - let body = ""; - req.on("data", (chunk: string) => { + let body = ''; + req.on('data', (chunk: string) => { body += chunk; }); - req.on("end", () => { + req.on('end', () => { try { resolve(JSON.parse(body)); } catch (e) { reject(e); } }); - req.on("error", reject); + req.on('error', reject); }); } async function main() { - console.log("[API Server] Initializing Gemini client..."); + console.log('[API Server] Initializing Gemini client...'); await initGenAI(); const server = createServer(async (req, res) => { // CORS headers - res.setHeader("Access-Control-Allow-Origin", "*"); - res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS"); - res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization"); + res.setHeader('Access-Control-Allow-Origin', '*'); + res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS'); + res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization'); - if (req.method === "OPTIONS") { + if (req.method === 'OPTIONS') { res.writeHead(204); res.end(); return; } // Health check - if (req.url === "/health" && req.method === "GET") { - res.writeHead(200, { "Content-Type": "application/json" }); - res.end(JSON.stringify({ status: "healthy" })); + if (req.url === '/health' && req.method === 'GET') { + res.writeHead(200, {'Content-Type': 'application/json'}); + res.end(JSON.stringify({status: 'healthy'})); return; } // Reset message log (useful before demo) - if (req.url === "/reset-log" && req.method === "POST") { + if (req.url === '/reset-log' && req.method === 'POST') { resetLog(); - res.writeHead(200, { "Content-Type": "application/json" }); - res.end(JSON.stringify({ status: "log reset", file: LOG_FILE })); + res.writeHead(200, {'Content-Type': 'application/json'}); + res.end(JSON.stringify({status: 'log reset', file: LOG_FILE})); return; } // View current log - if (req.url === "/log" && req.method === "GET") { - res.writeHead(200, { "Content-Type": "application/json" }); + if (req.url === '/log' && req.method === 'GET') { + res.writeHead(200, {'Content-Type': 'application/json'}); res.end(JSON.stringify(messageLog, null, 2)); return; } // Authorization check endpoint - used by frontend to verify user is allowed // This is the SINGLE SOURCE OF TRUTH for access control decisions - if (req.url === "/api/check-access" && req.method === "GET") { + if (req.url === '/api/check-access' && req.method === 'GET') { if (!(await authenticateRequest(req, res))) return; // If authenticateRequest passes, user is both authenticated AND authorized - res.writeHead(200, { "Content-Type": "application/json" }); - res.end(JSON.stringify({ authorized: true })); + res.writeHead(200, {'Content-Type': 'application/json'}); + res.end(JSON.stringify({authorized: true})); return; } // A2A Agent Engine endpoint - if (req.url === "/a2ui-agent/a2a/query" && req.method === "POST") { + if (req.url === '/a2ui-agent/a2a/query' && req.method === 'POST') { if (!(await authenticateRequest(req, res))) return; try { const body = await parseBody(req); - console.log("[API Server] ========================================"); - console.log("[API Server] A2A QUERY - REQUESTING A2UI CONTENT"); - console.log("[API Server] Full message:", body.message); - console.log("[API Server] Session ID:", body.session_id); - console.log("[API Server] ========================================"); + console.log('[API Server] ========================================'); + console.log('[API Server] A2A QUERY - REQUESTING A2UI CONTENT'); + console.log('[API Server] Full message:', body.message); + console.log('[API Server] Session ID:', body.session_id); + console.log('[API Server] ========================================'); // LOG: Client → Server request for A2UI content - logMessage("CLIENT_TO_SERVER", "/a2ui-agent/a2a/query", { - description: "Browser client requesting A2UI content generation", + logMessage('CLIENT_TO_SERVER', '/a2ui-agent/a2a/query', { + description: 'Browser client requesting A2UI content generation', requestBody: body, }); // Parse format from message (e.g., "flashcards:context" or just "flashcards") - const parts = (body.message || "flashcards").split(":"); + const parts = (body.message || 'flashcards').split(':'); const format = parts[0].trim(); - const context = parts.slice(1).join(":").trim(); + const context = parts.slice(1).join(':').trim(); - console.log("[API Server] Parsed format:", format); - console.log("[API Server] Parsed context (keywords):", context); - console.log("[API Server] This context will be sent to Agent Engine for topic matching"); + console.log('[API Server] Parsed format:', format); + console.log('[API Server] Parsed context (keywords):', context); + console.log('[API Server] This context will be sent to Agent Engine for topic matching'); let result = await queryAgentEngine(format, context); // If quiz was requested but Agent Engine returned Flashcards or empty, // generate quiz locally using Gemini - if (format.toLowerCase() === "quiz") { + if (format.toLowerCase() === 'quiz') { const a2uiStr = JSON.stringify(result.a2ui || []); - const hasFlashcards = a2uiStr.includes("Flashcard"); - const hasQuizCards = a2uiStr.includes("QuizCard"); + const hasFlashcards = a2uiStr.includes('Flashcard'); + const hasQuizCards = a2uiStr.includes('QuizCard'); const isEmpty = !result.a2ui || result.a2ui.length === 0; if (isEmpty || (hasFlashcards && !hasQuizCards)) { @@ -924,8 +977,8 @@ async function main() { } // LOG: Server → Client response with A2UI - logMessage("SERVER_TO_CLIENT", "/a2ui-agent/a2a/query", { - description: "Sending A2UI JSON payload to browser for rendering", + logMessage('SERVER_TO_CLIENT', '/a2ui-agent/a2a/query', { + description: 'Sending A2UI JSON payload to browser for rendering', format: result.format, surfaceId: result.surfaceId, source: result.source, @@ -933,63 +986,63 @@ async function main() { a2uiPayload: result.a2ui, }); - res.writeHead(200, { "Content-Type": "application/json" }); + res.writeHead(200, {'Content-Type': 'application/json'}); res.end(JSON.stringify(result)); } catch (error: any) { - console.error("[API Server] A2A error:", error); - res.writeHead(500, { "Content-Type": "application/json" }); - res.end(JSON.stringify({ error: error.message })); + console.error('[API Server] A2A error:', error); + res.writeHead(500, {'Content-Type': 'application/json'}); + res.end(JSON.stringify({error: error.message})); } return; } // Chat endpoint - if (req.url === "/api/chat" && req.method === "POST") { + if (req.url === '/api/chat' && req.method === 'POST') { if (!(await authenticateRequest(req, res))) return; try { const body = await parseBody(req); - console.log("[API Server] Chat request received"); + console.log('[API Server] Chat request received'); // LOG: Client → Server chat request - logMessage("CLIENT_TO_SERVER", "/api/chat", { - description: "Browser client sending chat message to Gemini", + logMessage('CLIENT_TO_SERVER', '/api/chat', { + description: 'Browser client sending chat message to Gemini', userMessage: body.userMessage, - intentGuidance: body.intentGuidance?.substring(0, 100) + "...", + intentGuidance: body.intentGuidance?.substring(0, 100) + '...', conversationLength: body.messages?.length || 0, }); const result = await handleChatRequest(body); // LOG: Server → Client chat response - logMessage("SERVER_TO_CLIENT", "/api/chat", { - description: "Gemini response text (conversational layer)", + logMessage('SERVER_TO_CLIENT', '/api/chat', { + description: 'Gemini response text (conversational layer)', responseText: result.text, }); - res.writeHead(200, { "Content-Type": "application/json" }); + res.writeHead(200, {'Content-Type': 'application/json'}); res.end(JSON.stringify(result)); } catch (error: any) { - console.error("[API Server] Error:", error); - res.writeHead(500, { "Content-Type": "application/json" }); - res.end(JSON.stringify({ error: error.message })); + console.error('[API Server] Error:', error); + res.writeHead(500, {'Content-Type': 'application/json'}); + res.end(JSON.stringify({error: error.message})); } return; } // Combined chat endpoint - performs intent detection AND response in one LLM call - if (req.url === "/api/chat-with-intent" && req.method === "POST") { + if (req.url === '/api/chat-with-intent' && req.method === 'POST') { if (!(await authenticateRequest(req, res))) return; try { const body = await parseBody(req); - console.log("[API Server] ========================================"); - console.log("[API Server] COMBINED CHAT REQUEST RECEIVED"); - console.log("[API Server] User message:", body.userMessage); - console.log("[API Server] Conversation history length:", body.messages?.length || 0); - console.log("[API Server] ========================================"); + console.log('[API Server] ========================================'); + console.log('[API Server] COMBINED CHAT REQUEST RECEIVED'); + console.log('[API Server] User message:', body.userMessage); + console.log('[API Server] Conversation history length:', body.messages?.length || 0); + console.log('[API Server] ========================================'); // LOG: Client → Server combined request - logMessage("CLIENT_TO_SERVER", "/api/chat-with-intent", { - description: "Browser client requesting combined intent+response (latency optimization)", + logMessage('CLIENT_TO_SERVER', '/api/chat-with-intent', { + description: 'Browser client requesting combined intent+response (latency optimization)', userMessage: body.userMessage, recentContext: body.recentContext, conversationLength: body.messages?.length || 0, @@ -998,52 +1051,52 @@ async function main() { const result = await handleCombinedChatRequest(body); - console.log("[API Server] ========================================"); - console.log("[API Server] GEMINI COMBINED RESPONSE:"); - console.log("[API Server] Intent:", result.intent); - console.log("[API Server] Keywords:", result.keywords || "(none - not a content intent)"); - console.log("[API Server] Text:", result.text.substring(0, 200)); - console.log("[API Server] ========================================"); + console.log('[API Server] ========================================'); + console.log('[API Server] GEMINI COMBINED RESPONSE:'); + console.log('[API Server] Intent:', result.intent); + console.log('[API Server] Keywords:', result.keywords || '(none - not a content intent)'); + console.log('[API Server] Text:', result.text.substring(0, 200)); + console.log('[API Server] ========================================'); // LOG: Server → Client combined response - logMessage("SERVER_TO_CLIENT", "/api/chat-with-intent", { - description: "Combined intent detection and response in single LLM call", + logMessage('SERVER_TO_CLIENT', '/api/chat-with-intent', { + description: 'Combined intent detection and response in single LLM call', intent: result.intent, keywords: result.keywords, responseText: result.text, }); - res.writeHead(200, { "Content-Type": "application/json" }); + res.writeHead(200, {'Content-Type': 'application/json'}); res.end(JSON.stringify(result)); } catch (error: any) { - console.error("[API Server] Combined chat error:", error); - res.writeHead(500, { "Content-Type": "application/json" }); - res.end(JSON.stringify({ error: error.message })); + console.error('[API Server] Combined chat error:', error); + res.writeHead(500, {'Content-Type': 'application/json'}); + res.end(JSON.stringify({error: error.message})); } return; } // Static file serving for frontend const MIME_TYPES: Record = { - ".html": "text/html", - ".js": "application/javascript", - ".css": "text/css", - ".json": "application/json", - ".png": "image/png", - ".jpg": "image/jpeg", - ".svg": "image/svg+xml", - ".ico": "image/x-icon", + '.html': 'text/html', + '.js': 'application/javascript', + '.css': 'text/css', + '.json': 'application/json', + '.png': 'image/png', + '.jpg': 'image/jpeg', + '.svg': 'image/svg+xml', + '.ico': 'image/x-icon', }; // Serve static files (Vite builds to dist/, but index.html is in root for dev) - if (req.method === "GET") { - let filePath = req.url === "/" ? "/index.html" : req.url || "/index.html"; + if (req.method === 'GET') { + let filePath = req.url === '/' ? '/index.html' : req.url || '/index.html'; // Remove query string - filePath = filePath.split("?")[0]; + filePath = filePath.split('?')[0]; // Try dist/ first (production build), then root (development) - const distPath = join(process.cwd(), "dist", filePath); + const distPath = join(process.cwd(), 'dist', filePath); const rootPath = join(process.cwd(), filePath); const fullPath = existsSync(distPath) ? distPath : rootPath; @@ -1051,10 +1104,10 @@ async function main() { if (existsSync(fullPath)) { try { const content = readFileSync(fullPath); - const ext = filePath.substring(filePath.lastIndexOf(".")); - const contentType = MIME_TYPES[ext] || "application/octet-stream"; + const ext = filePath.substring(filePath.lastIndexOf('.')); + const contentType = MIME_TYPES[ext] || 'application/octet-stream'; - res.writeHead(200, { "Content-Type": contentType }); + res.writeHead(200, {'Content-Type': contentType}); res.end(content); return; } catch (err) { @@ -1064,8 +1117,8 @@ async function main() { } // 404 for other routes - res.writeHead(404, { "Content-Type": "application/json" }); - res.end(JSON.stringify({ error: "Not found" })); + res.writeHead(404, {'Content-Type': 'application/json'}); + res.end(JSON.stringify({error: 'Not found'})); }); server.listen(PORT, () => { diff --git a/samples/client/lit/personalized_learning/index.html b/samples/client/lit/personalized_learning/index.html index b755d80ba..d80bc4327 100644 --- a/samples/client/lit/personalized_learning/index.html +++ b/samples/client/lit/personalized_learning/index.html @@ -1,4 +1,4 @@ - +
diff --git a/samples/client/lit/personalized_learning/public/404.html b/samples/client/lit/personalized_learning/public/404.html index 6b3db2d76..6706aa502 100644 --- a/samples/client/lit/personalized_learning/public/404.html +++ b/samples/client/lit/personalized_learning/public/404.html @@ -1,4 +1,4 @@ - + - - - Page Not Found - - - -
-

404

-

Page not found

-
- + + + Page Not Found + + + +
+

404

+

Page not found

+
+ diff --git a/samples/client/lit/personalized_learning/public/assets/openstax-bio-glossary.md b/samples/client/lit/personalized_learning/public/assets/openstax-bio-glossary.md index dc3ba0ac8..718e8b200 100644 --- a/samples/client/lit/personalized_learning/public/assets/openstax-bio-glossary.md +++ b/samples/client/lit/personalized_learning/public/assets/openstax-bio-glossary.md @@ -5,9 +5,9 @@ **5' UTR** [16.5 Eukaryotic Post-transcriptional Gene Regulation](https://openstax.org/books/biology-ap-courses/pages/16-5-eukaryotic-post-transcriptional-gene-regulation#term-00005) **60S ribosomal subunit** [16.6 Eukaryotic Translational and Post-translational Gene Regulation](https://openstax.org/books/biology-ap-courses/pages/16-6-eukaryotic-translational-and-post-translational-gene-regulation#term-00006) **7-methylguanosine cap** [15.4 RNA Processing in Eukaryotes](https://openstax.org/books/biology-ap-courses/pages/15-4-rna-processing-in-eukaryotes#term-00002) -***α*****\-helix** [3.4 Proteins](https://openstax.org/books/biology-ap-courses/pages/3-4-proteins#term-00009) -***β*****\-pleated sheet** [3.4 Proteins](https://openstax.org/books/biology-ap-courses/pages/3-4-proteins#term-00010) -**A** +**\*α\*\*\***\-helix** [3.4 Proteins](https://openstax.org/books/biology-ap-courses/pages/3-4-proteins#term-00009) +\***β**\***\-pleated sheet** [3.4 Proteins](https://openstax.org/books/biology-ap-courses/pages/3-4-proteins#term-00010) +**A\** **absorption spectrum** [8.2 The Light-Dependent Reaction of Photosynthesis](https://openstax.org/books/biology-ap-courses/pages/8-2-the-light-dependent-reaction-of-photosynthesis#term-00006) **abstract** [1.1 The Science of Biology](https://openstax.org/books/biology-ap-courses/pages/1-1-the-science-of-biology#term-00020) **acetyl CoA** [7.3 Oxidation of Pyruvate and the Citric Acid Cycle](https://openstax.org/books/biology-ap-courses/pages/7-3-oxidation-of-pyruvate-and-the-citric-acid-cycle#term-00001) @@ -107,8 +107,8 @@ **chiasmata** [11.1 The Process of Meiosis](https://openstax.org/books/biology-ap-courses/pages/11-1-the-process-of-meiosis#term-00009) **chitin** [3.2 Carbohydrates](https://openstax.org/books/biology-ap-courses/pages/3-2-carbohydrates#term-00009) **chlorophyll** [4.3 Eukaryotic Cells](https://openstax.org/books/biology-ap-courses/pages/4-3-eukaryotic-cells#term-00021) -**Chlorophyll *a*** [8.2 The Light-Dependent Reaction of Photosynthesis](https://openstax.org/books/biology-ap-courses/pages/8-2-the-light-dependent-reaction-of-photosynthesis#term-00003) -**chlorophyll *b*** [8.2 The Light-Dependent Reaction of Photosynthesis](https://openstax.org/books/biology-ap-courses/pages/8-2-the-light-dependent-reaction-of-photosynthesis#term-00004) +\*\*Chlorophyll *a**\* [8.2 The Light-Dependent Reaction of Photosynthesis](https://openstax.org/books/biology-ap-courses/pages/8-2-the-light-dependent-reaction-of-photosynthesis#term-00003) +**chlorophyll \*b**\* [8.2 The Light-Dependent Reaction of Photosynthesis](https://openstax.org/books/biology-ap-courses/pages/8-2-the-light-dependent-reaction-of-photosynthesis#term-00004) **chloroplast** [8.1 Overview of Photosynthesis](https://openstax.org/books/biology-ap-courses/pages/8-1-overview-of-photosynthesis#term-00006) **Chloroplasts** [4.3 Eukaryotic Cells](https://openstax.org/books/biology-ap-courses/pages/4-3-eukaryotic-cells#term-00020) **chromatids** [10.1 Cell Division](https://openstax.org/books/biology-ap-courses/pages/10-1-cell-division#term-00011) @@ -117,7 +117,7 @@ **chromosome inversion** [13.2 Chromosomal Basis of Inherited Disorders](https://openstax.org/books/biology-ap-courses/pages/13-2-chromosomal-basis-of-inherited-disorders#term-00012) **Chromosomes** [4.3 Eukaryotic Cells](https://openstax.org/books/biology-ap-courses/pages/4-3-eukaryotic-cells#term-00009), [10.1 Cell Division](https://openstax.org/books/biology-ap-courses/pages/10-1-cell-division#term-00006) **cilia** [4.5 Cytoskeleton](https://openstax.org/books/biology-ap-courses/pages/4-5-cytoskeleton#term-00006) -***cis*****\-acting element** [16.4 Eukaryotic Transcriptional Gene Regulation](https://openstax.org/books/biology-ap-courses/pages/16-4-eukaryotic-transcriptional-gene-regulation#term-00001) +\***cis**\***\-acting element** [16.4 Eukaryotic Transcriptional Gene Regulation](https://openstax.org/books/biology-ap-courses/pages/16-4-eukaryotic-transcriptional-gene-regulation#term-00001) **citric acid cycle** [7.3 Oxidation of Pyruvate and the Citric Acid Cycle](https://openstax.org/books/biology-ap-courses/pages/7-3-oxidation-of-pyruvate-and-the-citric-acid-cycle#term-00002) **clathrin** [5.4 Bulk Transport](https://openstax.org/books/biology-ap-courses/pages/5-4-bulk-transport#term-00002) **cleavage furrow** [10.2 The Cell Cycle](https://openstax.org/books/biology-ap-courses/pages/10-2-the-cell-cycle#term-00020) @@ -153,7 +153,7 @@ **cytoplasm** [4.3 Eukaryotic Cells](https://openstax.org/books/biology-ap-courses/pages/4-3-eukaryotic-cells#term-00004) **cytoskeleton** [4.5 Cytoskeleton](https://openstax.org/books/biology-ap-courses/pages/4-5-cytoskeleton#term-00001) **cytosol** [4.3 Eukaryotic Cells](https://openstax.org/books/biology-ap-courses/pages/4-3-eukaryotic-cells#term-00005) -**D** +**D\*\* **Deductive reasoning** [1.1 The Science of Biology](https://openstax.org/books/biology-ap-courses/pages/1-1-the-science-of-biology#term-00002) **degenerate** [15.1 The Genetic Code](https://openstax.org/books/biology-ap-courses/pages/15-1-the-genetic-code#term-00003) **dehydration synthesis** [3.1 Synthesis of Biological Macromolecules](https://openstax.org/books/biology-ap-courses/pages/3-1-synthesis-of-biological-macromolecules#term-00004) @@ -368,7 +368,7 @@ **Kozak’s rules** [15.5 Ribosomes and Protein Synthesis](https://openstax.org/books/biology-ap-courses/pages/15-5-ribosomes-and-protein-synthesis#term-00006) **Krebs cycle** [7.3 Oxidation of Pyruvate and the Citric Acid Cycle](https://openstax.org/books/biology-ap-courses/pages/7-3-oxidation-of-pyruvate-and-the-citric-acid-cycle#term-00004) **L** -***lac*** **operon** [16.2 Prokaryotic Gene Regulation](https://openstax.org/books/biology-ap-courses/pages/16-2-prokaryotic-gene-regulation#term-00012) +**_lac_** **operon** [16.2 Prokaryotic Gene Regulation](https://openstax.org/books/biology-ap-courses/pages/16-2-prokaryotic-gene-regulation#term-00012) **lagging strand** [14.4 DNA Replication in Prokaryotes](https://openstax.org/books/biology-ap-courses/pages/14-4-dna-replication-in-prokaryotes#term-00008) **law of dominance** [12.3 Laws of Inheritance](https://openstax.org/books/biology-ap-courses/pages/12-3-laws-of-inheritance#term-00001) **law of independent assortment** [12.3 Laws of Inheritance](https://openstax.org/books/biology-ap-courses/pages/12-3-laws-of-inheritance#term-00003) @@ -709,7 +709,7 @@ **triglycerides** [3.3 Lipids](https://openstax.org/books/biology-ap-courses/pages/3-3-lipids#term-00003) **trisomy** [13.2 Chromosomal Basis of Inherited Disorders](https://openstax.org/books/biology-ap-courses/pages/13-2-chromosomal-basis-of-inherited-disorders#term-00009) **Tryptophan** [16.2 Prokaryotic Gene Regulation](https://openstax.org/books/biology-ap-courses/pages/16-2-prokaryotic-gene-regulation#term-00004) -**tryptophan (*trp*) operon** [16.2 Prokaryotic Gene Regulation](https://openstax.org/books/biology-ap-courses/pages/16-2-prokaryotic-gene-regulation#term-00005) +**tryptophan (_trp_) operon** [16.2 Prokaryotic Gene Regulation](https://openstax.org/books/biology-ap-courses/pages/16-2-prokaryotic-gene-regulation#term-00005) **Tumor suppressor genes** [10.4 Cancer and the Cell Cycle](https://openstax.org/books/biology-ap-courses/pages/10-4-cancer-and-the-cell-cycle#term-00003) **U** **ubiquinone** [7.4 Oxidative Phosphorylation](https://openstax.org/books/biology-ap-courses/pages/7-4-oxidative-phosphorylation#term-00002) @@ -734,4 +734,4 @@ **X inactivation** [13.2 Chromosomal Basis of Inherited Disorders](https://openstax.org/books/biology-ap-courses/pages/13-2-chromosomal-basis-of-inherited-disorders#term-00011) **X-linked** [12.2 Characteristics and Traits](https://openstax.org/books/biology-ap-courses/pages/12-2-characteristics-and-traits#term-00015) **Z** -**Zoology** [1.2 Themes and Concepts of Biology](https://openstax.org/books/biology-ap-courses/pages/1-2-themes-and-concepts-of-biology#term-00024) \ No newline at end of file +**Zoology** [1.2 Themes and Concepts of Biology](https://openstax.org/books/biology-ap-courses/pages/1-2-themes-and-concepts-of-biology#term-00024) diff --git a/samples/client/lit/personalized_learning/public/maria-context.html b/samples/client/lit/personalized_learning/public/maria-context.html index 85dda34cf..7a13a3c6d 100644 --- a/samples/client/lit/personalized_learning/public/maria-context.html +++ b/samples/client/lit/personalized_learning/public/maria-context.html @@ -1,4 +1,4 @@ - + - - - + + + Learner Profile: Maria Thompson - - + + - - + +
-
-
- psychology -
-
-

Learner Context Profile

-

session_id: mcat-prep-2025-maria

+
+
+ psychology +
+
+

Learner Context Profile

+

session_id: mcat-prep-2025-maria

+
+
+ +
+
+
MT
+
+

Maria Thompson

+

Pre-Med Student, Cymbal University

+
+ MCAT Prep + AP Biology: 92% + AP Chemistry: Struggling
+
-
-
-
MT
-
-

Maria Thompson

-

Pre-Med Student, Cymbal University

-
- MCAT Prep - AP Biology: 92% - AP Chemistry: Struggling -
-
+
+
Identified Misconception
+
+
+
+ warning +
+
ATP & Bond Energy
- -
-
Identified Misconception
-
-
-
- warning -
-
ATP & Bond Energy
-
-
-
- Energy is stored in ATP bonds like a battery. When we break the phosphate bonds, that stored energy is released. -
-
- check_circle - Correct understanding: ATP releases energy because the products (ADP + Pi) are more thermodynamically stable, not because energy was "stored" in the bonds. -
-
-
+
+
+ Energy is stored in ATP bonds like a battery. When we break the phosphate bonds, + that stored energy is released. +
+
+ check_circle + Correct understanding: ATP releases energy because the products + (ADP + Pi) are more thermodynamically stable, not because energy was "stored" in + the bonds. +
+
+
-
-
Learning Preferences
-
-
-

Learning Style

-

Visual-Kinesthetic

-
-
-

Preferred Analogies

-

Sports & Gym

-
-
-

Study Mode

-

Active recall, spaced repetition

-
-
-

Best Time

-

Morning sessions

-
-
+
+
Learning Preferences
+
+
+

Learning Style

+

Visual-Kinesthetic

- -
-
Subject Proficiency
-
-
- AP Biology -
-
-
- 92% -
-
- AP Chemistry -
-
-
- 68% -
-
- AP Physics -
-
-
- 85% -
-
+
+

Preferred Analogies

+

Sports & Gym

+
+
+

Study Mode

+

Active recall, spaced repetition

+
+

Best Time

+

Morning sessions

+
+
-