Dataface Tasks

Embeddable Dashboards in Chat - Inline Preview, Modal Expand, and Save to Repo

IDMCP_ANALYST_AGENT-EMBEDDABLE_DASHBOARDS_IN_CHAT_INLINE_PREVIEW_MODAL_EXPAND_AND_SAVE_TO_REPO
Statuscompleted
Priorityp1
Milestonem2-internal-adoption-design-partners
Ownerdata-ai-engineer-architect
Initiativeai-agent-surfaces
Completed bydave
Completed2026-03-18

Problem

When the AI generates a dashboard in the chat interface, the result is currently just text (YAML content or a description). There's no way to:

  1. See the dashboard inline. The user has to copy YAML, navigate to the editor, paste it, and render — a multi-step process that breaks the conversational flow.

  2. Interact with it in context. Dashboards should be explorable right inside the chat — hover for tooltips, see the full layout, interact with variables — without leaving the conversation.

  3. Save it permanently. The most critical gap: when the AI builds something useful, there's no one-click path to make it a real dashboard. Today the user would have to manually create a dashboard in a project, paste the YAML, commit, and push. This should be a single "Save" button that commits to the git repo and creates the dashboard on dataface.com.

This task builds the rendering, interaction, and persistence layer for dashboards inside the chat interface (companion to the chat-first home page task).

Key constraint: All tool execution must flow through the existing MCP tools (render_dashboard, etc.) — no new copies of skills or tools except for UI-specific presentation logic unique to this interface.

Context

Existing rendering pipeline: - dataface/ai/mcp/tools.py::render_dashboard() — takes YAML content, compiles, executes queries, renders to HTML/SVG. Returns {"success": true, "html": "...", "svg": "...", ...} - dataface/core/render/ — render engine supports html, svg, png, pdf, terminal output formats - dataface/core/compile/ — YAML → normalized document (compiler) - dataface/core/execute/ — query execution against databases

Existing dashboard save flow (in cloud app): - apps/cloud/apps/dashboards/views.py::write_dashboard_yaml() — writes YAML to project_root/dashboards/{slug}.yml - apps/cloud/apps/dashboards/views.py::update_dashboard_cache() — parses YAML, updates Django Dashboard model - apps/cloud/git_service.py::GitService.commit()git add -A && git commit - apps/cloud/apps/dashboards/views.py::dashboard_save() — full save flow: validate, write YAML, update cache, optionally commit

Existing AI tool dispatch: - dataface/ai/tools.py::handle_tool_call() — dispatches tool calls to MCP tool implementations - dataface/ai/tool_schemas.py — canonical tool schema definitions (shared by MCP + OpenAI formats) - apps/cloud/apps/ai/views.py::_execute_tool_sync() — cloud-specific tool executor (currently bespoke; should be unified with canonical dispatch)

Dashboard rendering in the current cloud app: - apps/cloud/templates/dashboards/dashboard_view.html — full-page dashboard render - Rendered SVG is stored as DashboardSnapshot for thumbnails - Vega-Lite charts are interactive (tooltips, zoom) via client-side JS

Design philosophy reminders: - Django-first, server-side rendering - YAML as source of truth (all saves write YAML to git) - Minimal JS — but AI copilot chat and Vega-Lite interactivity are explicitly approved JS use cases

Possible Solutions

When the AI calls render_dashboard, the backend renders the dashboard to SVG. The SVG is embedded directly in the chat message DOM with full interactivity (tooltips, hover effects) — not as a static image. Clicking "Expand" opens an htmx-powered modal with the same SVG at full size. The modal includes a "Save to Project" button.

Chat message with embedded dashboard:

