Embed Studio - Architecture Document
Overview
The Embed Studio is a dedicated UI mode within the editor for creating, editing, and previewing HTML embed visualizations (D3.js charts). It isolates the dataviz workflow from the article editing flow, providing a focused chat + preview experience similar to the standalone dataviz-agent-space.
Context
The research-article-template uses <HtmlEmbed> components to embed self-contained D3.js charts into articles. These are .html files in app/src/content/embeds/ with strict conventions (scoped CSS, IIFE scripts, ColorPalettes, responsive, etc.) documented in .ai/skills/create-html-embed/directives.md.
In the editor, users need to create and iterate on these charts without leaving the editor. The Embed Studio solves this by providing a dedicated panel with an AI assistant specialized in D3 chart generation.
Storage
Y.Map("embeds")
Embed HTML content is stored in a collaborative Yjs Map, keyed by filename:
Y.Map("embeds") = {
"d3-scaling-chart.html": "<div class='d3-scaling-chart'>...</div>",
"d3-performance.html": "<div class='d3-performance'>...</div>"
}
The ProseMirror node (htmlEmbed) only stores the src attribute as a reference key. The actual HTML lives in the shared Y.Map, enabling real-time collaboration on embed content.
Node attributes
The htmlEmbed TipTap node stores:
| Attribute | Type | Description |
|---|---|---|
src |
string | Filename key into Y.Map("embeds") |
title |
string | Chart title (displayed above) |
desc |
string | Chart description |
wide |
boolean | Wide layout mode |
downloadable |
boolean | Show download button |
height |
number | Last known content height (pixels) |
The height attribute eliminates layout jumps: once a chart reports its height, it is persisted and used as the iframe's initial height on subsequent loads.
UI: Two Modes
1. Inline Preview (article view)
When the user is editing the article, the htmlEmbed NodeView shows a read-only preview:
┌─────────────────────────────────────────┐
│ 📊 Chart Title [Edit] [⋮] │
├─────────────────────────────────────────┤
│ │
│ <iframe preview> │
│ │
└─────────────────────────────────────────┘
- The iframe renders the chart using
srcdocwith the full HTML document (buildDoc wrapper) - Height comes from the stored
heightattribute (default: 400px) - Clicking "Edit" opens the Embed Studio panel
- No code editing in this mode
2. Embed Studio Panel (creation/editing)
A full-width panel (drawer or overlay) opens with a split layout:
┌────────────────────────────┬─────────────────────────────┐
│ Chat (D3 context) │ Live Preview │
│ │ │
│ System prompt includes: │ ┌─────────────────────┐ │
│ - D3 embed directives │ │ │ │
│ - ColorPalettes API │ │ [rendered chart] │ │
│ - Current chart HTML │ │ │ │
│ │ └─────────────────────┘ │
│ User: "make a bar chart │ │
│ showing model sizes" │ Toggle: [Preview] [Code] │
│ │ │
│ AI: Creating chart... │ │
│ │ [Save & Close] │
└────────────────────────────┴─────────────────────────────┘
Key design decisions:
- The chat in this panel has a separate system prompt with D3 directives injected. This avoids bloating the main article chat with 500+ lines of D3 conventions.
- The chat history is per-embed (scoped to the
srckey), so each chart has its own conversation thread. - The live preview uses double-buffered iframes (A/B swap with cross-fade) from the dataviz-agent pattern to avoid flashes on update.
- An optional "Code" toggle shows the raw HTML for power users (future enhancement).
AI Tools
The Embed Studio provides three tools to the AI (following the dataviz-agent pattern):
createEmbed(src, html, title, source)
Create or fully replace the HTML for an embed. Writes to Y.Map("embeds").
patchEmbed(src, search, replace)
Exact string replacement in the current HTML. More efficient than full rewrite for small changes (color tweaks, label updates, data changes). Reads from and writes to Y.Map("embeds").
readEmbed(src)
Read the current HTML content. The AI should call this before patching to verify exact content.
Preview Infrastructure
buildDoc(html, isDark, primaryColor)
Wraps a chart HTML fragment into a complete HTML document with:
- CSS variables for theming (
--primary-color,--text-color,--surface-bg, etc.) data-theme="dark"attribute when in dark mode- ColorPalettes polyfill (provides
window.ColorPalettes.getColors(),.getPrimary(), etc.) - Height reporting script (see below)
- Base styles (box-sizing, font stack, padding)
Height reporting
A script injected by buildDoc() observes the chart container and reports its height to the parent:
// Injected into every chart iframe
new ResizeObserver(entries => {
const height = Math.ceil(entries[0].contentRect.height);
window.parent.postMessage({ type: 'embedResize', height }, '*');
}).observe(document.body);
The NodeView listens for this message and updates the node's height attribute:
window.addEventListener('message', (e) => {
if (e.data?.type === 'embedResize') {
updateAttributes({ height: e.data.height });
}
});
On subsequent renders, the iframe starts at the stored height, eliminating layout jumps.
Preview strategy: srcdoc first
Initial implementation uses <iframe srcdoc="..."> directly. This avoids backend changes and keeps the architecture simple.
If we hit limitations (CSP restrictions, large HTML payloads, script execution issues), we migrate to a server-side preview route (POST /api/preview returning an ID, iframe loads /api/preview/:id), following the dataviz-agent pattern.
Export
Updated export API
The toMdx() function returns an object instead of a plain string:
interface ExportResult {
mdx: string; // The MDX content with frontmatter
embeds: Record<string, string>; // filename -> HTML content
}
The caller is responsible for writing the embed files to app/src/content/embeds/.
MDX output
Each embed in the article exports as:
<HtmlEmbed src="d3-scaling-chart.html" title="Chart Title" desc="Description" />
The HTML files are exported separately from the Y.Map("embeds") contents.
System Prompt for D3 Generation
The Embed Studio injects the D3 directives into the AI's system prompt. The content comes from two sources:
- Tool descriptions (from dataviz-agent): create/patch/read tools with usage guidelines
- Embed conventions (from research-article-template): structure, ColorPalettes, CSS variables, mount guard, D3 CDN loading, legends, controls, tooltips, responsiveness, error handling, accessibility, checklist
These are only injected when the Embed Studio is open, keeping the main article chat lightweight.
Implementation Plan
Phase 1: Core infrastructure
- Create
Y.Map("embeds")inEditor.tsxand pass to the embed store - Create
EmbedStore(similar to FrontmatterStore) with get/set/observe/patch operations - Replace the current atomic
htmlEmbedNodeView with an iframe-based preview - Implement
buildDoc()with CSS variables and ColorPalettes polyfill - Implement height reporting via postMessage
Phase 2: Embed Studio panel
- Create
EmbedStudio.tsx- the split-panel UI (chat + preview) - Create a dedicated chat hook (
useEmbedChat) with D3 system prompt - Implement
createEmbed,patchEmbed,readEmbedAI tools (backend + frontend) - Double-buffered iframe preview (A/B swap)
- Wire "Edit" button on inline NodeView to open the studio
Phase 3: Polish
- Per-embed chat history persistence
- Code view toggle
- Export API update (
toMdxreturns{ mdx, embeds }) - Data file upload support (CSV/JSON stored in Y.Map)
- Screenshot-based validation (Playwright, optional)
References
- dataviz-agent-space - Standalone D3 chart generation agent
- research-article-template embeds skill - Embed authoring conventions
- ChartFrame.jsx - Double-buffered iframe + buildDoc pattern