A single-page, dependency-free reference client for the AnswerLayer embed API.
It loads a dashboard manifest, fetches paginated tile data, and shows how a
host application turns the manifest's typed visualization encoding into a
rendered chart.
Everything is in one file (index.html) — inline HTML, CSS, and vanilla JS,
no build step. serve.py is an optional static server that disables caching
so edits show up on reload.
python3 serve.py # http://localhost:5174
python3 serve.py 8080 # custom portPlain python3 -m http.server works too; opening index.html directly as a
file:// URL also works, subject to the API's CORS policy.
Then in the page:
- Set API base URL (e.g.
http://localhost:8000), Dashboard ID, and an API key. - Load Manifest — fetches the dashboard's tile list.
- Fetch Tile Data — fetches each tile's data.
- Fetch Again — repeats the request; the
cache_hitbadge flips totruewhen the backend serves from cache. - Use Unique Filter — injects a one-off filter so the next fetch misses the cache.
Form values persist to localStorage.
The optional Subject org ID field is sent as an X-Subject-Org-ID header
on every request — embed and clustering alike. It is the platform's
row-level tenant scope: the backend reads it off the validated API-key request
(never from the body) and applies it as the saved query's subject filter.
Leave it blank for the org-level view; set it to scope to one of the
customer's end-customers.
The second panel drives the subject-scoped ML proxy at /api/v1/ml/*
(backend/app/api/v1/ml_proxy.py → the internal ML container). It uses the
same API key and Subject org ID as the embed panel. Clustering is
single-tenant: set a Subject org ID to train/read a tenant's model, or leave
it blank for the org-level model.
- Set a Saved query ID (the feature query the model trains on) and
optional config_overrides JSON (e.g.
{"n_clusters": "auto"}). - Train —
POST /cluster/train, then auto-pollsGET /jobs/{job_id}until the job reachessucceeded/failed. On success it fetches results. - Poll Job — resume polling a job id (e.g. after a timeout) without re-training.
- Get Results —
GET /cluster/results: run summary, per-cluster sizes, and per-entity assignments for the current subject scope. - Predict —
POST /cluster/predictwith an input_data JSON array of row objects; scores them against the latest artifact.
Org and subject org are never in the request body for any ML call — they
travel as the X-Org-ID (added by the backend proxy) and X-Subject-Org-ID
headers. The proxy is the trust boundary; both scopes come from the
authenticated context.
| Call | Purpose |
|---|---|
GET /api/v1/dashboards/{id}/manifest |
Tile list with tile_key, the typed visualization encoding, data_url, and pagination defaults |
POST {tile.data_url} |
Tile data; supports filters and cursor pagination |
POST {tile.data_url} with result_handle + pagination.cursor |
Next page of a materialized result (the handle must be one this tile produced) |
POST /api/v1/ml/cluster/train |
Enqueue a clustering job; body is {saved_query_id, config_overrides}. Returns 202 {job_id, status} |
GET /api/v1/ml/jobs/{job_id} |
Poll a training job's lifecycle (queued/running/succeeded/failed) |
GET /api/v1/ml/cluster/results?limit=N |
Latest model's run summary, cluster_sizes, and assignments for the subject scope |
POST /api/v1/ml/cluster/predict |
Score rows against the latest artifact; body is {input_data: [...]} |
Requests authenticate with an X-API-Key header; tenant scope (where
applicable) travels as the X-Subject-Org-ID header.
Small results come back inline. Larger results are materialized server-side
and returned with a result_handle and next_cursor; pass both back to page
through the data.
Each manifest tile carries a typed visualization object whose encoding
maps a chart role to a column name. The demo reads roles straight from it —
no column-name or key-casing guessing:
- chart type —
visualization.chart_type(metric,bar,line,area,donut,table) - x / label field —
encoding.x(axis charts) orencoding.label(donut) - value fields —
encoding.value(metric/donut) orencoding.y(axis charts)
It resolves each role to a column, finds that column's index in the response
columns, and draws a metric, bar, or table preview. If a tile's response
carries encoding_warnings, the demo shows them — the saved query no longer
returns a column the encoding names. A host application is free to ignore the
preview and feed columns/rows into its own charting library.
MIT — see LICENSE.
{ "columns": ["region", "revenue"], "rows": [["west", 1200], ["east", 980]], "row_count": 2, "total_rows": 2, "next_cursor": null, // present when more pages exist "result_handle": null, // present when the result was materialized "cache_hit": false, "execution_time_ms": 41, "computed_at": "2026-05-16T01:00:00Z", "encoding_warnings": [] // non-empty if the encoding references missing columns }