File size: 9,544 Bytes
561e6f0
 
 
 
20354ec
561e6f0
 
 
 
 
20354ec
561e6f0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
# 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