┌─────────────────────────────────────────────────┐
│ 🤖 AI: Here's your revenue dashboard:          │
│                                                 │
│ ┌─────────────────────────────────────────────┐ │
│ │  [Rendered SVG dashboard preview]           │ │
│ │  ┌──────────┐ ┌──────────────────────┐      │ │
│ │  │ $4.2M    │ │ ▄▄██▄▄ Revenue     │      │ │
│ │  │ Revenue  │ │ ▄██████▄ by Month   │      │ │
│ │  └──────────┘ └──────────────────────┘      │ │
│ │                                             │ │
│ │  [🔍 Expand]  [💾 Save Dashboard]           │ │
│ └─────────────────────────────────────────────┘ │
│                                                 │
│ I created a dashboard showing total revenue     │
│ with a monthly trend chart...                   │
└─────────────────────────────────────────────────┘

Expanded modal:

┌─────────────────────────────────────────────────────────┐
│  Revenue Dashboard                          [✕ Close]   │
│ ─────────────────────────────────────────────────────── │
│ │  Full interactive Vega-Lite dashboard                │ │
│ │  (tooltips, zoom, variable selectors work)          │ │
│ │                                                     │ │
│ │  ┌──────────┐ ┌──────────────────────────────┐      │ │
│ │  │ $4.2M    │ │ Interactive line chart        │      │ │
│ │  │ Revenue  │ │ (hover for tooltips)          │      │ │
│ │  └──────────┘ └──────────────────────────────┘      │ │
│ ─────────────────────────────────────────────────────── │
│ │  Save to: [Project Dropdown ▼]  Slug: [revenue-dash] │
│ │  [💾 Save Dashboard]  [📋 Copy YAML]  [📥 Download]  │
│ └─────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘

Pros: SVG embeds are interactive inline (tooltips, hover highlights) — not just static thumbnails. Single render pass (SVG only, no separate HTML render needed). SVG preview loads instantly. Save flow reuses existing write_dashboard_yaml() + update_dashboard_cache() + GitService.commit().

Cons: SVG embeds can be large for complex dashboards (mitigate with viewport clipping/scaling).

Interactivity approach — why not iframes:

Embedded dashboards need hover tooltips and highlight effects inline in the chat. Three approaches were evaluated:

  1. SVG with embedded JS (selected). Our SVGs already contain inline <script> tags with interactivity (tooltips, hover highlights) via chart_interactivity.js. The Cloud app already has executeEmbeddedScripts() in renderer.js that re-executes SVG-embedded scripts after innerHTML insertion. This works for multiple embeds because: - The interactivity JS is IIFE-wrapped — no global function pollution - initializeSvg() scopes event listeners to each individual SVG via document.currentScript.closest('svg') - The tooltip is a shared singleton (window.__datafaceChartHoverState) — one tooltip element reused across all SVGs. This is correct behavior (only one tooltip visible at a time) - Each SVG tracks its init state via data-dft-hover-initialized — prevents double-initialization even if the same SVG is re-inserted

  2. Iframes (rejected). Iframes give perfect isolation but are heavyweight (each creates a separate document/browsing context), cause sizing headaches (dashboard height varies), block seamless scrolling within the chat, and feel clunky for inline chat embeds. ChatGPT and Claude don't use iframes for inline content.

  3. Namespaced JS (unnecessary). We considered namespacing all CSS selectors and JS globals per embed instance (e.g., dft-embed-{id}). After reviewing the code, this isn't needed: the interactivity JS already scopes correctly per SVG, the tooltip is intentionally shared (singleton is the right pattern for "only one tooltip at a time"), and CSS is inline in the SVG (no selector collisions). If we add more complex interactivity later (e.g., variable controls in embeds), we should revisit namespacing — but for hover tooltips, the current architecture handles multiple identical dashboards in one thread correctly.

What ChatGPT/Claude do: Both render inline content (code blocks, images, charts) directly into the DOM — not in iframes. They use CSS containment (contain: content) and scoped selectors. Our approach matches this pattern: SVG embeds are direct DOM children of the chat message, interactive via embedded JS, expanded to a modal for full-screen view.

Option B: Client-rendered iframe embed (Rejected)

Render the dashboard to HTML, serve it from a unique URL, and embed it in the chat via <iframe>. Modal simply enlarges the iframe.

Pros: Perfect CSS/JS isolation. Full interactivity.

