The canonical representation of a developer annotation.
An annotation is created in the browser by the widget (POST /annotai/api/annotations), enriched and owned by Annotai.Store, and read by
the AI agent through the tools in Annotai.MCP.
This struct is the single source of truth for an annotation's shape. It is
built from the widget's string-keyed JSON via new/1, and serialized back to
string-keyed JSON at the HTTP and MCP edges through the derived
Jason.Encoder — atom fields such as :status encode as their string names
(e.g. "pending"), which is exactly what the widget reads back.
Timestamps are kept as ISO-8601 strings so they survive the JSON round-trip unchanged.
Summary
Functions
Build an annotation from the widget's string-keyed JSON params.
Return the annotation with each image's base64 "data" stripped, keeping only
metadata (id, mime, width, height).
The valid lifecycle statuses, in order.
Types
A reply-thread entry: %{"role" => _, "content" => _, "at" => _}.
@type status() :: :pending | :acknowledged | :resolved | :dismissed
@type t() :: %Annotai.Annotation{ anchor_frac: map() | nil, anchor_index: non_neg_integer() | nil, bounding_box: map() | nil, comment: String.t() | nil, component: String.t() | nil, css_classes: String.t() | nil, element: String.t() | nil, element_path: String.t() | nil, id: String.t() | nil, images: [map()], inserted_at: String.t() | nil, nearby_text: String.t() | nil, phx_selector: String.t() | nil, point: map() | nil, resolved_at: String.t() | nil, resolved_by: :human | :agent | nil, selected_text: String.t() | nil, source_file: String.t() | nil, source_line: non_neg_integer() | nil, status: status(), thread: [message()], url: String.t() | nil }
Functions
Build an annotation from the widget's string-keyed JSON params.
Unrecognized keys are ignored; status is normalized to an atom (defaulting
to :pending). id and inserted_at are left nil for Annotai.Store.put/1
to fill in. resolved_by/resolved_at are never accepted from the client.
Return the annotation with each image's base64 "data" stripped, keeping only
metadata (id, mime, width, height).
Used at every JSON edge (REST responses, the MCP text block) so heavy image bytes never ride along the annotation list / 2s poll. The bytes are fetched on demand from the image route and embedded as MCP image blocks instead.
@spec statuses() :: [status()]
The valid lifecycle statuses, in order.