tfrere's picture
tfrere HF Staff
refactor: rename to Science Article Template Editor, remove MDX export, fix settings drawer
20354ec
# 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](https://huggingface.co/spaces/tfrere/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 `srcdoc` with the full HTML document (buildDoc wrapper)
- Height comes from the stored `height` attribute (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 `src` key), 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:
```js
// 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:
```js
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:
```typescript
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:
```mdx
<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:
1. **Tool descriptions** (from dataviz-agent): create/patch/read tools with usage guidelines
2. **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
1. Create `Y.Map("embeds")` in `Editor.tsx` and pass to the embed store
2. Create `EmbedStore` (similar to FrontmatterStore) with get/set/observe/patch operations
3. Replace the current atomic `htmlEmbed` NodeView with an iframe-based preview
4. Implement `buildDoc()` with CSS variables and ColorPalettes polyfill
5. Implement height reporting via postMessage
### Phase 2: Embed Studio panel
6. Create `EmbedStudio.tsx` - the split-panel UI (chat + preview)
7. Create a dedicated chat hook (`useEmbedChat`) with D3 system prompt
8. Implement `createEmbed`, `patchEmbed`, `readEmbed` AI tools (backend + frontend)
9. Double-buffered iframe preview (A/B swap)
10. Wire "Edit" button on inline NodeView to open the studio
### Phase 3: Polish
11. Per-embed chat history persistence
12. Code view toggle
13. Export API update (`toMdx` returns `{ mdx, embeds }`)
14. Data file upload support (CSV/JSON stored in Y.Map)
15. Screenshot-based validation (Playwright, optional)
## References
- [dataviz-agent-space](../../../dataviz-agent-space/) - Standalone D3 chart generation agent
- [research-article-template embeds skill](../../../research-article-template/.ai/skills/create-html-embed/) - Embed authoring conventions
- [ChartFrame.jsx](../../../dataviz-agent-space/frontend/src/components/ChartFrame.jsx) - Double-buffered iframe + buildDoc pattern