@zumer/snapdom-plugins 1.1.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +41 -10
  2. package/package.json +1 -1
  3. package/prompt-export.js +63 -40
package/README.md CHANGED
@@ -172,23 +172,33 @@ snapdom(el, { plugins: [htmlInCanvas()] });
172
172
 
173
173
  ### `prompt-export`
174
174
 
175
- Adds a `toPrompt()` export method that returns an LLM-ready package: an annotated screenshot, a structured element map with bounding boxes, and a pre-formatted text description. Useful for vision-language models, browser-agent pipelines, visual QA, and any workflow that pairs a capture with structured metadata.
175
+ Adds a `toPrompt()` export method that returns an LLM-ready package: a structured element map with bounding boxes, a pre-formatted prompt text, and (optionally) an annotated screenshot. Tuned for vision-language models, browser-agent pipelines, visual QA, and any workflow that pairs a capture with structured metadata.
176
176
 
177
177
  ```js
178
178
  import { promptExport } from '@zumer/snapdom-plugins/prompt-export';
179
179
 
180
180
  const result = await snapdom(el, { plugins: [promptExport()] });
181
- const { image, elements, dimensions, prompt } = await result.toPrompt();
181
+ // Default: no image, just the structured map + prompt text (cheapest)
182
+ const { elements, prompt, dimensions } = await result.toPrompt();
182
183
  ```
183
184
 
184
- The returned object:
185
+ To also include the annotated image (for tasks that truly depend on vision):
186
+
187
+ ```js
188
+ const result = await snapdom(el, {
189
+ plugins: [promptExport({ include: ['image', 'elements', 'prompt'] })]
190
+ });
191
+ const { image, elements, prompt, dimensions } = await result.toPrompt();
192
+ ```
193
+
194
+ The returned object (fields present only if requested via `include`):
185
195
 
186
196
  | Field | Type | Description |
187
197
  |-------|------|-------------|
188
- | `image` | `string` | Data URL of the (optionally annotated) screenshot |
189
- | `elements` | `Array` | One entry per detected element: `{ id, tag, type, text, bbox, attributes }` |
190
- | `dimensions` | `{width, height}` | Image dimensions after `maxImageWidth` scaling |
198
+ | `elements` | `Array` | One entry per detected element: `{ id, tag, type, name, text, bbox, attributes, state?, styles?, covered? }` |
191
199
  | `prompt` | `string` | Pre-formatted text describing interactive + semantic elements |
200
+ | `image` | `string` | Data URL of the (optionally annotated) screenshot — **only when `include` contains `'image'`** |
201
+ | `dimensions` | `{width, height}` | Scaled dimensions (always present) |
192
202
 
193
203
  `elements` is split into two `type`s:
194
204
  - `'interactive'` — buttons, links, inputs, `[role]`/`[tabindex]` targets. These get numbered badges overlaid on the screenshot when `annotate` is on.
@@ -196,9 +206,17 @@ The returned object:
196
206
 
197
207
  Each `bbox` is in pixel coordinates of the returned image (scaled against `maxImageWidth`).
198
208
 
