canvas_present
Create or update an artifact on the chat canvas, the side panel that renders rich content next to the conversation. Use this whenever the user asks for a diagram, architecture map, flowchart, sequence diagram, chart, dashboard, widget, visualisation, report, or any self-contained HTML, SVG, JavaScript, or Markdown document they should see rendered, on Canvas. Canvas is the only delivery surface for requested visuals: do not return inline Mermaid/chart blocks, ASCII diagrams, source dumps, or file attachments instead. Use native JSON Render components where possible; for custom HTML/SVG draw actual static elements, never embed Mermaid source or depend on a JavaScript renderer. For HTML/SVG, content must be fully self-contained: inline CSS; no external scripts, stylesheets, fonts, or network calls. HTML renders inside a sandboxed iframe. In the Classic portal, the sandbox exposes window.ankra for live platform data: await window.ankra.getClusterHealth(clusterId) resolves to the read-only health snapshot {cluster_id, health_status, nodes: {total, ready, not_ready}, pods: {total, running, pending, failed, succeeded, crash_loop, high_restart}, problematic_pods: [{name, namespace, issue, restarts}], generated_at}; poll it on an interval of 10 seconds or more for a self-refreshing widget, and render a friendly error state when the call rejects. Omit artifact_id to create a new artifact; the tool result returns the assigned artifact_id. Call again with that artifact_id to update the artifact in place (an open canvas refreshes live) instead of creating a duplicate. Always send the complete artifact content, never a partial diff; to change a json dashboard already on the canvas use canvas_patch instead of resending it. Keep one artifact per deliverable and update it as you iterate. Canvas visual style rule: use Ankra Next (Ink) styling by default unless the user explicitly requests another style. Use quiet neutral surfaces, thin neutral borders, restrained 8px corners, and Inter/system-ui sans-serif text (14px body, readable line-height, modest sentence-case headings); no decorative gradients, glowing borders, emoji headings, saturated blue panels, or nested cards. Use spacing from 4, 8, 16, 32px, one surface per section, and colour only for meaningful warnings/errors or necessary chart distinctions; healthy states stay neutral and must reflect observed data. For dark mode use canvas #0a0a0b, surface #141416, subtle #1c1c1f, text #f4f4f5, muted text #a1a1aa, borders #27272a; for light mode use canvas #fafafa, surface #ffffff, subtle #f4f4f5, text #111113, muted text #5f5f66, borders #e4e4e7. Support both light and dark modes, respecting the portal account theme rather than assuming the operating-system preference. Ankra Next supplies the resolved theme as root data-theme=light or dark, color-scheme, and the active --ink-bg-canvas, --ink-bg-surface, --ink-bg-subtle, --ink-fg-default, --ink-fg-muted, --ink-border-default palette. Use these variables with light-dark(light, dark) fallbacks, or root [data-theme] selectors for other colours. For standalone/downloaded documents default color-scheme to light dark using a low-specificity :where(:root) rule; do not override the portal-selected scheme or theme. Do not rely solely on prefers-color-scheme media queries in embedded previews. Inline the styling; portal stylesheets and fonts are not inherited. Make HTML responsive from a narrow canvas to the full workspace: width:100%, min-width:0, fluid grids that stack on small widths, wrapping text, and viewBox-based SVG diagrams instead of fixed desktop widths. For HTML/SVG present the primary content as static HTML/CSS or SVG: Ankra Next blocks scripts and does not expose window.ankra, so its canvas must not depend on JavaScript or that bridge. Do not claim live updates for a static snapshot; label the observation time when available. Prefer language="json" (a JSON Render spec) for dashboards, charts and diagrams in Ankra Next: native Ink components that follow the account theme and resize; never JSX, HTML, CSS, scripts, Mermaid or markdown fences inside it. content is a JSON-encoded string: {"schema_version":2,"root":"page","elements":{"page":{"type":"Grid","props":{"layout":{...}},"children":["cpu"]},"cpu":{"type":"LineChart","props":{"label":"CPU","binding":{...}}}},"time":{"default":"24h","options":["1h","6h","24h","7d","30d"],"compare":false},"filters":[{"name":"cluster","label":"Cluster","default":""}]}. Every element is {type, props, children?}; one connected tree, ids unique, a letter then letters, digits, - or _ (max 64), no cycles or shared children, at most 128 elements, 12 levels and 32 children per element. time options come from 1h|6h|24h|7d|30d and include default; filters (max 3, each once) are cluster|environment|application. time, filters, binding, layout, annotations and sparkline need schema_version 2; Gauge, Donut and Diagram kind/group work in v1 too. Components (props strict; label max 120, text max 2000, unit max 20; only containers take children): Stack {}; Grid {layout?}; Section {title, description?}; StatGroup {} (1-4 Stat children); Text {text, muted?}; Note {title?, text, tone?:"neutral"|"warn"}; Badge {text, tone?:"neutral"|"warn"|"danger"}; Stat {label, value:string, unit?, delta?, tone?} or {label, binding, unit?, sparkline?:boolean} (sparkline only when bound; it needs a timeseries); Gauge {label, value:number|null (required; null is unknown), unit?, min, max (> min), thresholds?:{warn?, danger?, direction?:"above"|"below"}} or {label, binding, unit?, min, max, thresholds?} (binds a value or timeseries dataset; colour only at or beyond a level); Donut {label, unit?, segments:[{name (unique), value:number>=0|null}] (1-8)} or {label, binding, unit?, label_key?, value_key?} (binds a table dataset; from nine rows it shows the top 7 plus Other); LineChart {label, labels:string[] (2-100), series:[{name, values:(number|null)[]}] (1-4, each matching labels), unit?} or {label, binding, unit?, annotations?:boolean} (annotations marks the cluster's deployments and alerts); BarChart {label, bars:[{label, value:number|null}] (1-24), unit?} or {label, binding, unit?, label_key?, value_key?}; BarList {label, items:[{label, value:number|null, detail?}] (1-20), unit?} or {label, binding, unit?, label_key?, value_key?, limit?:1-20}; Histogram {label, unit?, series:[{key, label, tone?:"neutral"|"warn"|"danger"}] (1-4), buckets:[{start, end, values:{:number|null}}] (1-120, unix seconds; null is not collected), overlay?:{label, unit?, points}, gaps?:[{start, end}]} or {label, binding, unit?} (stacked bars per time bucket; binds application.requests_by_status, application.log_volume or cluster.pod_restarts_by_bucket); Table {label, columns:string[] (1-8), rows:string[][] (max 100, each matching columns)} or {label, binding, unit?}; StatusList {label, items:[{label, state:"ok"|"warn"|"danger"|"unknown", detail?}] (1-50)} or {label, binding, unit?}; Diagram {label, nodes:[{id, label (max 32), detail? (max 64), column 0-3, row 0-7, tone?, kind?:"users"|"ingress"|"service"|"database"|"cache"|"queue"|"storage"|"cluster"|"external", group? (max 32; a labelled boundary around nodes sharing it)}] (1-24, distinct cells), edges:[{from, to, label? (max 40)}] (max 48, unique, between distinct nodes)}. Grid layout: {"columns":12,"items":{"":{"x":0,"y":0,"w":6,"h":4}}}, integers with x 0-11, w 1-12, x+w<=12, h 1-12 (rows of 32px or more), y 0-384; keys are that Grid's own children, no overlaps, max 32 items; an unplaced child renders full width below. binding: {"dataset":"","params":{"cluster":{"$filter":"cluster"}}}; a param is a literal or {"$filter": a declared filter}, and a cluster param is a cluster id. Never put a time range, query or URL in params: the canvas time control sets the window. A bound panel shows its source and observation time and its own loading, empty, stale, unavailable or denied state, never zero or healthy. A live public link does not redraw a bound Gauge, Donut, Histogram or Stat sparkline: those stay as read. No other props, state, actions, events, visibility, repeat, URLs or components. Dashboards: call list_canvas_datasets first and bind every panel a dataset covers instead of inlining numbers you observed; a binding must use a component in that dataset's fits or it is refused. For a single number bind Stat or Gauge to a value dataset (cluster.cpu_utilisation_percent and cluster.memory_utilisation_percent in %, cluster.cpu_cores_in_use, cluster.memory_in_use, cluster.pod_restarts_in_range, deployments.count), never to a table; a Gauge is fit on a "%" dataset, on one with has_capacity, or when it sets max in the dataset's unit, and its unit, if set, is the dataset's; fleet.availability feeds a StatusList; inline data only for facts no dataset has, never invented, healthy stays neutral. Use one Grid with an explicit layout and one panel per question: LineChart for trends (annotations: true for a cluster), Gauge for a single utilisation-like value, Donut for part-to-whole, BarList or BarChart to rank or compare, StatusList for states, Table only for genuine row detail, never a table standing in for a chart. Text is a caption of at most about 140 characters, never an explanatory paragraph; at most one Note, for a genuine caveat. Never write "n/a", "unknown" or placeholder stats: when a dataset is unavailable, omit the panel or bind it anyway so the panel shows that honest state. For visuals beyond the catalog use static HTML/SVG. Code and Download preserve the spec.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| title | Yes | Short human-readable artifact title shown on the canvas header and the transcript card. | |
| content | Yes | Complete artifact content. For html send a full self-contained document. | |
| language | Yes | Content type. html, svg and javascript render as a live preview; the rest display as documents. | |
| artifact_id | No | Omit to create a new artifact. Pass the artifact_id from a previous canvas_present result to update that artifact in place. |