Cons: Heavyweight (separate document context per embed), sizing headaches (dashboard height varies), blocks seamless scrolling, feels clunky for inline chat. ChatGPT and Claude don't use iframes for inline content — neither should we.

Option C: Vega-Lite spec embed (render on client)

Pass the Vega-Lite spec directly to the client and render charts in the browser.

Pros: Lightweight data transfer. Full interactivity.

Cons: Only works for single charts, not full dashboards (which have layouts, KPIs, tables, multiple charts). Doesn't use our render pipeline. Would need a parallel client-side renderer.

Plan

Selected approach: Option A — Server-rendered SVG/HTML embed with htmx modal.

Files to Create

  1. apps/cloud/templates/chat/_dashboard_embed.html — Partial template for an embedded dashboard in a chat message. Contains: - Scaled SVG preview (max-width constrained, clickable) - "Expand" button that opens modal via htmx - "Save Dashboard" button - Dashboard title and brief metadata

  2. apps/cloud/templates/chat/_dashboard_modal.html — Modal template loaded via htmx. Contains: - Full interactive HTML dashboard render (Vega-Lite charts, tables, KPIs) - Variable selectors (if dashboard has variables) - Save form: project selector, slug input, description - Copy YAML button, Download HTML button

  3. apps/cloud/apps/chat/views.py (extend from chat-first home task): - dashboard_modal() — htmx endpoint that returns the full rendered dashboard in a modal - save_dashboard_from_chat() — saves the dashboard YAML to a project's git repo

  4. apps/cloud/static/js/chat/dashboard-embed.js — Minimal JS for: - Modal open/close - Vega-Lite chart initialization inside modal - Save form submission (htmx handles the POST)

Files to Modify

  1. apps/cloud/apps/ai/views.py — Extend chat_stream() SSE endpoint to: - When the AI calls render_dashboard, render to both SVG (preview) and HTML (for modal) - Return an SSE event with type dashboard_embed containing the SVG preview and a reference ID for the full render - Cache the full HTML render server-side (keyed by a session + render ID) for modal loading

  2. apps/cloud/apps/ai/service.py — Extend tool result handling to detect render_dashboard results and format them for embedding

  3. apps/cloud/static/js/chat/chat.js (from chat-first home task) — Handle dashboard_embed SSE events: - Insert the SVG preview and action buttons into the chat message - Wire up expand/save button handlers

  4. apps/cloud/urls.py — Add endpoints: - GET /org/<slug>/chat/dashboard/<render_id>/modal/ — returns modal HTML - POST /org/<slug>/chat/dashboard/save/ — saves dashboard to project

  5. apps/cloud/apps/dashboards/views.py — Reuse write_dashboard_yaml(), update_dashboard_cache() from save flow. No duplication — the chat save endpoint calls these same functions.

Implementation Steps

Step 1: Render pipeline for chat embeds (~2 days) - Extend the chat tool executor so that when render_dashboard is called, it runs the full render pipeline and produces: - SVG output with embedded interactivity scripts (tooltips, hover) — same output as dft render - Dashboard YAML (preserved for save flow) - Store rendered SVG + YAML in a lightweight server-side cache (Django cache or in-memory dict with TTL). Keyed by {session_id}:{render_id}. - Return the SVG + render_id in the SSE dashboard_embed event.

Step 2: Inline interactive SVG in chat messages (~1.5 days) - Build _dashboard_embed.html partial template - Handle dashboard_embed SSE event in chat.js: - Insert the SVG into the chat message DOM via innerHTML - Call executeEmbeddedScripts() on the inserted container (reuse from renderer.js) — this activates the tooltip/hover interactivity embedded in the SVG - Add "Expand" and "Save" action buttons below the preview - CSS: constrain SVG to chat width via max-width + overflow: hidden, add subtle border/shadow - The SVG is fully interactive inline — hover tooltips work immediately, no iframe needed

