Packages

Lightweight Elixir library for Google's A2UI protocol. Serve interactive, agent-driven UI surfaces from any BEAM app via declarative JSON over WebSocket — no Phoenix or LiveView required.

Current section

Files

Jump to
ex_a2ui lib a2ui surface.ex
Raw

lib/a2ui/surface.ex

defmodule A2UI.Surface do
@moduledoc """
An A2UI surface — a canvas holding a flat list of components.
A surface is the top-level container for A2UI components. It has a unique `id`,
a list of components (flat adjacency list, not nested tree), an optional
`root_component_id` that identifies the entry point for rendering, and an
optional `data` map for initial data model values.
## Why flat?
A2UI uses a flat adjacency list rather than nested component trees. This makes
surfaces LLM-friendly (flat lists are easier to generate incrementally than
nested trees) and enables efficient streaming updates (add, modify, or remove
individual components without rebuilding the whole structure).
Parent-child relationships are expressed via `children` property on container
components (Card, Row, Column, etc.), which reference other component IDs.
## Examples
%A2UI.Surface{
id: "dashboard",
root_component_id: "main-card",
components: [
%A2UI.Component{id: "title", type: :text, properties: %{...}},
%A2UI.Component{id: "main-card", type: :card, properties: %{
children: ["title"]
}}
]
}
"""
@type t :: %__MODULE__{
id: String.t(),
catalog_id: String.t() | nil,
components: [A2UI.Component.t()],
root_component_id: String.t() | nil,
data: map(),
theme: A2UI.Theme.t() | nil,
send_data_model: boolean() | nil
}
@enforce_keys [:id]
defstruct [
:id,
:catalog_id,
:root_component_id,
:theme,
:send_data_model,
components: [],
data: %{}
]
@doc "Creates a new empty surface."
@spec new(String.t()) :: t()
def new(id) when is_binary(id), do: %__MODULE__{id: id}
@doc "Adds a component to the surface."
@spec add_component(t(), A2UI.Component.t()) :: t()
def add_component(%__MODULE__{} = surface, %A2UI.Component{} = component) do
%{surface | components: surface.components ++ [component]}
end
@doc "Sets the root component ID for rendering."
@spec set_root(t(), String.t()) :: t()
def set_root(%__MODULE__{} = surface, component_id) when is_binary(component_id) do
%{surface | root_component_id: component_id}
end
@doc "Sets a value in the surface's data model."
@spec put_data(t(), String.t(), term()) :: t()
def put_data(%__MODULE__{} = surface, path, value) when is_binary(path) do
%{surface | data: Map.put(surface.data, path, value)}
end
@doc "Finds a component by ID."
@spec get_component(t(), String.t()) :: A2UI.Component.t() | nil
def get_component(%__MODULE__{components: components}, id) do
Enum.find(components, &(&1.id == id))
end
@doc "Returns the number of components in the surface."
@spec component_count(t()) :: non_neg_integer()
def component_count(%__MODULE__{components: components}), do: length(components)
end