@agent-native/creative-context 0.5.12 → 0.6.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/dist/actions/list-context-connections.js +1 -1
- package/dist/client/CreativeContextPanel.js +1 -1
- package/dist/connectors/index.d.ts +1 -0
- package/dist/connectors/index.d.ts.map +1 -1
- package/dist/connectors/index.js +1 -0
- package/dist/connectors/index.js.map +1 -1
- package/dist/connectors/rendered-design.d.ts +68 -0
- package/dist/connectors/rendered-design.d.ts.map +1 -0
- package/dist/connectors/rendered-design.js +362 -0
- package/dist/connectors/rendered-design.js.map +1 -0
- package/dist/connectors/rendered-page.d.ts +19 -1
- package/dist/connectors/rendered-page.d.ts.map +1 -1
- package/dist/connectors/rendered-page.js +385 -129
- package/dist/connectors/rendered-page.js.map +1 -1
- package/dist/server/index.d.ts +1 -0
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +1 -0
- package/dist/server/index.js.map +1 -1
- package/package.json +2 -2
- package/src/actions/list-context-connections.ts +1 -1
- package/src/client/CreativeContextPanel.tsx +1 -1
- package/src/connectors/index.ts +9 -0
- package/src/connectors/rendered-design.spec.ts +171 -0
- package/src/connectors/rendered-design.ts +498 -0
- package/src/connectors/rendered-page.spec.ts +94 -0
- package/src/connectors/rendered-page.ts +494 -174
- package/src/connectors/website.spec.ts +27 -71
- package/src/server/index.ts +9 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
2
|
import { isBlockedExtensionUrlWithDns, ssrfSafeFetch, } from "@agent-native/core/extensions/url-safety";
|
|
3
3
|
import { extractStaticWebsiteContext, rankColorSamples, readBoundedResponseBytes, } from "@agent-native/core/ingestion";
|
|
4
4
|
import { normalizeWhitespace } from "./normalize.js";
|
|
@@ -12,75 +12,43 @@ const MAX_TYPOGRAPHY = 100;
|
|
|
12
12
|
const MAX_SPACING = 100;
|
|
13
13
|
const MAX_RADII = 64;
|
|
14
14
|
const MAX_CSS_VARIABLES = 128;
|
|
15
|
+
const MAX_COMPONENT_STYLES = 24;
|
|
16
|
+
const FONT_READY_TIMEOUT_MS = 4_000;
|
|
17
|
+
const BROWSER_NAVIGATION_DISABLED_WARNING = "Browser rendering is disabled until Chromium navigation has a connect-time SSRF guard.";
|
|
15
18
|
export class LayeredRenderedPageProvider {
|
|
16
|
-
#requestBuilderBrowserConnection;
|
|
17
|
-
#loadPlaywright;
|
|
18
|
-
#requestAttachedBrowserConnection;
|
|
19
19
|
#staticFetch;
|
|
20
20
|
constructor(options = {}) {
|
|
21
|
-
this.#requestBuilderBrowserConnection =
|
|
22
|
-
options.requestBuilderBrowserConnection ?? defaultBuilderBrowserRequest;
|
|
23
|
-
this.#loadPlaywright = options.loadPlaywright ?? loadOptionalPlaywright;
|
|
24
|
-
this.#requestAttachedBrowserConnection =
|
|
25
|
-
options.requestAttachedBrowserConnection;
|
|
26
21
|
this.#staticFetch = options.staticFetch ?? ssrfSafeFetch;
|
|
27
22
|
}
|
|
28
23
|
async render(request) {
|
|
29
24
|
await assertPublicBrowserUrl(request.url);
|
|
30
|
-
|
|
31
|
-
const playwright = await this.#loadPlaywright().catch((error) => {
|
|
32
|
-
warnings.push(`Playwright unavailable: ${errorMessage(error)}`);
|
|
33
|
-
return null;
|
|
34
|
-
});
|
|
35
|
-
if (request.preferHosted !== false && playwright) {
|
|
36
|
-
try {
|
|
37
|
-
const connection = await this.#requestBuilderBrowserConnection({
|
|
38
|
-
sessionId: `creative-context-${randomUUID()}`,
|
|
39
|
-
});
|
|
40
|
-
const wsUrl = stringValue(connection.wsUrl);
|
|
41
|
-
if (!wsUrl)
|
|
42
|
-
throw new Error("Builder Browser did not return wsUrl.");
|
|
43
|
-
return await renderWithPlaywright(playwright, request, warnings, "builder-browser", wsUrl);
|
|
44
|
-
}
|
|
45
|
-
catch (error) {
|
|
46
|
-
warnings.push(`Builder Browser unavailable: ${errorMessage(error)}`);
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
if (playwright) {
|
|
50
|
-
try {
|
|
51
|
-
return await renderWithPlaywright(playwright, request, warnings, "local-playwright");
|
|
52
|
-
}
|
|
53
|
-
catch (error) {
|
|
54
|
-
warnings.push(`Local Playwright unavailable: ${errorMessage(error)}`);
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
if (playwright && this.#requestAttachedBrowserConnection) {
|
|
58
|
-
try {
|
|
59
|
-
const connection = await this.#requestAttachedBrowserConnection({
|
|
60
|
-
sessionId: `creative-context-attached-${randomUUID()}`,
|
|
61
|
-
url: request.url,
|
|
62
|
-
});
|
|
63
|
-
if (!connection.wsUrl?.trim()) {
|
|
64
|
-
throw new Error("Approved attached browser did not return wsUrl.");
|
|
65
|
-
}
|
|
66
|
-
return await renderWithPlaywright(playwright, request, warnings, "attached-chrome", connection.wsUrl);
|
|
67
|
-
}
|
|
68
|
-
catch (error) {
|
|
69
|
-
warnings.push(`Attached Chrome unavailable: ${errorMessage(error)}`);
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
else {
|
|
73
|
-
warnings.push("Attached Chrome unavailable: no approved browser connection adapter is configured.");
|
|
74
|
-
}
|
|
75
|
-
return renderStatic(request, warnings, this.#staticFetch);
|
|
25
|
+
return renderStatic(request, [BROWSER_NAVIGATION_DISABLED_WARNING], this.#staticFetch);
|
|
76
26
|
}
|
|
77
27
|
}
|
|
78
|
-
|
|
28
|
+
/**
|
|
29
|
+
* Internal browser renderer retained for a future connect-time-guarded
|
|
30
|
+
* adapter. `LayeredRenderedPageProvider` must not call this for arbitrary
|
|
31
|
+
* caller URLs until Chromium's socket connection is guarded as well.
|
|
32
|
+
*/
|
|
33
|
+
export async function renderWithPlaywright(playwright, request, warnings, method, wsUrl) {
|
|
79
34
|
const browser = wsUrl
|
|
80
35
|
? await playwright.chromium.connectOverCDP(wsUrl)
|
|
81
|
-
: await playwright.chromium
|
|
36
|
+
: await launchChromium(playwright.chromium);
|
|
37
|
+
let isolatedContext;
|
|
82
38
|
try {
|
|
83
|
-
|
|
39
|
+
// A hosted CDP browser can already contain the user's other tabs. Prefer a
|
|
40
|
+
// fresh context so extraction never reads ambient browser state, cookies,
|
|
41
|
+
// or a page left behind by another workflow. Older browser adapters may not
|
|
42
|
+
// expose newContext, so retain the existing-context fallback.
|
|
43
|
+
try {
|
|
44
|
+
isolatedContext = await browser.newContext?.();
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
isolatedContext = undefined;
|
|
48
|
+
}
|
|
49
|
+
const context = isolatedContext ?? browser.contexts()[0];
|
|
50
|
+
if (!context)
|
|
51
|
+
throw new Error("Browser did not provide an isolated context.");
|
|
84
52
|
const page = context.pages()[0] ?? (await context.newPage());
|
|
85
53
|
await installNavigationGuard(page);
|
|
86
54
|
await page.setViewportSize({ width: 1440, height: 900 });
|
|
@@ -88,27 +56,60 @@ async function renderWithPlaywright(playwright, request, warnings, method, wsUrl
|
|
|
88
56
|
timeout: boundedTimeout(request.timeoutMs),
|
|
89
57
|
waitUntil: request.waitUntil ?? "domcontentloaded",
|
|
90
58
|
});
|
|
59
|
+
await page
|
|
60
|
+
.waitForLoadState?.("load", {
|
|
61
|
+
timeout: Math.min(8_000, boundedTimeout(request.timeoutMs)),
|
|
62
|
+
})
|
|
63
|
+
.catch((error) => {
|
|
64
|
+
warnings.push(`Browser load stabilization unavailable: ${errorMessage(error)}`);
|
|
65
|
+
});
|
|
66
|
+
// React hydration, CSS-in-JS insertion, and web fonts commonly finish just
|
|
67
|
+
// after `load`. Give those layers a bounded chance to settle, then capture
|
|
68
|
+
// the computed cascade rather than the server HTML.
|
|
69
|
+
await page
|
|
70
|
+
.waitForLoadState?.("networkidle", { timeout: 4_000 })
|
|
71
|
+
.catch((error) => {
|
|
72
|
+
warnings.push(`Browser network-idle stabilization unavailable: ${errorMessage(error)}`);
|
|
73
|
+
});
|
|
74
|
+
await waitForFontReadiness(page, Math.min(FONT_READY_TIMEOUT_MS, boundedTimeout(request.timeoutMs))).catch((error) => {
|
|
75
|
+
warnings.push(`Browser font readiness unavailable: ${errorMessage(error)}`);
|
|
76
|
+
});
|
|
77
|
+
await new Promise((resolve) => setTimeout(resolve, 150));
|
|
78
|
+
await page.evaluate(dismissConsentOverlays).catch(() => undefined);
|
|
91
79
|
const finalUrl = page.url();
|
|
92
80
|
await assertPublicBrowserUrl(finalUrl);
|
|
93
81
|
const [title, text, desktopScreenshot, extraction] = await Promise.all([
|
|
94
|
-
page.title().catch(() =>
|
|
82
|
+
page.title().catch((error) => {
|
|
83
|
+
warnings.push(`Browser title extraction unavailable: ${errorMessage(error)}`);
|
|
84
|
+
return "";
|
|
85
|
+
}),
|
|
95
86
|
page
|
|
96
87
|
.locator("body")
|
|
97
88
|
.innerText()
|
|
98
|
-
.catch(() =>
|
|
89
|
+
.catch((error) => {
|
|
90
|
+
warnings.push(`Browser text extraction unavailable: ${errorMessage(error)}`);
|
|
91
|
+
return "";
|
|
92
|
+
}),
|
|
99
93
|
page
|
|
100
94
|
.screenshot({ type: "png", fullPage: false })
|
|
101
95
|
.then(boundedScreenshot)
|
|
102
|
-
.catch(() =>
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
96
|
+
.catch((error) => {
|
|
97
|
+
warnings.push(`Desktop screenshot unavailable: ${errorMessage(error)}`);
|
|
98
|
+
return undefined;
|
|
99
|
+
}),
|
|
100
|
+
page.evaluate(captureRenderedWebsiteContext).catch((error) => {
|
|
101
|
+
warnings.push(`Browser style extraction unavailable: ${errorMessage(error)}`);
|
|
102
|
+
return emptyExtraction();
|
|
103
|
+
}),
|
|
106
104
|
]);
|
|
107
105
|
await page.setViewportSize({ width: 390, height: 844 });
|
|
108
106
|
const mobileScreenshot = await page
|
|
109
107
|
.screenshot({ type: "png", fullPage: false })
|
|
110
108
|
.then(boundedScreenshot)
|
|
111
|
-
.catch(() =>
|
|
109
|
+
.catch((error) => {
|
|
110
|
+
warnings.push(`Mobile screenshot unavailable: ${errorMessage(error)}`);
|
|
111
|
+
return undefined;
|
|
112
|
+
});
|
|
112
113
|
const unboundedText = normalizeWhitespace(text);
|
|
113
114
|
const textTruncated = unboundedText.length > MAX_RENDERED_TEXT_CHARS;
|
|
114
115
|
const normalizedText = unboundedText.slice(0, MAX_RENDERED_TEXT_CHARS);
|
|
@@ -173,9 +174,32 @@ async function renderWithPlaywright(playwright, request, warnings, method, wsUrl
|
|
|
173
174
|
};
|
|
174
175
|
}
|
|
175
176
|
finally {
|
|
177
|
+
await isolatedContext?.close?.().catch((error) => {
|
|
178
|
+
warnings.push(`Browser context cleanup unavailable: ${errorMessage(error)}`);
|
|
179
|
+
});
|
|
176
180
|
await browser.close().catch(() => undefined);
|
|
177
181
|
}
|
|
178
182
|
}
|
|
183
|
+
async function waitForFontReadiness(page, timeoutMs) {
|
|
184
|
+
let timeout;
|
|
185
|
+
try {
|
|
186
|
+
await Promise.race([
|
|
187
|
+
page.evaluate(async () => {
|
|
188
|
+
if (document.fonts?.ready)
|
|
189
|
+
await document.fonts.ready;
|
|
190
|
+
}),
|
|
191
|
+
new Promise((_, reject) => {
|
|
192
|
+
timeout = setTimeout(() => {
|
|
193
|
+
reject(new Error(`font readiness timed out after ${timeoutMs}ms`));
|
|
194
|
+
}, timeoutMs);
|
|
195
|
+
}),
|
|
196
|
+
]);
|
|
197
|
+
}
|
|
198
|
+
finally {
|
|
199
|
+
if (timeout !== undefined)
|
|
200
|
+
clearTimeout(timeout);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
179
203
|
async function installNavigationGuard(page) {
|
|
180
204
|
await page.route("**/*", async (route) => {
|
|
181
205
|
const request = route.request();
|
|
@@ -205,6 +229,69 @@ async function installNavigationGuard(page) {
|
|
|
205
229
|
await route.continue();
|
|
206
230
|
});
|
|
207
231
|
}
|
|
232
|
+
const SYSTEM_CHROME_EXECUTABLES = [
|
|
233
|
+
"/usr/bin/google-chrome-stable",
|
|
234
|
+
"/usr/bin/google-chrome",
|
|
235
|
+
"/usr/bin/chromium",
|
|
236
|
+
"/usr/bin/chromium-browser",
|
|
237
|
+
];
|
|
238
|
+
async function launchChromium(chromium) {
|
|
239
|
+
const launchOptions = {
|
|
240
|
+
headless: true,
|
|
241
|
+
args: ["--no-sandbox", "--disable-dev-shm-usage"],
|
|
242
|
+
};
|
|
243
|
+
try {
|
|
244
|
+
return await chromium.launch(launchOptions);
|
|
245
|
+
}
|
|
246
|
+
catch (error) {
|
|
247
|
+
if (!isMissingBrowserError(error))
|
|
248
|
+
throw error;
|
|
249
|
+
for (const executablePath of SYSTEM_CHROME_EXECUTABLES) {
|
|
250
|
+
if (!existsSync(executablePath))
|
|
251
|
+
continue;
|
|
252
|
+
try {
|
|
253
|
+
return await chromium.launch({ ...launchOptions, executablePath });
|
|
254
|
+
}
|
|
255
|
+
catch {
|
|
256
|
+
continue;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
throw error;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
function isMissingBrowserError(error) {
|
|
263
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
264
|
+
return /Executable doesn't exist|playwright install|browser.*not found|chromium.*not found/i.test(message);
|
|
265
|
+
}
|
|
266
|
+
/** Close common consent banners without accepting tracking or changing page data. */
|
|
267
|
+
function dismissConsentOverlays() {
|
|
268
|
+
const selectors = [
|
|
269
|
+
'[aria-label*="reject" i]',
|
|
270
|
+
'[aria-label*="decline" i]',
|
|
271
|
+
'[aria-label*="close" i]',
|
|
272
|
+
'[data-testid*="reject" i]',
|
|
273
|
+
'[data-testid*="decline" i]',
|
|
274
|
+
'[data-testid*="close" i]',
|
|
275
|
+
'button[id*="reject" i]',
|
|
276
|
+
'button[id*="decline" i]',
|
|
277
|
+
'button[class*="reject" i]',
|
|
278
|
+
'button[class*="decline" i]',
|
|
279
|
+
];
|
|
280
|
+
for (const selector of selectors) {
|
|
281
|
+
const element = document.querySelector(selector);
|
|
282
|
+
if (!element)
|
|
283
|
+
continue;
|
|
284
|
+
const rect = element.getBoundingClientRect();
|
|
285
|
+
const style = getComputedStyle(element);
|
|
286
|
+
if (rect.width > 0 &&
|
|
287
|
+
rect.height > 0 &&
|
|
288
|
+
style.visibility !== "hidden" &&
|
|
289
|
+
style.display !== "none") {
|
|
290
|
+
element.click();
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
208
295
|
async function renderStatic(request, warnings, fetcher) {
|
|
209
296
|
const response = await fetcher(request.url, {
|
|
210
297
|
headers: {
|
|
@@ -264,22 +351,6 @@ async function assertPublicBrowserUrl(value) {
|
|
|
264
351
|
throw new Error("SSRF blocked: website resolved to a private/internal host.");
|
|
265
352
|
}
|
|
266
353
|
}
|
|
267
|
-
async function defaultBuilderBrowserRequest(input) {
|
|
268
|
-
const server = (await import("@agent-native/core/server"));
|
|
269
|
-
if (!server.requestBuilderBrowserConnection) {
|
|
270
|
-
throw new Error("@agent-native/core/server does not export requestBuilderBrowserConnection.");
|
|
271
|
-
}
|
|
272
|
-
return server.requestBuilderBrowserConnection(input);
|
|
273
|
-
}
|
|
274
|
-
async function loadOptionalPlaywright() {
|
|
275
|
-
const specifier = "playwright";
|
|
276
|
-
try {
|
|
277
|
-
return (await import(specifier));
|
|
278
|
-
}
|
|
279
|
-
catch {
|
|
280
|
-
return null;
|
|
281
|
-
}
|
|
282
|
-
}
|
|
283
354
|
function captureRenderedWebsiteContext() {
|
|
284
355
|
const MAX_ASSETS = 500;
|
|
285
356
|
const MAX_LINKS = 500;
|
|
@@ -287,15 +358,148 @@ function captureRenderedWebsiteContext() {
|
|
|
287
358
|
const MAX_TYPE_STYLES = 100;
|
|
288
359
|
const MAX_SPACING_VALUES = 100;
|
|
289
360
|
const MAX_RADIUS_VALUES = 64;
|
|
361
|
+
const MAX_SHADOWS = 32;
|
|
362
|
+
const MAX_BACKGROUNDS = 32;
|
|
290
363
|
const MAX_VARIABLES = 128;
|
|
291
364
|
const MAX_TEXT = 2_000_000;
|
|
365
|
+
const MAX_COMPONENT_STYLES = 24;
|
|
292
366
|
const assets = new Map();
|
|
293
367
|
const links = new Set();
|
|
294
368
|
const colors = [];
|
|
295
369
|
const typography = new Map();
|
|
296
370
|
const spacing = new Set();
|
|
297
371
|
const radii = new Set();
|
|
372
|
+
const shadows = new Set();
|
|
373
|
+
const backgrounds = new Set();
|
|
374
|
+
const components = [];
|
|
298
375
|
const cssVariables = {};
|
|
376
|
+
const semanticColors = {};
|
|
377
|
+
const isOpaque = (value) => {
|
|
378
|
+
const normalized = value.trim().toLowerCase();
|
|
379
|
+
if (!normalized || normalized === "transparent")
|
|
380
|
+
return false;
|
|
381
|
+
const functionBody = normalized.match(/^[a-z-]+\((.*)\)$/)?.[1];
|
|
382
|
+
if (!functionBody)
|
|
383
|
+
return true;
|
|
384
|
+
const alphaValue = functionBody.includes("/")
|
|
385
|
+
? functionBody.split("/").at(-1)?.trim()
|
|
386
|
+
: functionBody.split(",").length === 4
|
|
387
|
+
? functionBody.split(",").at(-1)?.trim()
|
|
388
|
+
: undefined;
|
|
389
|
+
if (!alphaValue)
|
|
390
|
+
return true;
|
|
391
|
+
const alpha = alphaValue.endsWith("%")
|
|
392
|
+
? Number.parseFloat(alphaValue) / 100
|
|
393
|
+
: Number.parseFloat(alphaValue);
|
|
394
|
+
return Number.isNaN(alpha) || alpha > 0.02;
|
|
395
|
+
};
|
|
396
|
+
const addColor = (value) => {
|
|
397
|
+
if (colors.length >= MAX_COLOR_VALUES || !isOpaque(value))
|
|
398
|
+
return;
|
|
399
|
+
const normalized = value.trim();
|
|
400
|
+
if (!colors.includes(normalized))
|
|
401
|
+
colors.push(normalized);
|
|
402
|
+
};
|
|
403
|
+
const visible = (element) => {
|
|
404
|
+
const style = getComputedStyle(element);
|
|
405
|
+
const rect = element.getBoundingClientRect();
|
|
406
|
+
return (rect.width > 0 &&
|
|
407
|
+
rect.height > 0 &&
|
|
408
|
+
style.display !== "none" &&
|
|
409
|
+
style.visibility !== "hidden" &&
|
|
410
|
+
Number(style.opacity || 1) > 0.02);
|
|
411
|
+
};
|
|
412
|
+
const firstVisible = (selector) => Array.from(document.querySelectorAll(selector)).find(visible);
|
|
413
|
+
const opaqueValue = (value) => isOpaque(value) ? value.trim() : undefined;
|
|
414
|
+
const recordComputedStyle = (element, role) => {
|
|
415
|
+
const style = getComputedStyle(element);
|
|
416
|
+
const values = [
|
|
417
|
+
style.color,
|
|
418
|
+
style.backgroundColor,
|
|
419
|
+
style.borderTopColor,
|
|
420
|
+
style.borderRightColor,
|
|
421
|
+
style.borderBottomColor,
|
|
422
|
+
style.borderLeftColor,
|
|
423
|
+
];
|
|
424
|
+
values.forEach(addColor);
|
|
425
|
+
if (style.boxShadow && style.boxShadow !== "none") {
|
|
426
|
+
if (shadows.size < MAX_SHADOWS)
|
|
427
|
+
shadows.add(style.boxShadow);
|
|
428
|
+
}
|
|
429
|
+
if (style.backgroundImage && style.backgroundImage !== "none") {
|
|
430
|
+
if (backgrounds.size < MAX_BACKGROUNDS) {
|
|
431
|
+
backgrounds.add(style.backgroundImage);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
for (const value of [
|
|
435
|
+
style.marginTop,
|
|
436
|
+
style.marginRight,
|
|
437
|
+
style.marginBottom,
|
|
438
|
+
style.marginLeft,
|
|
439
|
+
style.paddingTop,
|
|
440
|
+
style.paddingRight,
|
|
441
|
+
style.paddingBottom,
|
|
442
|
+
style.paddingLeft,
|
|
443
|
+
style.gap,
|
|
444
|
+
style.rowGap,
|
|
445
|
+
style.columnGap,
|
|
446
|
+
]) {
|
|
447
|
+
if (spacing.size < MAX_SPACING_VALUES &&
|
|
448
|
+
value &&
|
|
449
|
+
value !== "0px" &&
|
|
450
|
+
value !== "normal") {
|
|
451
|
+
spacing.add(value);
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
for (const value of [
|
|
455
|
+
style.borderTopLeftRadius,
|
|
456
|
+
style.borderTopRightRadius,
|
|
457
|
+
style.borderBottomRightRadius,
|
|
458
|
+
style.borderBottomLeftRadius,
|
|
459
|
+
]) {
|
|
460
|
+
if (radii.size < MAX_RADIUS_VALUES && value && value !== "0px") {
|
|
461
|
+
radii.add(value);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
const type = {
|
|
465
|
+
family: style.fontFamily,
|
|
466
|
+
size: style.fontSize,
|
|
467
|
+
weight: style.fontWeight,
|
|
468
|
+
lineHeight: style.lineHeight,
|
|
469
|
+
letterSpacing: style.letterSpacing,
|
|
470
|
+
};
|
|
471
|
+
if (typography.size < MAX_TYPE_STYLES && type.family) {
|
|
472
|
+
typography.set(JSON.stringify(type), type);
|
|
473
|
+
}
|
|
474
|
+
if (!role || components.length >= MAX_COMPONENT_STYLES)
|
|
475
|
+
return style;
|
|
476
|
+
components.push({
|
|
477
|
+
role,
|
|
478
|
+
fontFamily: style.fontFamily,
|
|
479
|
+
fontSize: style.fontSize,
|
|
480
|
+
fontWeight: style.fontWeight,
|
|
481
|
+
lineHeight: style.lineHeight,
|
|
482
|
+
letterSpacing: style.letterSpacing,
|
|
483
|
+
color: opaqueValue(style.color),
|
|
484
|
+
backgroundColor: opaqueValue(style.backgroundColor),
|
|
485
|
+
backgroundImage: style.backgroundImage !== "none" ? style.backgroundImage : undefined,
|
|
486
|
+
border: style.borderStyle !== "none" && style.borderWidth !== "0px"
|
|
487
|
+
? style.border
|
|
488
|
+
: undefined,
|
|
489
|
+
borderRadius: style.borderTopLeftRadius !== "0px" ? style.borderRadius : undefined,
|
|
490
|
+
boxShadow: style.boxShadow !== "none" ? style.boxShadow : undefined,
|
|
491
|
+
padding: style.padding !== "0px" ? style.padding : undefined,
|
|
492
|
+
gap: style.gap !== "normal" ? style.gap : undefined,
|
|
493
|
+
textTransform: style.textTransform !== "none" ? style.textTransform : undefined,
|
|
494
|
+
});
|
|
495
|
+
return style;
|
|
496
|
+
};
|
|
497
|
+
const styleFor = (selector, role) => {
|
|
498
|
+
const element = firstVisible(selector);
|
|
499
|
+
if (!element)
|
|
500
|
+
return undefined;
|
|
501
|
+
return recordComputedStyle(element, role);
|
|
502
|
+
};
|
|
299
503
|
const addAsset = (raw, kind, role) => {
|
|
300
504
|
if (!raw || assets.size >= MAX_ASSETS)
|
|
301
505
|
return;
|
|
@@ -374,52 +578,67 @@ function captureRenderedWebsiteContext() {
|
|
|
374
578
|
}
|
|
375
579
|
}
|
|
376
580
|
}
|
|
377
|
-
const
|
|
581
|
+
const bodyStyle = document.body
|
|
582
|
+
? recordComputedStyle(document.body)
|
|
583
|
+
: undefined;
|
|
584
|
+
const rootBackground = opaqueValue(rootStyle.backgroundColor);
|
|
585
|
+
const bodyBackground = bodyStyle
|
|
586
|
+
? opaqueValue(bodyStyle.backgroundColor)
|
|
587
|
+
: undefined;
|
|
588
|
+
const headingStyle = styleFor("h1, h2, h3", "heading");
|
|
589
|
+
const textStyle = styleFor("p, li, label, body", "body");
|
|
590
|
+
const buttonStyle = styleFor('button, [role="button"], input[type="submit"], a[class*="button" i], a[class*="cta" i]', "button");
|
|
591
|
+
const linkStyle = styleFor("a[href]", "link");
|
|
592
|
+
const cardStyle = styleFor('article, [class*="card" i], [class*="panel" i], [class*="surface" i], section', "card");
|
|
593
|
+
styleFor("input, textarea, select", "input");
|
|
594
|
+
styleFor("nav, header", "nav");
|
|
595
|
+
styleFor('main, [class*="hero" i]', "hero");
|
|
596
|
+
const setSemantic = (role, value) => {
|
|
597
|
+
if (value)
|
|
598
|
+
semanticColors[role] = value;
|
|
599
|
+
};
|
|
600
|
+
setSemantic("background", bodyBackground ?? rootBackground);
|
|
601
|
+
setSemantic("surface", cardStyle ? opaqueValue(cardStyle.backgroundColor) : undefined);
|
|
602
|
+
setSemantic("text", textStyle ? opaqueValue(textStyle.color) : undefined);
|
|
603
|
+
const mutedElement = firstVisible("small, figcaption, [class*='muted' i]");
|
|
604
|
+
setSemantic("textMuted", mutedElement
|
|
605
|
+
? opaqueValue(getComputedStyle(mutedElement).color)
|
|
606
|
+
: undefined);
|
|
607
|
+
setSemantic("accent", linkStyle
|
|
608
|
+
? opaqueValue(linkStyle.color)
|
|
609
|
+
: buttonStyle
|
|
610
|
+
? opaqueValue(buttonStyle.backgroundColor)
|
|
611
|
+
: undefined);
|
|
612
|
+
setSemantic("primary", buttonStyle
|
|
613
|
+
? (opaqueValue(buttonStyle.backgroundColor) ??
|
|
614
|
+
opaqueValue(buttonStyle.color))
|
|
615
|
+
: colors[0]);
|
|
616
|
+
setSemantic("secondary", semanticColors.surface ?? colors[1] ?? semanticColors.background);
|
|
617
|
+
const layoutElement = firstVisible("main, [role='main'], body > div");
|
|
618
|
+
const layoutStyle = layoutElement
|
|
619
|
+
? getComputedStyle(layoutElement)
|
|
620
|
+
: undefined;
|
|
621
|
+
const layoutRect = layoutElement?.getBoundingClientRect();
|
|
622
|
+
const layout = {
|
|
623
|
+
contentWidth: layoutStyle?.maxWidth && layoutStyle.maxWidth !== "none"
|
|
624
|
+
? layoutStyle.maxWidth
|
|
625
|
+
: layoutRect && layoutRect.width > 0
|
|
626
|
+
? `${Math.round(layoutRect.width)}px`
|
|
627
|
+
: undefined,
|
|
628
|
+
pagePadding: bodyStyle?.padding && bodyStyle.padding !== "0px"
|
|
629
|
+
? bodyStyle.padding
|
|
630
|
+
: undefined,
|
|
631
|
+
sectionGap: firstVisible("section, article") &&
|
|
632
|
+
getComputedStyle(firstVisible("section, article")).gap !== "normal" &&
|
|
633
|
+
getComputedStyle(firstVisible("section, article")).gap !== "0px"
|
|
634
|
+
? getComputedStyle(firstVisible("section, article")).gap
|
|
635
|
+
: undefined,
|
|
636
|
+
};
|
|
637
|
+
const elements = Array.from(document.querySelectorAll("body *"))
|
|
638
|
+
.filter(visible)
|
|
639
|
+
.slice(0, 700);
|
|
378
640
|
for (const element of elements) {
|
|
379
|
-
|
|
380
|
-
if (colors.length < MAX_COLOR_VALUES) {
|
|
381
|
-
colors.push(style.color, style.backgroundColor, style.borderTopColor, style.borderRightColor, style.borderBottomColor, style.borderLeftColor);
|
|
382
|
-
}
|
|
383
|
-
const type = {
|
|
384
|
-
family: style.fontFamily,
|
|
385
|
-
size: style.fontSize,
|
|
386
|
-
weight: style.fontWeight,
|
|
387
|
-
lineHeight: style.lineHeight,
|
|
388
|
-
letterSpacing: style.letterSpacing,
|
|
389
|
-
};
|
|
390
|
-
if (typography.size < MAX_TYPE_STYLES) {
|
|
391
|
-
typography.set(JSON.stringify(type), type);
|
|
392
|
-
}
|
|
393
|
-
for (const value of [
|
|
394
|
-
style.marginTop,
|
|
395
|
-
style.marginRight,
|
|
396
|
-
style.marginBottom,
|
|
397
|
-
style.marginLeft,
|
|
398
|
-
style.paddingTop,
|
|
399
|
-
style.paddingRight,
|
|
400
|
-
style.paddingBottom,
|
|
401
|
-
style.paddingLeft,
|
|
402
|
-
style.gap,
|
|
403
|
-
style.rowGap,
|
|
404
|
-
style.columnGap,
|
|
405
|
-
]) {
|
|
406
|
-
if (spacing.size < MAX_SPACING_VALUES &&
|
|
407
|
-
value &&
|
|
408
|
-
value !== "0px" &&
|
|
409
|
-
value !== "normal") {
|
|
410
|
-
spacing.add(value);
|
|
411
|
-
}
|
|
412
|
-
}
|
|
413
|
-
for (const value of [
|
|
414
|
-
style.borderTopLeftRadius,
|
|
415
|
-
style.borderTopRightRadius,
|
|
416
|
-
style.borderBottomRightRadius,
|
|
417
|
-
style.borderBottomLeftRadius,
|
|
418
|
-
]) {
|
|
419
|
-
if (radii.size < MAX_RADIUS_VALUES && value && value !== "0px") {
|
|
420
|
-
radii.add(value);
|
|
421
|
-
}
|
|
422
|
-
}
|
|
641
|
+
recordComputedStyle(element);
|
|
423
642
|
}
|
|
424
643
|
return {
|
|
425
644
|
title: document.title,
|
|
@@ -432,10 +651,21 @@ function captureRenderedWebsiteContext() {
|
|
|
432
651
|
spacing: [...spacing],
|
|
433
652
|
radii: [...radii],
|
|
434
653
|
cssVariables,
|
|
654
|
+
semanticColors,
|
|
655
|
+
shadows: [...shadows],
|
|
656
|
+
backgrounds: [...backgrounds],
|
|
657
|
+
components,
|
|
658
|
+
layout,
|
|
435
659
|
},
|
|
436
660
|
};
|
|
437
661
|
}
|
|
438
662
|
export function boundWebsiteExtraction(extraction) {
|
|
663
|
+
const boundedComponents = extraction.designTokens.components
|
|
664
|
+
?.slice(0, MAX_COMPONENT_STYLES)
|
|
665
|
+
.map((component) => Object.fromEntries(Object.entries(component).map(([key, value]) => [
|
|
666
|
+
key,
|
|
667
|
+
typeof value === "string" ? value.slice(0, 500) : value,
|
|
668
|
+
])));
|
|
439
669
|
return {
|
|
440
670
|
title: normalizeWhitespace(extraction.title).slice(0, 500),
|
|
441
671
|
text: normalizeWhitespace(extraction.text).slice(0, MAX_RENDERED_TEXT_CHARS),
|
|
@@ -454,6 +684,35 @@ export function boundWebsiteExtraction(extraction) {
|
|
|
454
684
|
cssVariables: Object.fromEntries(Object.entries(extraction.designTokens.cssVariables)
|
|
455
685
|
.slice(0, MAX_CSS_VARIABLES)
|
|
456
686
|
.map(([name, value]) => [name.slice(0, 500), value.slice(0, 4_096)])),
|
|
687
|
+
...(extraction.designTokens.semanticColors
|
|
688
|
+
? {
|
|
689
|
+
semanticColors: Object.fromEntries(Object.entries(extraction.designTokens.semanticColors)
|
|
690
|
+
.filter(([, value]) => typeof value === "string" && value)
|
|
691
|
+
.map(([name, value]) => [name, value.slice(0, 200)])),
|
|
692
|
+
}
|
|
693
|
+
: {}),
|
|
694
|
+
...(extraction.designTokens.shadows
|
|
695
|
+
? {
|
|
696
|
+
shadows: extraction.designTokens.shadows
|
|
697
|
+
.slice(0, 32)
|
|
698
|
+
.map((value) => value.slice(0, 500)),
|
|
699
|
+
}
|
|
700
|
+
: {}),
|
|
701
|
+
...(extraction.designTokens.backgrounds
|
|
702
|
+
? {
|
|
703
|
+
backgrounds: extraction.designTokens.backgrounds
|
|
704
|
+
.slice(0, 32)
|
|
705
|
+
.map((value) => value.slice(0, 500)),
|
|
706
|
+
}
|
|
707
|
+
: {}),
|
|
708
|
+
...(boundedComponents ? { components: boundedComponents } : {}),
|
|
709
|
+
...(extraction.designTokens.layout
|
|
710
|
+
? {
|
|
711
|
+
layout: Object.fromEntries(Object.entries(extraction.designTokens.layout)
|
|
712
|
+
.filter(([, value]) => typeof value === "string" && value)
|
|
713
|
+
.map(([name, value]) => [name, value.slice(0, 200)])),
|
|
714
|
+
}
|
|
715
|
+
: {}),
|
|
457
716
|
},
|
|
458
717
|
};
|
|
459
718
|
}
|
|
@@ -495,9 +754,6 @@ function boundedTimeout(value) {
|
|
|
495
754
|
function boundedScreenshot(value) {
|
|
496
755
|
return value.byteLength <= MAX_SCREENSHOT_BYTES ? value : undefined;
|
|
497
756
|
}
|
|
498
|
-
function stringValue(value) {
|
|
499
|
-
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
|
500
|
-
}
|
|
501
757
|
function errorMessage(error) {
|
|
502
758
|
return error instanceof Error ? error.message : String(error);
|
|
503
759
|
}
|