@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.
- package/README.md +41 -10
- package/package.json +1 -1
- 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:
|
|
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
|
-
|
|
181
|
+
// Default: no image, just the structured map + prompt text (cheapest)
|
|
182
|
+
const { elements, prompt, dimensions } = await result.toPrompt();
|
|
182
183
|
```
|
|
183
184
|
|
|
184
|
-
|
|
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
|
-
| `
|
|
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({
|
|
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
|
-
| `
|
|
211
|
-
| `
|
|
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
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
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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
|
|
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
|
|
147
|
-
?
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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
|
},
|