209
+ Each interactive entry also carries:
210
+ - `name` — the computed accessible name (aria-label → labelledby → alt → title → labels[0] → textContent)
211
+ - `state` — runtime state: `{ checked, disabled, focus, open, value, selectedText }` (only keys that apply)
212
+ - `styles` — visually-meaningful computed props filtered to drop defaults
213
+ - `covered: true` when another element is painted on top of the bbox center (an agent won't click through a modal)
214
+
199
215
  ```js
200
216
  // Example — feed a vision-capable LLM
201
- const { image, elements } = await result.toPrompt({ maxImageWidth: 1024 });
217
+ const { image, elements } = await result.toPrompt({
218
+ include: ['image', 'elements', 'prompt']
219
+ });
202
220
 
203
221
  // image is a data URL → pass as image input
204
222
  // elements is JSON → pass as structured context alongside the image
@@ -207,8 +225,11 @@ const { image, elements } = await result.toPrompt({ maxImageWidth: 1024 });
207
225
 
208
226
  | Option | Type | Default | Description |
209
227
  |--------|------|---------|-------------|
210
- | `annotate` | `boolean` | `true` | Overlay numbered badges on interactive elements |
211
- | `imageFormat` | `'png' \| 'jpg' \| 'webp'` | `'png'` | Output image format |
228
+ | `include` | `string[]` | `['elements', 'prompt']` | Fields to return. Add `'image'` for tasks that need vision (chart content, layout QA, canvas). Use `['prompt']` for the cheapest text-only mode. |
229
+ | `annotate` | `boolean` | `true` | Overlay numbered badges on interactive elements (only affects the image when included) |
230
+ | `promptMode` | `'compact' \| 'verbose'` | `'compact'` | Prompt text verbosity. Compact omits coords when badges are on the image. |
231
+ | `includeCoords` | `boolean` | `true` | Include bbox in the prompt text |
232
+ | `imageFormat` | `'png' \| 'jpg' \| 'webp'` | `'png'` | Output image format (only used when `image` is included) |
212
233
  | `imageQuality` | `number` | `0.8` | Quality for lossy formats (0–1) |
213
234
  | `maxImageWidth` | `number` | `1024` | Max width in px; downscales and rescales bboxes if larger |
214
235
  | `interactiveSelector` | `string` | see below | CSS selector for the interactive element set |
@@ -219,7 +240,17 @@ Defaults:
219
240
  - **interactive**: `a[href], button, input, select, textarea, [role="button"|"link"|"tab"|"menuitem"|"checkbox"|"radio"], [tabindex]:not([tabindex="-1"]), summary, [contenteditable="true"]`
220
241
  - **semantic**: `h1–h6, p, li, img[alt], nav, main, article, section, header, footer, label, td, th, figcaption, blockquote, legend`
221
242
 
222
- Both per-call options (`opts.imageFormat`, etc.) and constructor options are supported; per-call wins.
243
+ Both per-call options (`opts.include`, `opts.imageFormat`, etc.) and constructor options are supported; per-call wins.
244
+
245
+ The image is the most expensive part of `toPrompt()` to produce (canvas draw + data-URL serialization), so the default skips it. Add `'image'` to `include` when the task actually uses vision:
246
+
247
+ ```js
248
+ // Vision-dependent task (chart content, layout QA, visual diff)
249
+ await result.toPrompt({ include: ['image', 'elements', 'prompt'] });
250
+
251
+ // Pure structured agent loop (cheapest)
252
+ await result.toPrompt({ include: ['prompt'] });
253
+ ```
223
254
 
224
255
  ---
225
256
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zumer/snapdom-plugins",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "description": "Official plugins for SnapDOM",
5
5
  "type": "module",
6
6
  "exports": {
package/prompt-export.js CHANGED
@@ -19,6 +19,11 @@
19
19
  * @param {Object} [options.labelStyle={}] - Override styles for annotation badges
20
20
  * @param {'compact'|'verbose'} [options.promptMode='compact'] - Prompt text verbosity
21
21
  * @param {boolean} [options.includeCoords=true] - Include bbox in the prompt text
22
+ * @param {string[]} [options.include] - Which fields to return. Default
23
+ * ['elements', 'prompt']. For vision-dependent tasks (chart content, layout QA,
24
+ * canvas) pass ['image', 'elements', 'prompt'] or add 'image' to the array. For
25
+ * text-only agent prompts pass ['prompt'] (cheapest — skips canvas draw entirely).
26
+ * Accepted values: 'image', 'elements', 'prompt'.
22
27
  * @returns {Object} SnapDOM plugin
23
28
  */
24
29
 
@@ -58,6 +63,12 @@ function isDefaultStyleValue(prop, value) {
58
63
  return false;
59
64
  }
60
65
 
66
+ // Default omits 'image'. Benchmarking showed the text + JSON map is enough
67
+ // to answer most UI-inspection questions and uses ~14× fewer tokens. Pass
68
+ // `include: ['image', 'elements', 'prompt']` explicitly when the task truly
69
+ // depends on vision (charts, canvas content, layout QA).
70
+ const DEFAULT_INCLUDE = ['elements', 'prompt'];
71
+
61
72
  export function promptExport(options = {}) {
62
73
  const {
63
74
  annotate = true,
@@ -69,6 +80,7 @@ export function promptExport(options = {}) {
69
80
  labelStyle = {},
70
81
  promptMode = 'compact',
71
82
  includeCoords = true,
83
+ include = DEFAULT_INCLUDE,
72
84
  } = options;
73
85
 
74
86
  return {
@@ -90,13 +102,17 @@ export function promptExport(options = {}) {
90
102
  return {
91
103
  prompt: async (ctx, opts = {}) => {
92
104
  const meta = ctx.__promptMetadata;
105
+ const wantSet = new Set(opts.include || include || DEFAULT_INCLUDE);
106
+ const wantImage = wantSet.has('image');
107
+ const wantElements = wantSet.has('elements');
108
+ const wantPrompt = wantSet.has('prompt');
109
+
93
110
  if (!meta || !meta.elements.length) {
94
- return {
95
- image: ctx.export.url,
96
- elements: [],
97
- dimensions: { width: 0, height: 0 },
98
- prompt: '',
99
- };
111
+ const empty = { dimensions: { width: 0, height: 0 } };
112
+ if (wantImage) empty.image = ctx.export.url;
113
+ if (wantElements) empty.elements = [];
114
+ if (wantPrompt) empty.prompt = '';
115
+ return empty;
100
116
  }
101
117
 
102
118
  const format = opts.imageFormat || imageFormat;
@@ -105,32 +121,40 @@ export function promptExport(options = {}) {
105
121
  const mode = opts.promptMode || promptMode;
106
122
  const withCoords = opts.includeCoords !== undefined ? opts.includeCoords : includeCoords;
107
123
 
108
- const img = new Image();
109
- img.src = ctx.export.url;
110
- await new Promise((res, rej) => {
111
- img.onload = res;
112
- img.onerror = rej;
113
- });
114
-
115
- const ratio = img.naturalWidth > maxWidth ? maxWidth / img.naturalWidth : 1;
116
- const w = Math.round(img.naturalWidth * ratio);
117
- const h = Math.round(img.naturalHeight * ratio);
118
-
119
- const canvas = document.createElement('canvas');
120
- canvas.width = w;
121
- canvas.height = h;
122
- canvas.getContext('2d').drawImage(img, 0, 0, w, h);
123
-
124
- const mime =
125
- format === 'jpg' || format === 'jpeg' ? 'image/jpeg'
126
- : format === 'webp' ? 'image/webp'
127
- : 'image/png';
128
- const dataURL = canvas.toDataURL(mime, quality);
124
+ // Only load + rasterize the SVG when the caller actually wants the
125
+ // image. Skipping saves the img decode + canvas draw + toDataURL —
126
+ // the most expensive steps of this export.
127
+ let w, h, dataURL;
128
+ if (wantImage) {
129
+ const img = new Image();
130
+ img.src = ctx.export.url;
131
+ await new Promise((res, rej) => { img.onload = res; img.onerror = rej; });
132
+ const ratio = img.naturalWidth > maxWidth ? maxWidth / img.naturalWidth : 1;
133
+ w = Math.round(img.naturalWidth * ratio);
134
+ h = Math.round(img.naturalHeight * ratio);
135
+ const canvas = document.createElement('canvas');
136
+ canvas.width = w;
137
+ canvas.height = h;
138
+ canvas.getContext('2d').drawImage(img, 0, 0, w, h);
139
+ const mime =
140
+ format === 'jpg' || format === 'jpeg' ? 'image/jpeg'
141
+ : format === 'webp' ? 'image/webp'
142
+ : 'image/png';
143
+ dataURL = canvas.toDataURL(mime, quality);
144
+ } else {
145
+ // No image — scale bboxes to the same target width the image would
146
+ // have used, so downstream callers can still render the map over
147
+ // a separately-rendered screenshot at the same scale.
148
+ const sourceW = meta.dimensions.width || 1;
149
+ const ratio = sourceW > maxWidth ? maxWidth / sourceW : 1;
150
+ w = Math.round(sourceW * ratio);
151
+ h = Math.round(meta.dimensions.height * ratio);
152
+ }
129
153
 
130
154
  const sx = w / (meta.dimensions.width || 1);
131
155
  const sy = h / (meta.dimensions.height || 1);
132
156
 
133
- const elements = meta.elements.map((el) => ({
157
+ const scaledElements = meta.elements.map((el) => ({
134
158
  ...el,
135
159
  bbox: {
136
160
  x: Math.round(el.bbox.x * sx),
@@ -140,19 +164,18 @@ export function promptExport(options = {}) {
140
164
  },
141
165
  }));
142
166
 
143
- // In compact + annotate, badges already encode positions on the
144
- // image itself, so repeating coords in the prompt text is noise.
145
167
  const emitCoords = mode === 'verbose' ? true : (withCoords && !annotate);
146
- const prompt = mode === 'verbose'
147
- ? formatPromptVerbose(elements, { width: w, height: h }, emitCoords)
148
- : formatPromptCompact(elements, { width: w, height: h }, emitCoords);
149
-
150
- return {
151
- image: dataURL,
152
- elements,
153
- dimensions: { width: w, height: h },
154
- prompt,
155
- };
168
+ const promptText = (wantPrompt)
169
+ ? (mode === 'verbose'
170
+ ? formatPromptVerbose(scaledElements, { width: w, height: h }, emitCoords)
171
+ : formatPromptCompact(scaledElements, { width: w, height: h }, emitCoords))
172
+ : null;
173
+
174
+ const out = { dimensions: { width: w, height: h } };
175
+ if (wantImage) out.image = dataURL;
176
+ if (wantElements) out.elements = scaledElements;
177
+ if (wantPrompt) out.prompt = promptText;
178
+ return out;
156
179
  },
157
180
  };
158
181
  },