Render UI
render_uiVisualize PDF data by running JavaScript codemode that queries SQL and renders charts, tables, metrics, or dashboards in the MCP App.
Instructions
Run codemode against docs/sql/citation and render the returned visualization as an MCP App. Use for charts, tables, metrics, and small dashboards derived from PDFs after you know the schema. Call describe_collection first, and validate exploratory SQL with execute_code before embedding it here; SQL failures are returned as codemode errors. The codemode sandbox includes the preinstalled "okra-render-ui" module with querySql() plus prebuilt component helpers like TimeSeriesLineChart(), DataTable(), MetricCards(), dashboard(), and chart/table/html specs; keep query code separate from reusable UI helpers, return compact data, not screenshots, and do not rely on CDN scripts such as Chart.js inside html().
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| code | Yes | JavaScript codemode source that queries docs/sql/citation and returns a UI spec for the MCP Apps render_ui widget. Runs in the same isolated V8 codemode environment as execute_code: no network, but docs/sql/citation are available. Before writing SQL, call describe_collection to inspect the document list and schema. For exploratory SQL, validate with execute_code first, then move the working query into render_ui. SQL errors are terminal tool errors, and the renderer only gets the UI payload after codemode succeeds. Preinstalled module: const { querySql, render_ui, dashboard, TimeSeriesLineChart, DataTable, MetricCards, barChart, lineChart, areaChart, scatterPlot, vegaLite, proofCard, table, metric, html } = await import("okra-render-ui.js"); SQLite tables per doc: nodes (id, parent_id, type, label, value, status, page_number, confidence, metadata, sort_order) nodes_fts — FTS5 on label+value (use MATCH) meta (key, value) page_ledger (page_number, status, pass, vendor, attempt, confidence, error) Node data model — the `nodes` table is the extracted document tree: - Each row is one element. `type` is the raw parser type and varies by vendor, so always run SELECT DISTINCT type FROM nodes first (common values: text, table, row, cell, figure, heading). - Tables come in TWO shapes depending on the parser; check with SELECT DISTINCT type: (a) a single `table` node with the whole table serialized in its `value` (e.g. the gemini-vision default), or (b) a nested `table` → `row` → `cell` tree: row.parent_id = table.id, cell.parent_id = row.id, and each `cell.value` holds one cell's text (table/row values are usually empty). For shape (b) read a grid by joining BOTH levels and ordering by sort_order; row/column headers are the first/edge cells positionally, NOT stored on `label` (which is usually NULL). - `value` is always raw text exactly as printed ("88,268", "16 %", "Sept 30, 2025") — never a typed number/date. Cast in SQL for arithmetic, e.g. CAST(REPLACE(REPLACE(value,',',''),'$','') AS REAL). Recommended modular style: keep data access in sql.query()/querySql(), then pass the rows into a prebuilt component helper such as TimeSeriesLineChart(), DataTable(), MetricCards(), dashboard(), barChart(), lineChart(), table(), or metric(). This mirrors product UI components without hardcoding screenshots or CDN chart libraries. Return one of — dashboard()/chart/table/metric helpers render inside Okra's bundled MCP App without external libraries. html() is the flexible escape hatch for fully custom layouts, but external scripts/resources are blocked by the MCP App CSP; do not use CDN libraries such as Chart.js, Plotly, or D3. Use dashboard(), chart helpers, or inline SVG/CSS instead: - html("<h1>...</h1>") or { type: "html", html: "<!doctype html>..." } - TimeSeriesLineChart({ title, labels, series: [{ key, label, data }] }) for PostHog-style time-series data - dashboard({ title, items: [barChart(...), lineChart(...), metric(...), table(...)] }) - barChart({ title, data, x, y, series? }) - lineChart({ title, data, x, y, series? }) - vegaLite({ mark, data: { values }, encoding }) for constrained Vega-Lite-like grouped bars / multi-series lines - proofCard({ claim, page_image, bbox, text, url }) to render citation proof inline - scatterPlot({ title, data, x, y, series? }) - table({ title, rows, columns? }) - metric({ title, metrics: [{ label, value, delta? }] }) - any object/array, which renders as JSON/table fallback Examples: // 1) bar chart of blocks per page async () => { const { querySql, barChart } = await import("okra-render-ui.js"); const [{ id: docId }] = await docs.list(); const rows = await querySql("SELECT page_number, count(*) AS blocks FROM nodes GROUP BY page_number ORDER BY page_number", { docId }); return barChart({ title: "Blocks per page", data: rows, x: "page_number", y: "blocks" }); } // 2) query first, then use a reusable product-style component async () => { const { querySql, TimeSeriesLineChart } = await import("okra-render-ui.js"); const [{ id: docId }] = await docs.list(); const rows = await querySql("SELECT page_number AS day, count(*) AS user_count FROM nodes GROUP BY page_number ORDER BY page_number", { docId }); return TimeSeriesLineChart({ title: "Blocks over pages", labels: rows.map((r) => String(r.day)), series: [{ key: "blocks", label: "Blocks", data: rows.map((r) => Number(r.user_count)) }], }); } // 3) nested table → row → cell grid (only when tables are shape (b); cell.value holds each cell) async () => { const { querySql, DataTable } = await import("okra-render-ui.js"); const [{ id: docId }] = await docs.list(); const rows = await querySql("SELECT r.sort_order AS row, c.sort_order AS col, c.value FROM nodes t JOIN nodes r ON r.parent_id = t.id AND r.type = 'row' JOIN nodes c ON c.parent_id = r.id AND c.type = 'cell' WHERE t.type = 'table' ORDER BY t.id, r.sort_order, c.sort_order LIMIT 100", { docId }); return DataTable({ title: "Table cells", rows }); } // 4) FTS keyword search rendered as custom HTML via html() async () => { const { querySql, html } = await import("okra-render-ui.js"); const [{ id: docId }] = await docs.list(); const rows = await querySql("SELECT n.page_number, substr(n.value,1,200) AS excerpt FROM nodes_fts f JOIN nodes n ON n.id = f.node_id WHERE nodes_fts MATCH 'revenue' LIMIT 10", { docId }); return html("<ul>" + rows.map((r) => "<li>p" + r.page_number + ": " + r.excerpt + "</li>").join("") + "</ul>"); } | |
| title | No | Optional fallback title for the rendered UI. | |
| height | No | Preferred inline widget height in pixels. | |
| document_id | No | Optional primary okraPDF document ID to mount for this visualization. Use after resolve_pdf_url when the user points at a public PDF. | |
| public_doc_ids | No | Optional public document IDs to mount alongside the authenticated user library for multi-document visualizations. |