Step 3: Modal expand with full-size interactive SVG (~1.5 days) - Build _dashboard_modal.html template with: - Same SVG content rendered at full width (re-render server-side at larger dimensions, or use CSS scaling) - Variable selectors if the dashboard defines variables (via embedded foreignObject controls) - Close button - Build dashboard_modal() view: retrieve cached SVG by render ID, return modal partial - Wire "Expand" button to htmx: hx-get="/org/<slug>/chat/dashboard/<render_id>/modal/"hx-target="#modal-container"hx-swap="innerHTML" - Call executeEmbeddedScripts() after modal load to activate interactivity - Modal overlay: CSS + minimal JS for open/close (~20 lines)

Step 4: Save dashboard to project (~2 days) - Build save form in modal: project dropdown (user's projects in this org), dashboard slug (auto-generated from title, editable), optional description - Build save_dashboard_from_chat() view: 1. Receive YAML content + project slug + dashboard slug 2. Call dispatch_tool_call("save_dashboard", {"yaml_content": ..., "path": ...}) — the MCP tool handles validation + file write (see dependency below) 3. On success, run Cloud-specific post-save hooks: - Call update_dashboard_cache(project, slug, yaml_content) — updates Django model - Call GitService(project.project_root).commit(f"Add dashboard: {slug}") — commits to git - Generate DashboardSnapshot for thumbnail 4. Return success response with link to the new dashboard view page - Wire "Save" button: htmx POST with form data, show success message with link to dashboard - Handle errors: YAML validation failures, slug conflicts, git errors

Dependency: This step depends on the save_dashboard MCP tool (save-dashboard-mcp-tool-persist-agent-work-to-project.md). The MCP tool provides the universal validate-and-write primitive. This task adds Cloud-specific post-save hooks (Django model, git commit, snapshot) on top. Steps 1-3 can proceed in parallel while the MCP tool is built.

Step 5: Polish and edge cases (~1 day) - Handle re-renders: if the AI iterates on a dashboard, update the existing embed in place - Handle large dashboards: if SVG is too large (>100KB), show a simplified placeholder with "Click to view" - Handle render failures: show error state in the embed with the error message - Copy YAML button in modal (navigator.clipboard API) - Download HTML button (Blob + download link)

Architecture Invariants

No tool duplication. Rendering and saving MUST use the existing MCP tool implementations: - Render: handle_tool_call("render_dashboard", ...)dataface.ai.mcp.tools.render_dashboard() → compile → execute → render - Save: handle_tool_call("save_dashboard", ...)dataface.ai.mcp.tools.save_dashboard() (the MCP tool validates + writes the file). Cloud-specific post-save hooks (update_dashboard_cache, GitService.commit, DashboardSnapshot) layer on top — they are NOT duplicates, they're Cloud-only additions that the MCP tool doesn't need to know about.

htmx for non-streaming UI, JS for the stream. The dashboard embeds follow the same hybrid boundary as the chat: - The SVG preview is inserted by chat.js when it receives a dashboard_embed SSE event (JS island — because it's part of the streaming message flow) - The Expand modal is loaded via htmx: hx-get="/org/<slug>/chat/dashboard/<render_id>/modal/" hx-target="#modal-container" — standard htmx fragment swap, no JS needed - The Save form is htmx: hx-post="/org/<slug>/chat/dashboard/save/" — server validates, saves, returns success/error HTML fragment - Modal open/close is minimal vanilla JS (~20 lines: toggle a .modal-open class, handle Escape key)

The only new code unique to this interface: - dashboard_embed SSE event formatting in the chat stream backend - Server-side render cache (store HTML for modal loading, TTL-based) - _dashboard_embed.html partial (SVG preview + action buttons) - _dashboard_modal.html partial (full dashboard + save form, loaded via htmx) - ~20 lines of modal open/close JS

Non-Goals (for this task)

  • Editing dashboard YAML inside the chat (edit happens in the dashboard editor after save)
  • Collaborative editing (multiple users working on the same chat-generated dashboard)
  • Version history for chat-generated dashboards (git history handles this after save)
  • Sharing dashboards via URL before saving (future: ephemeral preview links)

Implementation Progress

Not yet started.

QA Exploration

  • [ ] QA exploration completed (or N/A for non-UI tasks)

Review Feedback

  • [ ] Review cleared