@ggui-ai/ui-gen 0.16.0 → 0.18.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/adapters/generation-dispatch.d.ts +6 -3
- package/dist/adapters/generation-dispatch.js +5161 -4312
- package/dist/adapters/generation-dispatch.js.map +1 -1
- package/dist/adapters/index.js +8 -5
- package/dist/adapters/index.js.map +1 -1
- package/dist/advanced/index.d.ts +12 -8
- package/dist/advanced/index.js +4685 -3801
- package/dist/advanced/index.js.map +1 -1
- package/dist/blueprint-validator.js +8 -12
- package/dist/blueprint-validator.js.map +1 -1
- package/dist/boilerplate.d.ts +5 -2
- package/dist/boilerplate.js +120 -13
- package/dist/boilerplate.js.map +1 -1
- package/dist/check/index.d.ts +2 -2
- package/dist/check/index.js +72 -0
- package/dist/check/index.js.map +1 -1
- package/dist/compose.js +1 -1
- package/dist/compose.js.map +1 -1
- package/dist/{create-ui-generator-CCIeQaEe.d.ts → create-ui-generator-C8Ki9Wz1.d.ts} +18 -2
- package/dist/{design-mode-BHEE7flj.d.ts → design-mode-DLufhrNp.d.ts} +0 -6
- package/dist/{dispatch-BA5dJcg-.d.ts → dispatch-D2htzi9h.d.ts} +2 -2
- package/dist/evaluation/axis-checks/index.d.ts +4 -4
- package/dist/evaluation/axis-checks/index.js +347 -13
- package/dist/evaluation/axis-checks/index.js.map +1 -1
- package/dist/evaluation/axis-checks/registry.d.ts +2 -2
- package/dist/evaluation/axis-checks/registry.js +302 -1
- package/dist/evaluation/axis-checks/registry.js.map +1 -1
- package/dist/evaluation/index.d.ts +160 -8
- package/dist/evaluation/index.js +752 -81
- package/dist/evaluation/index.js.map +1 -1
- package/dist/fragments/index.js +1 -1
- package/dist/fragments/index.js.map +1 -1
- package/dist/{generate-8L_bc2iP.d.ts → generate-9IeNT4ZY.d.ts} +1 -1
- package/dist/harness/check/runtime-render/index.d.ts +3 -2
- package/dist/harness/check/runtime-render/index.js +8 -12
- package/dist/harness/check/runtime-render/index.js.map +1 -1
- package/dist/harness/index.d.ts +3 -2
- package/dist/harness/index.js +410 -255
- package/dist/harness/index.js.map +1 -1
- package/dist/harness/prompts.d.ts +2 -2
- package/dist/harness/prompts.js +7 -3
- package/dist/harness/prompts.js.map +1 -1
- package/dist/harness/result-types.d.ts +3 -3
- package/dist/harness/runtime.d.ts +13 -4
- package/dist/harness/runtime.js +131 -20
- package/dist/harness/runtime.js.map +1 -1
- package/dist/harness/types-public.d.ts +13 -2
- package/dist/harness/types-public.js +410 -255
- package/dist/harness/types-public.js.map +1 -1
- package/dist/harness/validator-trace-sink.d.ts +2 -2
- package/dist/index.d.ts +29 -7
- package/dist/index.js +4691 -3805
- package/dist/index.js.map +1 -1
- package/dist/patch.d.ts +3 -2
- package/dist/{prompts-k6m6reqe.d.ts → prompts-YU9piOgs.d.ts} +12 -46
- package/dist/styling-profile-CxfvV48g.d.ts +35 -0
- package/dist/tools/get-primitives-ts.d.ts +1 -1
- package/dist/tools/get-primitives-ts.js +1 -1
- package/dist/tools/get-primitives-ts.js.map +1 -1
- package/dist/{types-public-D24JLJgl.d.ts → types-public-DVSssdc-.d.ts} +39 -3
- package/dist/validation/index.d.ts +1 -1
- package/dist/validation/index.js +1 -1
- package/dist/validation/index.js.map +1 -1
- package/dist/workflows.d.ts +3 -2
- package/package.json +12 -12
- package/src/adapters/generation-dispatch.ts +6 -1
- package/src/boilerplate/free-design-prompt.ts +9 -0
- package/src/boilerplate/generate.ts +8 -1
- package/src/boilerplate/hard-sections.ts +45 -1
- package/src/boilerplate/styling-profile.ts +166 -0
- package/src/boilerplate/system-prompt.ts +22 -8
- package/src/boilerplate/templates/layouts/fullscreen-desktop.tsx.tmpl +5 -3
- package/src/boilerplate/templates/layouts/fullscreen-mobile.tsx.tmpl +3 -3
- package/src/boilerplate/templates/layouts/fullscreen-universal.tsx.tmpl +5 -3
- package/src/check/run-tier0.ts +154 -0
- package/src/contract-context.ts +28 -14
- package/src/create-harness.ts +4 -0
- package/src/create-ui-generator.ts +41 -2
- package/src/design-mode.ts +11 -1
- package/src/design-system-docs.ts +7 -4
- package/src/effort.ts +48 -0
- package/src/evaluation/axis-checks/checks/grid.ts +148 -0
- package/src/evaluation/axis-checks/checks/universal.ts +308 -0
- package/src/evaluation/axis-checks/dispatch.ts +78 -13
- package/src/evaluation/axis-checks/helpers.ts +12 -0
- package/src/evaluation/axis-checks/registry.ts +2 -0
- package/src/evaluation/design-identity.ts +96 -0
- package/src/evaluation/index.ts +17 -0
- package/src/evaluation/llm-evaluator.ts +13 -1
- package/src/evaluation/theme-css.ts +14 -0
- package/src/evaluation/types-public.ts +35 -1
- package/src/evaluation/visual-evaluator.ts +478 -45
- package/src/fragments/render.ts +1 -1
- package/src/harness/check/runtime-render/prepare-mockup.ts +18 -11
- package/src/harness/coding/init-session.ts +1 -0
- package/src/harness/coding/run-coding-turn.ts +6 -14
- package/src/harness/coding/run-eval-round.ts +31 -3
- package/src/harness/runtime.ts +10 -0
- package/src/harness/types-public.ts +11 -0
- package/src/index.ts +2 -0
- package/src/pin-fixtures.ts +98 -0
- package/src/run-check.ts +10 -10
- package/src/tools/get-primitives-ts.ts +1 -1
- package/src/tools.ts +2 -1
- package/src/validation/primitives.ts +1 -1
package/dist/evaluation/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { mkdirSync, writeFileSync, unlinkSync, existsSync, rm, createReadStream, createWriteStream } from 'fs';
|
|
1
|
+
import { readFileSync, readdirSync, statSync, mkdirSync, writeFileSync, unlinkSync, existsSync, rm, createReadStream, createWriteStream } from 'fs';
|
|
2
2
|
import 'fs/promises';
|
|
3
3
|
import { tmpdir } from 'os';
|
|
4
4
|
import { resolve, dirname, join, basename } from 'path';
|
|
@@ -9,9 +9,12 @@ import { fileURLToPath } from 'url';
|
|
|
9
9
|
import { query, createSdkMcpServer, tool } from '@anthropic-ai/claude-agent-sdk';
|
|
10
10
|
import Anthropic from '@anthropic-ai/sdk';
|
|
11
11
|
import { z } from 'zod';
|
|
12
|
-
import {
|
|
12
|
+
import { LUCIDE_ICON_NAMES } from '@ggui-ai/design';
|
|
13
|
+
import { listContractGadgets, HOOK_NAME_RE, STDLIB_GADGETS_PACKAGE, APP_GENERATION_PROFILE_BOUNDS } from '@ggui-ai/protocol';
|
|
14
|
+
import { createHash } from 'crypto';
|
|
13
15
|
import { build } from 'esbuild';
|
|
14
|
-
import { getCssTokens } from '@ggui-ai/design/rendering';
|
|
16
|
+
import { getCssTokens, composeThemeCss, fillFitRule } from '@ggui-ai/design/rendering';
|
|
17
|
+
import { createRequire } from 'module';
|
|
15
18
|
|
|
16
19
|
var __create = Object.create;
|
|
17
20
|
var __defProp = Object.defineProperty;
|
|
@@ -1404,8 +1407,8 @@ var require_streamx = __commonJS({
|
|
|
1404
1407
|
return this;
|
|
1405
1408
|
},
|
|
1406
1409
|
next() {
|
|
1407
|
-
return new Promise(function(
|
|
1408
|
-
promiseResolve =
|
|
1410
|
+
return new Promise(function(resolve3, reject) {
|
|
1411
|
+
promiseResolve = resolve3;
|
|
1409
1412
|
promiseReject = reject;
|
|
1410
1413
|
const data = stream.read();
|
|
1411
1414
|
if (data !== null) ondata(data);
|
|
@@ -1438,11 +1441,11 @@ var require_streamx = __commonJS({
|
|
|
1438
1441
|
}
|
|
1439
1442
|
function destroy(err) {
|
|
1440
1443
|
stream.destroy(err);
|
|
1441
|
-
return new Promise((
|
|
1442
|
-
if (stream._duplexState & DESTROYED) return
|
|
1444
|
+
return new Promise((resolve3, reject) => {
|
|
1445
|
+
if (stream._duplexState & DESTROYED) return resolve3({ value: void 0, done: true });
|
|
1443
1446
|
stream.once("close", function() {
|
|
1444
1447
|
if (err) reject(err);
|
|
1445
|
-
else
|
|
1448
|
+
else resolve3({ value: void 0, done: true });
|
|
1446
1449
|
});
|
|
1447
1450
|
});
|
|
1448
1451
|
}
|
|
@@ -1486,8 +1489,8 @@ var require_streamx = __commonJS({
|
|
|
1486
1489
|
const writes = pending + (ws._duplexState & WRITE_WRITING ? 1 : 0);
|
|
1487
1490
|
if (writes === 0) return Promise.resolve(true);
|
|
1488
1491
|
if (state.drains === null) state.drains = [];
|
|
1489
|
-
return new Promise((
|
|
1490
|
-
state.drains.push({ writes, resolve:
|
|
1492
|
+
return new Promise((resolve3) => {
|
|
1493
|
+
state.drains.push({ writes, resolve: resolve3 });
|
|
1491
1494
|
});
|
|
1492
1495
|
}
|
|
1493
1496
|
write(data) {
|
|
@@ -1592,10 +1595,10 @@ var require_streamx = __commonJS({
|
|
|
1592
1595
|
cb(null);
|
|
1593
1596
|
}
|
|
1594
1597
|
function pipelinePromise(...streams) {
|
|
1595
|
-
return new Promise((
|
|
1598
|
+
return new Promise((resolve3, reject) => {
|
|
1596
1599
|
return pipeline2(...streams, (err) => {
|
|
1597
1600
|
if (err) return reject(err);
|
|
1598
|
-
|
|
1601
|
+
resolve3();
|
|
1599
1602
|
});
|
|
1600
1603
|
});
|
|
1601
1604
|
}
|
|
@@ -2253,16 +2256,16 @@ var require_extract = __commonJS({
|
|
|
2253
2256
|
entryCallback = null;
|
|
2254
2257
|
cb(err);
|
|
2255
2258
|
}
|
|
2256
|
-
function onnext(
|
|
2259
|
+
function onnext(resolve3, reject) {
|
|
2257
2260
|
if (error) {
|
|
2258
2261
|
return reject(error);
|
|
2259
2262
|
}
|
|
2260
2263
|
if (entryStream) {
|
|
2261
|
-
|
|
2264
|
+
resolve3({ value: entryStream, done: false });
|
|
2262
2265
|
entryStream = null;
|
|
2263
2266
|
return;
|
|
2264
2267
|
}
|
|
2265
|
-
promiseResolve =
|
|
2268
|
+
promiseResolve = resolve3;
|
|
2266
2269
|
promiseReject = reject;
|
|
2267
2270
|
consumeCallback(null);
|
|
2268
2271
|
if (extract3._finished && promiseResolve) {
|
|
@@ -2290,11 +2293,11 @@ var require_extract = __commonJS({
|
|
|
2290
2293
|
function destroy(err) {
|
|
2291
2294
|
extract3.destroy(err);
|
|
2292
2295
|
consumeCallback(err);
|
|
2293
|
-
return new Promise((
|
|
2294
|
-
if (extract3.destroyed) return
|
|
2296
|
+
return new Promise((resolve3, reject) => {
|
|
2297
|
+
if (extract3.destroyed) return resolve3({ value: void 0, done: true });
|
|
2295
2298
|
extract3.once("close", function() {
|
|
2296
2299
|
if (err) reject(err);
|
|
2297
|
-
else
|
|
2300
|
+
else resolve3({ value: void 0, done: true });
|
|
2298
2301
|
});
|
|
2299
2302
|
});
|
|
2300
2303
|
}
|
|
@@ -3190,9 +3193,9 @@ var init_helper = __esm({
|
|
|
3190
3193
|
try {
|
|
3191
3194
|
await pipeline(Readable.fromWeb(response.body), (0, import_tar_fs.extract)(destDir));
|
|
3192
3195
|
} catch (error) {
|
|
3193
|
-
await new Promise((
|
|
3196
|
+
await new Promise((resolve3) => {
|
|
3194
3197
|
rm(destDir, { force: true, recursive: true }, () => {
|
|
3195
|
-
|
|
3198
|
+
resolve3();
|
|
3196
3199
|
});
|
|
3197
3200
|
});
|
|
3198
3201
|
throw error;
|
|
@@ -3211,14 +3214,14 @@ var init_lambdafs = __esm({
|
|
|
3211
3214
|
GZIP_EXTENSION_REGEX = /gz$/i;
|
|
3212
3215
|
inflate = (filePath) => {
|
|
3213
3216
|
const output = filePath.includes("swiftshader") ? tmpdir() : join(tmpdir(), basename(filePath).replace(ARCHIVE_EXTENSION_REGEX, ""));
|
|
3214
|
-
return new Promise((
|
|
3217
|
+
return new Promise((resolve3, reject) => {
|
|
3215
3218
|
if (filePath.includes("swiftshader")) {
|
|
3216
3219
|
if (existsSync(`${output}/libGLESv2.so`)) {
|
|
3217
|
-
|
|
3220
|
+
resolve3(output);
|
|
3218
3221
|
return;
|
|
3219
3222
|
}
|
|
3220
3223
|
} else if (existsSync(output)) {
|
|
3221
|
-
|
|
3224
|
+
resolve3(output);
|
|
3222
3225
|
return;
|
|
3223
3226
|
}
|
|
3224
3227
|
const isBrotli = BROTLI_EXTENSION_REGEX.test(filePath);
|
|
@@ -3234,12 +3237,12 @@ var init_lambdafs = __esm({
|
|
|
3234
3237
|
if (isTar) {
|
|
3235
3238
|
target = (0, import_tar_fs2.extract)(output);
|
|
3236
3239
|
target.once("finish", () => {
|
|
3237
|
-
|
|
3240
|
+
resolve3(output);
|
|
3238
3241
|
});
|
|
3239
3242
|
} else {
|
|
3240
3243
|
target = createWriteStream(output, { mode: 448 });
|
|
3241
3244
|
target.once("close", () => {
|
|
3242
|
-
|
|
3245
|
+
resolve3(output);
|
|
3243
3246
|
});
|
|
3244
3247
|
}
|
|
3245
3248
|
target.once("error", handleError);
|
|
@@ -3818,7 +3821,7 @@ function extractRetryAfterSec(e) {
|
|
|
3818
3821
|
return Number.isFinite(parsed) && parsed >= 0 ? Math.floor(parsed) : void 0;
|
|
3819
3822
|
}
|
|
3820
3823
|
function sleep(ms) {
|
|
3821
|
-
return new Promise((
|
|
3824
|
+
return new Promise((resolve3) => setTimeout(resolve3, ms));
|
|
3822
3825
|
}
|
|
3823
3826
|
var MAX_RETRY_ATTEMPTS = 2;
|
|
3824
3827
|
var RETRY_AFTER_CAP_SEC = 15;
|
|
@@ -5555,6 +5558,9 @@ function mkIssue(subcategory, description, fix, result = "fail") {
|
|
|
5555
5558
|
function cap(s) {
|
|
5556
5559
|
return s.charAt(0).toUpperCase() + s.slice(1);
|
|
5557
5560
|
}
|
|
5561
|
+
function axisCheckTraceEnabled() {
|
|
5562
|
+
return process.env["GGUI_AXIS_CHECK_TRACE"] === "1";
|
|
5563
|
+
}
|
|
5558
5564
|
|
|
5559
5565
|
// src/evaluation/axis-checks/checks/universal.ts
|
|
5560
5566
|
var ALL_RENDER_VALUES = [
|
|
@@ -5658,7 +5664,222 @@ function runPropSeedNoResync(input) {
|
|
|
5658
5664
|
}
|
|
5659
5665
|
return issues;
|
|
5660
5666
|
}
|
|
5667
|
+
var KNOWN_ICON_KEYS = new Set(
|
|
5668
|
+
LUCIDE_ICON_NAMES.map((n) => n.replace(/[-_]/g, "").toLowerCase())
|
|
5669
|
+
);
|
|
5670
|
+
function isKnownIconName(name) {
|
|
5671
|
+
return KNOWN_ICON_KEYS.has(name.replace(/[-_]/g, "").toLowerCase());
|
|
5672
|
+
}
|
|
5673
|
+
var ICON_NAME_LITERAL_RX = /<Icon\b[^>]*?\bname\s*=\s*(?:"([^"]*)"|'([^']*)'|\{\s*"([^"]*)"\s*\}|\{\s*'([^']*)'\s*\})/g;
|
|
5674
|
+
function findUnknownIconNames(sourceCode) {
|
|
5675
|
+
const unknown = /* @__PURE__ */ new Set();
|
|
5676
|
+
for (const m of sourceCode.matchAll(ICON_NAME_LITERAL_RX)) {
|
|
5677
|
+
const name = m[1] ?? m[2] ?? m[3] ?? m[4] ?? "";
|
|
5678
|
+
if (name.length === 0) continue;
|
|
5679
|
+
if (/[^\x20-\x7e]/.test(name)) continue;
|
|
5680
|
+
if (!isKnownIconName(name)) unknown.add(name);
|
|
5681
|
+
}
|
|
5682
|
+
return [...unknown];
|
|
5683
|
+
}
|
|
5684
|
+
function runIconNameKnown(input) {
|
|
5685
|
+
if (input.compiledCode === null) return [];
|
|
5686
|
+
const unknown = findUnknownIconNames(input.sourceCode);
|
|
5687
|
+
if (unknown.length === 0) return [];
|
|
5688
|
+
const list = unknown.map((n) => `"${n}"`).join(", ");
|
|
5689
|
+
return [
|
|
5690
|
+
mkIssue(
|
|
5691
|
+
"universal.icon_name_known",
|
|
5692
|
+
`<Icon name=\u2026> uses ${unknown.length} name(s) outside the design system's Lucide subset: ${list} \u2014 an unknown name renders an empty box.`,
|
|
5693
|
+
"Call get_available_icons and pick a listed name (kebab-case), or use an emoji directly."
|
|
5694
|
+
)
|
|
5695
|
+
];
|
|
5696
|
+
}
|
|
5697
|
+
var LADDER_STOP_AS_TEXT_RX = /(?<![\w-])color\s*(?:=\s*\{?\s*|:\s*)["'`]?\s*var\(--ggui-color-primary-(\d{2,3})\s*[,)]/g;
|
|
5698
|
+
var TEXT_INK_STOP_MIN = 300;
|
|
5699
|
+
var TEXT_INK_STOP_MAX = 600;
|
|
5700
|
+
function findLadderStopsAsText(sourceCode) {
|
|
5701
|
+
const found = /* @__PURE__ */ new Set();
|
|
5702
|
+
for (const m of sourceCode.matchAll(LADDER_STOP_AS_TEXT_RX)) {
|
|
5703
|
+
const digits = m[1] ?? "";
|
|
5704
|
+
const stop = Number(digits);
|
|
5705
|
+
if (stop >= TEXT_INK_STOP_MIN && stop <= TEXT_INK_STOP_MAX) found.add(`primary-${digits}`);
|
|
5706
|
+
}
|
|
5707
|
+
return [...found];
|
|
5708
|
+
}
|
|
5709
|
+
function runAccentTextInk(input) {
|
|
5710
|
+
if (input.compiledCode === null) return [];
|
|
5711
|
+
const stops = findLadderStopsAsText(input.sourceCode);
|
|
5712
|
+
if (stops.length === 0) return [];
|
|
5713
|
+
const list = stops.map((s) => `\`${s}\``).join(", ");
|
|
5714
|
+
return [
|
|
5715
|
+
mkIssue(
|
|
5716
|
+
"universal.accent_text_ink",
|
|
5717
|
+
`Text is painted with a brand-ladder fill stop as its color (${list}) \u2014 on a low-saturation brand these stops are mid-grey on the surface and the text fails readability.`,
|
|
5718
|
+
'Accent text (eyebrows, taglines, links, labels, inline arrows) takes var(--ggui-color-link) or <Text tone="emphasized">; body text takes var(--ggui-color-onContainer); keep primary-* stops on fills and borders.'
|
|
5719
|
+
)
|
|
5720
|
+
];
|
|
5721
|
+
}
|
|
5722
|
+
var SCOPED_SURFACE_TAG_RX = /<(Card|Box)\b([^<>]*?)surface\s*=\s*["'](inverted|hero)["']([^<>]*?)>/g;
|
|
5723
|
+
var STYLE_BACKGROUND_RX = /\bstyle\s*=\s*\{\{[^}]*\b(background|backgroundColor|backgroundImage)\s*:/;
|
|
5724
|
+
function findScopedSurfaceBackgrounds(sourceCode) {
|
|
5725
|
+
const hits = [];
|
|
5726
|
+
for (const m of sourceCode.matchAll(SCOPED_SURFACE_TAG_RX)) {
|
|
5727
|
+
const attrs = `${m[2] ?? ""} ${m[4] ?? ""}`;
|
|
5728
|
+
if (STYLE_BACKGROUND_RX.test(attrs)) hits.push([m[1] ?? "", m[3] ?? ""]);
|
|
5729
|
+
}
|
|
5730
|
+
return hits;
|
|
5731
|
+
}
|
|
5732
|
+
function runScopedSurfaceOwnsGround(input) {
|
|
5733
|
+
if (input.compiledCode === null) return [];
|
|
5734
|
+
if (input.designMode === "free") return [];
|
|
5735
|
+
const hits = findScopedSurfaceBackgrounds(input.sourceCode);
|
|
5736
|
+
if (hits.length === 0) return [];
|
|
5737
|
+
const [tag, surface] = hits[0];
|
|
5738
|
+
return [
|
|
5739
|
+
mkIssue(
|
|
5740
|
+
"universal.scoped_surface_owns_ground",
|
|
5741
|
+
`<${tag} surface="${surface}"> sets its own background in style \u2014 a scoped surface owns its ground; repainting it puts the scope's swapped inks over the wrong ground (ink on ink).`,
|
|
5742
|
+
`Remove the background from that ${tag}'s style \u2014 surface="${surface}" paints the ground; pick another surface if a different ground is wanted.`
|
|
5743
|
+
)
|
|
5744
|
+
];
|
|
5745
|
+
}
|
|
5746
|
+
var CAPS_TEXT_RX = /<Text\b(?=[^>]*\bcaps\b)[^>]*>([\s\S]*?)<\/Text>/g;
|
|
5747
|
+
function findInventedCapsLabels(sourceCode) {
|
|
5748
|
+
const out = [];
|
|
5749
|
+
for (const m of sourceCode.matchAll(CAPS_TEXT_RX)) {
|
|
5750
|
+
const inner = (m[1] ?? "").trim();
|
|
5751
|
+
if (inner.length === 0) continue;
|
|
5752
|
+
if (inner.includes("{")) continue;
|
|
5753
|
+
out.push(inner.replace(/\s+/g, " ").slice(0, 60));
|
|
5754
|
+
}
|
|
5755
|
+
return out;
|
|
5756
|
+
}
|
|
5757
|
+
function runCapsLabelInvented(input) {
|
|
5758
|
+
if (input.compiledCode === null) return [];
|
|
5759
|
+
const invented = findInventedCapsLabels(input.sourceCode);
|
|
5760
|
+
if (invented.length === 0) return [];
|
|
5761
|
+
const list = invented.map((t) => `"${t}"`).join(", ");
|
|
5762
|
+
return [
|
|
5763
|
+
mkIssue(
|
|
5764
|
+
"universal.caps_label_invented",
|
|
5765
|
+
`${invented.length} caps label(s) carry invented copy the props do not supply: ${list} \u2014 an eyebrow, kicker or overline the agent cannot change is chatter on every render.`,
|
|
5766
|
+
"Remove the label, or bind its text to a prop (`<Text caps>{props.section}</Text>`); copy comes from the contract, never from the component."
|
|
5767
|
+
)
|
|
5768
|
+
];
|
|
5769
|
+
}
|
|
5770
|
+
var VIEWPORT_UNIT = String.raw`(?:calc\(\s*)?100(?:vh|dvh|svh|lvh)\b(?:[^)"'\x60;\n]*\))?`;
|
|
5771
|
+
var VIEWPORT_STYLE_OBJECT_RX = new RegExp(
|
|
5772
|
+
String.raw`\b(?:minHeight|height|minBlockSize|blockSize)\s*:\s*["'\x60]\s*${VIEWPORT_UNIT}["'\x60]?`,
|
|
5773
|
+
"g"
|
|
5774
|
+
);
|
|
5775
|
+
var VIEWPORT_CSS_RX = new RegExp(
|
|
5776
|
+
String.raw`(?<![\w-])(?:min-)?(?:height|block-size)\s*:\s*${VIEWPORT_UNIT}`,
|
|
5777
|
+
"g"
|
|
5778
|
+
);
|
|
5779
|
+
var VIEWPORT_CLASS_RX = /(?<![\w-])(?:min-)?h-(?:screen|dvh|svh|lvh)\b/g;
|
|
5780
|
+
function findViewportSizing(sourceCode) {
|
|
5781
|
+
const hits = [];
|
|
5782
|
+
for (const rx of [VIEWPORT_STYLE_OBJECT_RX, VIEWPORT_CSS_RX, VIEWPORT_CLASS_RX]) {
|
|
5783
|
+
for (const m of sourceCode.matchAll(rx)) {
|
|
5784
|
+
hits.push({ at: m.index ?? 0, text: m[0].replace(/\s+/g, " ").trim().slice(0, 60) });
|
|
5785
|
+
}
|
|
5786
|
+
}
|
|
5787
|
+
return hits.sort((a, b) => a.at - b.at).map((h) => h.text);
|
|
5788
|
+
}
|
|
5789
|
+
function runViewportSized(input) {
|
|
5790
|
+
if (input.compiledCode === null) return [];
|
|
5791
|
+
const sized = findViewportSizing(input.sourceCode);
|
|
5792
|
+
if (sized.length === 0) return [];
|
|
5793
|
+
const list = sized.map((t) => "`" + t + "`").join(", ");
|
|
5794
|
+
return [
|
|
5795
|
+
mkIssue(
|
|
5796
|
+
"universal.viewport_sized",
|
|
5797
|
+
`${sized.length} element(s) are sized to the viewport: ${list} \u2014 the frame owns the height; a content-sized frame is measured from the content, so a viewport-sized root never shrinks and grows the frame on every re-measure.`,
|
|
5798
|
+
"Remove the viewport height \u2014 size to content, never to the viewport. Under fullscreen the frame stretches your root already (centre inside it with `display: flex; flex-direction: column; justify-content: center`); to fill a parent that has a height use `height: 100%` or `flex: 1`."
|
|
5799
|
+
)
|
|
5800
|
+
];
|
|
5801
|
+
}
|
|
5802
|
+
var TERMINAL_WORDS = /* @__PURE__ */ new Set([
|
|
5803
|
+
"submit",
|
|
5804
|
+
"confirm",
|
|
5805
|
+
"approve",
|
|
5806
|
+
"schedule",
|
|
5807
|
+
"reserve",
|
|
5808
|
+
"book",
|
|
5809
|
+
"pay",
|
|
5810
|
+
"checkout",
|
|
5811
|
+
"purchase",
|
|
5812
|
+
"order"
|
|
5813
|
+
]);
|
|
5814
|
+
var TERMINAL_LABEL_RX = /\b(submit|confirm|approve|schedule|reserve|book|pay|checkout|purchase|place\s+order)\b/i;
|
|
5815
|
+
var GUARD_RX = /\b(disabled|aria-disabled)\s*=/;
|
|
5816
|
+
function nameWords(name) {
|
|
5817
|
+
return name.split(/(?=[A-Z])|[_\-\s]+/).map((w) => w.toLowerCase()).filter((w) => w.length > 0);
|
|
5818
|
+
}
|
|
5819
|
+
function findUnguardedTerminalControls(sourceCode) {
|
|
5820
|
+
if (GUARD_RX.test(sourceCode)) return [];
|
|
5821
|
+
const names = /* @__PURE__ */ new Set();
|
|
5822
|
+
for (const m of sourceCode.matchAll(/useAction(?:<[^>]*>)?\(\s*['"`]([^'"`]+)['"`]/g)) {
|
|
5823
|
+
const name = m[1] ?? "";
|
|
5824
|
+
if (nameWords(name).some((w) => TERMINAL_WORDS.has(w))) names.add(name);
|
|
5825
|
+
}
|
|
5826
|
+
for (const m of sourceCode.matchAll(/>([^<>{}]{1,60})<\/Button>/g)) {
|
|
5827
|
+
const label = (m[1] ?? "").replace(/\s+/g, " ").trim();
|
|
5828
|
+
if (label.length > 0 && TERMINAL_LABEL_RX.test(label)) names.add(label.slice(0, 40));
|
|
5829
|
+
}
|
|
5830
|
+
return [...names];
|
|
5831
|
+
}
|
|
5832
|
+
function runTerminalActionUnguarded(input) {
|
|
5833
|
+
if (input.compiledCode === null) return [];
|
|
5834
|
+
const found = findUnguardedTerminalControls(input.sourceCode);
|
|
5835
|
+
if (found.length === 0) return [];
|
|
5836
|
+
const list = found.map((t) => `"${t}"`).join(", ");
|
|
5837
|
+
return [
|
|
5838
|
+
mkIssue(
|
|
5839
|
+
"universal.terminal_action_unguarded",
|
|
5840
|
+
`${list} reads as an action the user means once, and nothing in this component disables the control after it fires \u2014 a double-click sends it twice.`,
|
|
5841
|
+
'Hold the fired state (`const [submitted, setSubmitted] = useState(false)`), set it in the handler, and pass `disabled={submitted}` to that control with a word that says so ("Submitted"). If the action IS meant to repeat \u2014 sending a message, adding an item \u2014 leave it armed and ignore this.',
|
|
5842
|
+
"warn"
|
|
5843
|
+
)
|
|
5844
|
+
];
|
|
5845
|
+
}
|
|
5661
5846
|
var UNIVERSAL_CHECKS = [
|
|
5847
|
+
{
|
|
5848
|
+
id: "universal.icon_name_known",
|
|
5849
|
+
axis: "render",
|
|
5850
|
+
values: ALL_RENDER_VALUES,
|
|
5851
|
+
run: runIconNameKnown
|
|
5852
|
+
},
|
|
5853
|
+
{
|
|
5854
|
+
id: "universal.caps_label_invented",
|
|
5855
|
+
axis: "render",
|
|
5856
|
+
values: ALL_RENDER_VALUES,
|
|
5857
|
+
run: runCapsLabelInvented
|
|
5858
|
+
},
|
|
5859
|
+
{
|
|
5860
|
+
id: "universal.viewport_sized",
|
|
5861
|
+
axis: "render",
|
|
5862
|
+
values: ALL_RENDER_VALUES,
|
|
5863
|
+
run: runViewportSized
|
|
5864
|
+
},
|
|
5865
|
+
{
|
|
5866
|
+
id: "universal.terminal_action_unguarded",
|
|
5867
|
+
axis: "render",
|
|
5868
|
+
values: ALL_RENDER_VALUES,
|
|
5869
|
+
run: runTerminalActionUnguarded
|
|
5870
|
+
},
|
|
5871
|
+
{
|
|
5872
|
+
id: "universal.accent_text_ink",
|
|
5873
|
+
axis: "render",
|
|
5874
|
+
values: ALL_RENDER_VALUES,
|
|
5875
|
+
run: runAccentTextInk
|
|
5876
|
+
},
|
|
5877
|
+
{
|
|
5878
|
+
id: "universal.scoped_surface_owns_ground",
|
|
5879
|
+
axis: "render",
|
|
5880
|
+
values: ALL_RENDER_VALUES,
|
|
5881
|
+
run: runScopedSurfaceOwnsGround
|
|
5882
|
+
},
|
|
5662
5883
|
{
|
|
5663
5884
|
id: "universal.prop_coverage",
|
|
5664
5885
|
axis: "render",
|
|
@@ -6461,6 +6682,87 @@ var TOOLING_CHECKS = [
|
|
|
6461
6682
|
}
|
|
6462
6683
|
];
|
|
6463
6684
|
|
|
6685
|
+
// src/evaluation/axis-checks/checks/grid.ts
|
|
6686
|
+
var BOARD_PROP_RX = /^(columns?|lanes?|stages?|lists?|buckets?|swimlanes?)$/i;
|
|
6687
|
+
var BOARD_PROMPT_RX = /\b(kanban|board)\b/i;
|
|
6688
|
+
var BOARD_NAMES = ["columns", "lanes", "stages", "lists", "buckets", "swimlanes"];
|
|
6689
|
+
var LAYOUT_TAG_RX = /<(\/?)(Grid|Row|Stack|Card|Box|Container|ScrollArea|div|section)\b([^<>]*?)>/g;
|
|
6690
|
+
var SIDE_BY_SIDE = /* @__PURE__ */ new Set(["Grid", "Row"]);
|
|
6691
|
+
function boardPropNames(contract) {
|
|
6692
|
+
const propsField = contract?.propsSpec;
|
|
6693
|
+
const properties = propsField?.properties ?? {};
|
|
6694
|
+
return Object.entries(properties).filter(([name, p]) => BOARD_PROP_RX.test(name) && (p?.type === "array" || p?.schema?.type === "array")).map(([name]) => name);
|
|
6695
|
+
}
|
|
6696
|
+
function jsxMapPositions(sourceCode, name) {
|
|
6697
|
+
const rx = new RegExp(
|
|
6698
|
+
`\\b${name}(?:\\?\\.)?\\.map\\(\\s*\\(?\\s*\\w+(?:\\s*,\\s*\\w+)?\\s*\\)?\\s*=>\\s*(?:\\(?\\s*<|\\{[^{}]{0,600}?\\breturn\\s*\\(?\\s*<)`,
|
|
6699
|
+
"g"
|
|
6700
|
+
);
|
|
6701
|
+
return [...sourceCode.matchAll(rx)].map((m) => m.index ?? 0);
|
|
6702
|
+
}
|
|
6703
|
+
function enclosingLayoutTag(sourceCode, pos) {
|
|
6704
|
+
const stack = [];
|
|
6705
|
+
for (const m of sourceCode.slice(0, pos).matchAll(LAYOUT_TAG_RX)) {
|
|
6706
|
+
const closing = m[1] === "/";
|
|
6707
|
+
const name = m[2] ?? "";
|
|
6708
|
+
const attrs = m[3] ?? "";
|
|
6709
|
+
if (closing) {
|
|
6710
|
+
if (stack[stack.length - 1] === name) stack.pop();
|
|
6711
|
+
continue;
|
|
6712
|
+
}
|
|
6713
|
+
if (attrs.trimEnd().endsWith("/")) continue;
|
|
6714
|
+
stack.push(name);
|
|
6715
|
+
}
|
|
6716
|
+
return stack[stack.length - 1];
|
|
6717
|
+
}
|
|
6718
|
+
function describeBoardRead(input) {
|
|
6719
|
+
const declared = boardPropNames(input.contract);
|
|
6720
|
+
const names = declared.length > 0 ? declared : BOARD_PROMPT_RX.test(input.originalPrompt) ? [...BOARD_NAMES] : BOARD_NAMES.filter((n) => jsxMapPositions(input.sourceCode, n).length > 0);
|
|
6721
|
+
const perName = names.map((name) => {
|
|
6722
|
+
const positions = jsxMapPositions(input.sourceCode, name);
|
|
6723
|
+
return { name, maps: positions.length, enclosing: positions.map((pos) => enclosingLayoutTag(input.sourceCode, pos) ?? "nothing") };
|
|
6724
|
+
});
|
|
6725
|
+
return { declared, names, perName };
|
|
6726
|
+
}
|
|
6727
|
+
function findStackedBoardColumns(input) {
|
|
6728
|
+
const stacked = [];
|
|
6729
|
+
for (const { name, enclosing } of describeBoardRead(input).perName) {
|
|
6730
|
+
for (const tag of enclosing) if (!SIDE_BY_SIDE.has(tag)) stacked.push([name, tag]);
|
|
6731
|
+
}
|
|
6732
|
+
return stacked;
|
|
6733
|
+
}
|
|
6734
|
+
function runBoardColumnsSideBySide(input) {
|
|
6735
|
+
if (input.compiledCode === null) return [];
|
|
6736
|
+
const standDown = input.designMode === "free";
|
|
6737
|
+
const stacked = standDown ? [] : findStackedBoardColumns(input);
|
|
6738
|
+
if (axisCheckTraceEnabled()) {
|
|
6739
|
+
const line = {
|
|
6740
|
+
...describeBoardRead(input),
|
|
6741
|
+
designMode: input.designMode ?? "constrained",
|
|
6742
|
+
standDown,
|
|
6743
|
+
result: standDown ? "stand-down" : stacked.length > 0 ? "fail" : "pass"
|
|
6744
|
+
};
|
|
6745
|
+
console.log(JSON.stringify({ boardColumnsTrace: line }));
|
|
6746
|
+
}
|
|
6747
|
+
if (stacked.length === 0) return [];
|
|
6748
|
+
const [name, tag] = stacked[0];
|
|
6749
|
+
return [
|
|
6750
|
+
mkIssue(
|
|
6751
|
+
"grid.board_columns_side_by_side",
|
|
6752
|
+
`The board's \`${name}\` are composed under <${tag}> \u2014 a vertical stack of column sections; at md and wider the columns must sit side by side.`,
|
|
6753
|
+
`Wrap the ${name} map in <Grid columns={{ base: 1, md: ${name}.length }} gap="md"> (each column its own <Stack> of cards); never a <Stack> of column sections.`
|
|
6754
|
+
)
|
|
6755
|
+
];
|
|
6756
|
+
}
|
|
6757
|
+
var GRID_CHECKS = [
|
|
6758
|
+
{
|
|
6759
|
+
id: "grid.board_columns_side_by_side",
|
|
6760
|
+
axis: "render",
|
|
6761
|
+
values: ["grid"],
|
|
6762
|
+
run: runBoardColumnsSideBySide
|
|
6763
|
+
}
|
|
6764
|
+
];
|
|
6765
|
+
|
|
6464
6766
|
// src/evaluation/axis-checks/extras.ts
|
|
6465
6767
|
function mkIssue2(subcategory, description, fix, result = "warn") {
|
|
6466
6768
|
return { tier: 0, result, category: "mode", subcategory, description, fix };
|
|
@@ -6600,29 +6902,164 @@ var REGISTRY = [
|
|
|
6600
6902
|
...WRITES_CHECKS,
|
|
6601
6903
|
...STATE_PAYLOAD_CHECKS,
|
|
6602
6904
|
...TOOLING_CHECKS,
|
|
6905
|
+
...GRID_CHECKS,
|
|
6603
6906
|
...EXTRA_CHECKS
|
|
6604
6907
|
];
|
|
6605
|
-
|
|
6606
|
-
// src/evaluation/axis-checks/dispatch.ts
|
|
6607
6908
|
function runAxisChecks(classification, input) {
|
|
6608
|
-
|
|
6609
|
-
const axisInput = {
|
|
6909
|
+
const facts = {
|
|
6610
6910
|
sourceCode: input.sourceCode,
|
|
6611
|
-
compiledCode: input.compiledCode,
|
|
6612
6911
|
...input.contract !== void 0 ? { contract: input.contract } : {},
|
|
6613
6912
|
originalPrompt: input.originalPrompt,
|
|
6614
6913
|
classification,
|
|
6615
6914
|
...input.designMode !== void 0 ? { designMode: input.designMode } : {}
|
|
6616
6915
|
};
|
|
6916
|
+
if (input.compiledCode === null) {
|
|
6917
|
+
traceAxisChecksSkipped(facts, "compiledCode null \u2014 no check ran");
|
|
6918
|
+
return [];
|
|
6919
|
+
}
|
|
6920
|
+
const axisInput = { ...facts, compiledCode: input.compiledCode };
|
|
6921
|
+
return runGatedAxisChecks(
|
|
6922
|
+
REGISTRY.filter((check) => matches(classification.vector, check)),
|
|
6923
|
+
axisInput
|
|
6924
|
+
).issues;
|
|
6925
|
+
}
|
|
6926
|
+
function runGatedAxisChecks(checks, input) {
|
|
6617
6927
|
const issues = [];
|
|
6618
|
-
const firedIds =
|
|
6619
|
-
|
|
6620
|
-
|
|
6621
|
-
if (
|
|
6622
|
-
|
|
6623
|
-
|
|
6928
|
+
const firedIds = [];
|
|
6929
|
+
const seen = /* @__PURE__ */ new Set();
|
|
6930
|
+
for (const check of checks) {
|
|
6931
|
+
if (seen.has(check.id)) continue;
|
|
6932
|
+
seen.add(check.id);
|
|
6933
|
+
firedIds.push(check.id);
|
|
6934
|
+
issues.push(...check.run(input));
|
|
6935
|
+
}
|
|
6936
|
+
if (axisCheckTraceEnabled()) {
|
|
6937
|
+
const line = {
|
|
6938
|
+
...traceFacts(input),
|
|
6939
|
+
matched: [...firedIds],
|
|
6940
|
+
issues: issues.map((i) => ({ id: i.subcategory ?? i.category, result: i.result }))
|
|
6941
|
+
};
|
|
6942
|
+
console.log(JSON.stringify({ axisCheckTrace: line }));
|
|
6624
6943
|
}
|
|
6625
|
-
return issues;
|
|
6944
|
+
return { issues, firedIds };
|
|
6945
|
+
}
|
|
6946
|
+
function traceAxisChecksSkipped(facts, note) {
|
|
6947
|
+
if (!axisCheckTraceEnabled()) return;
|
|
6948
|
+
const line = { ...traceFacts(facts), matched: [], issues: [], note };
|
|
6949
|
+
console.log(JSON.stringify({ axisCheckTrace: line }));
|
|
6950
|
+
}
|
|
6951
|
+
function traceFacts(input) {
|
|
6952
|
+
const propsSpec = input.contract?.propsSpec;
|
|
6953
|
+
return {
|
|
6954
|
+
sourceSha256: createHash("sha256").update(input.sourceCode, "utf8").digest("hex"),
|
|
6955
|
+
originalPrompt: input.originalPrompt,
|
|
6956
|
+
designMode: input.designMode ?? "constrained",
|
|
6957
|
+
classification: input.classification.vector,
|
|
6958
|
+
propsSpecKeys: Object.keys(propsSpec ?? {}),
|
|
6959
|
+
propsSpecPropertyKeys: Object.keys(propsSpec?.properties ?? {})
|
|
6960
|
+
};
|
|
6961
|
+
}
|
|
6962
|
+
var STYLING_PROFILE_TRUNCATION_MARKER = " \u2026[truncated at the profile bound]";
|
|
6963
|
+
var STYLING_PROFILE_JUDGE_FRAME = `The app's operator declared the styling profile below. Judge visual treatment RELATIVE to it \u2014 a dense profile is not "cramped", a flat one is not "unpolished", a sparse one is not "empty", and a directed composition is not missing what its Direction leaves out; completeness, hierarchy, accessibility and correctness are unchanged by it.`;
|
|
6964
|
+
var AESTHETIC_BRIEF_MAX_CHARS = 2e3;
|
|
6965
|
+
function sanitizeProfileMember(text, member) {
|
|
6966
|
+
return sanitizeProfileText(text, APP_GENERATION_PROFILE_BOUNDS[member]);
|
|
6967
|
+
}
|
|
6968
|
+
function sanitizeProfileText(text, bound) {
|
|
6969
|
+
if (typeof text !== "string") return "";
|
|
6970
|
+
const lines = [];
|
|
6971
|
+
let inFence = false;
|
|
6972
|
+
for (const raw of text.split(/\r?\n/)) {
|
|
6973
|
+
const line = raw.replace(/[ \t]+/g, " ").trim();
|
|
6974
|
+
if (line.startsWith("```")) {
|
|
6975
|
+
inFence = !inFence;
|
|
6976
|
+
continue;
|
|
6977
|
+
}
|
|
6978
|
+
if (inFence || line.length === 0 || line.startsWith("#")) continue;
|
|
6979
|
+
lines.push(line);
|
|
6980
|
+
}
|
|
6981
|
+
let out = lines.join("\n").replace(/`/g, "");
|
|
6982
|
+
if (out.length > bound) out = out.slice(0, bound) + STYLING_PROFILE_TRUNCATION_MARKER;
|
|
6983
|
+
return out;
|
|
6984
|
+
}
|
|
6985
|
+
function aestheticRefLabel(profile) {
|
|
6986
|
+
const ref = profile?.aesthetic;
|
|
6987
|
+
if (ref === void 0 || sanitizeProfileText(profile?.aestheticBrief, AESTHETIC_BRIEF_MAX_CHARS).length === 0) return "";
|
|
6988
|
+
return ref.version !== void 0 ? `${ref.id}@${ref.version}` : ref.id;
|
|
6989
|
+
}
|
|
6990
|
+
function sanitizeProfile(profile) {
|
|
6991
|
+
const aestheticRef = aestheticRefLabel(profile);
|
|
6992
|
+
return {
|
|
6993
|
+
styling: sanitizeProfileMember(profile?.styling, "styling"),
|
|
6994
|
+
density: sanitizeProfileMember(profile?.density, "density"),
|
|
6995
|
+
layout: sanitizeProfileMember(profile?.layout, "layout"),
|
|
6996
|
+
direction: sanitizeProfileMember(profile?.direction, "direction"),
|
|
6997
|
+
aesthetic: aestheticRef.length > 0 ? sanitizeProfileText(profile?.aestheticBrief, AESTHETIC_BRIEF_MAX_CHARS) : "",
|
|
6998
|
+
aestheticRef
|
|
6999
|
+
};
|
|
7000
|
+
}
|
|
7001
|
+
function renderProfileQuote(profile) {
|
|
7002
|
+
const p = sanitizeProfile(profile);
|
|
7003
|
+
const parts = [];
|
|
7004
|
+
if (p.styling.length > 0) parts.push(p.styling.split("\n").map((line) => `> ${line}`).join("\n"));
|
|
7005
|
+
if (p.density.length > 0) parts.push(`> **Density**: ${p.density}`);
|
|
7006
|
+
if (p.layout.length > 0) parts.push(`> **Layout**: ${p.layout}`);
|
|
7007
|
+
if (p.direction.length > 0) parts.push(`> **Direction**: ${p.direction}`);
|
|
7008
|
+
if (p.aesthetic.length > 0) {
|
|
7009
|
+
const [first, ...rest] = p.aesthetic.split("\n");
|
|
7010
|
+
parts.push([`> **Aesthetic** (\`${p.aestheticRef}\`): ${first ?? ""}`, ...rest.map((line) => `> ${line}`)].join("\n"));
|
|
7011
|
+
}
|
|
7012
|
+
return parts.join("\n>\n");
|
|
7013
|
+
}
|
|
7014
|
+
function buildStylingProfileJudgeBlock(profile) {
|
|
7015
|
+
const quote = renderProfileQuote(profile);
|
|
7016
|
+
if (quote.length === 0) return "";
|
|
7017
|
+
return ["## Declared styling profile", "", STYLING_PROFILE_JUDGE_FRAME, "", quote].join("\n");
|
|
7018
|
+
}
|
|
7019
|
+
function isDesignRenderingInput(relativePath) {
|
|
7020
|
+
const rel = relativePath.split("\\").join("/");
|
|
7021
|
+
if (rel.startsWith("__tests__/") || rel.includes("/__tests__/")) return false;
|
|
7022
|
+
if (/\.test\.[cm]?[jt]sx?$/.test(rel)) return false;
|
|
7023
|
+
if (/\.stories\.[cm]?[jt]sx?$/.test(rel)) return false;
|
|
7024
|
+
if (rel.endsWith(".md")) return false;
|
|
7025
|
+
return true;
|
|
7026
|
+
}
|
|
7027
|
+
function designTreeSha256(root) {
|
|
7028
|
+
const files = [];
|
|
7029
|
+
const walk = (dir) => {
|
|
7030
|
+
for (const name of readdirSync(dir).sort()) {
|
|
7031
|
+
const full = resolve(dir, name);
|
|
7032
|
+
if (statSync(full).isDirectory()) walk(full);
|
|
7033
|
+
else if (isDesignRenderingInput(full.slice(root.length + 1))) files.push(full);
|
|
7034
|
+
}
|
|
7035
|
+
};
|
|
7036
|
+
walk(root);
|
|
7037
|
+
const h = createHash("sha256");
|
|
7038
|
+
for (const f of files) {
|
|
7039
|
+
h.update(f.slice(root.length + 1) + "\n");
|
|
7040
|
+
h.update(readFileSync(f));
|
|
7041
|
+
h.update("\n");
|
|
7042
|
+
}
|
|
7043
|
+
return h.digest("hex");
|
|
7044
|
+
}
|
|
7045
|
+
var identityCache = /* @__PURE__ */ new Map();
|
|
7046
|
+
function judgeDesignIdentity(src) {
|
|
7047
|
+
const key = resolve(src);
|
|
7048
|
+
const cached = identityCache.get(key);
|
|
7049
|
+
if (cached !== void 0) return cached;
|
|
7050
|
+
let identity;
|
|
7051
|
+
try {
|
|
7052
|
+
identity = { src: key, srcSha256: designTreeSha256(key) };
|
|
7053
|
+
} catch (e) {
|
|
7054
|
+
const reason = e instanceof Error ? e.message : String(e);
|
|
7055
|
+
console.warn(`[visual-eval] design tree unreadable at ${key} \u2014 the judgement carries no design identity (${reason})`);
|
|
7056
|
+
identity = null;
|
|
7057
|
+
}
|
|
7058
|
+
identityCache.set(key, identity);
|
|
7059
|
+
return identity;
|
|
7060
|
+
}
|
|
7061
|
+
function resetJudgeDesignIdentityCache() {
|
|
7062
|
+
identityCache.clear();
|
|
6626
7063
|
}
|
|
6627
7064
|
|
|
6628
7065
|
// src/design-mode.ts
|
|
@@ -6640,6 +7077,9 @@ var CANVAS_VIEWPORTS = {
|
|
|
6640
7077
|
lg: { width: 1024, height: 768 },
|
|
6641
7078
|
xl: { width: 1440, height: 900 }
|
|
6642
7079
|
};
|
|
7080
|
+
function displayModeForCanvas(canvas) {
|
|
7081
|
+
return canvas === "xs-chat-card" ? "inline" : "fullscreen";
|
|
7082
|
+
}
|
|
6643
7083
|
function canvasDisplayModeForShell(shell) {
|
|
6644
7084
|
return shell === "chat" || shell === "inline" ? "inline" : "fullscreen";
|
|
6645
7085
|
}
|
|
@@ -6698,24 +7138,76 @@ var CANVAS_DESCRIPTORS = {
|
|
|
6698
7138
|
};
|
|
6699
7139
|
function describeCanvas(canvas) {
|
|
6700
7140
|
const d = CANVAS_DESCRIPTORS[canvas];
|
|
6701
|
-
return `This component renders on the \`${canvas}\` canvas (${d.label}): ${d.minWidthPx}\u2013${d.maxWidthPx}px wide, ${d.host}. Design for the whole range \u2014 the host may size the iframe anywhere inside it.`;
|
|
7141
|
+
return `This component renders on the \`${canvas}\` canvas (${d.label}): ${d.minWidthPx}\u2013${d.maxWidthPx}px wide, ${d.host}. Design for the whole range \u2014 the host may size the iframe anywhere inside it, and the host owns the height: size to your content, never to the viewport.`;
|
|
6702
7142
|
}
|
|
6703
7143
|
|
|
6704
7144
|
// src/evaluation/visual-evaluator.ts
|
|
7145
|
+
function canvasFitPolicy(canvas) {
|
|
7146
|
+
switch (canvas) {
|
|
7147
|
+
case "xs-chat-card":
|
|
7148
|
+
return { capture: "viewport", overflow: "fail" };
|
|
7149
|
+
case "mobile-fullscreen-small":
|
|
7150
|
+
return { capture: "full-page", overflow: "warn" };
|
|
7151
|
+
default:
|
|
7152
|
+
return { capture: "full-page", overflow: "none" };
|
|
7153
|
+
}
|
|
7154
|
+
}
|
|
7155
|
+
var JUDGE_SCOPE_CLASS = "ggui-judge-scope";
|
|
7156
|
+
function canvasFit(canvas) {
|
|
7157
|
+
return displayModeForCanvas(canvas) === "fullscreen" ? "fill" : void 0;
|
|
7158
|
+
}
|
|
7159
|
+
function canvasOverflowIssue(canvas, viewport, contentHeight, verdict) {
|
|
7160
|
+
const hidden = contentHeight - viewport.height;
|
|
7161
|
+
return {
|
|
7162
|
+
dimension: "canvas-overflow",
|
|
7163
|
+
severity: verdict === "fail" ? "critical" : "major",
|
|
7164
|
+
description: `Rendered content is ${contentHeight}px tall on the ${canvas} canvas (${viewport.width}\xD7${viewport.height}) \u2014 ${hidden}px ${verdict === "fail" ? "is cut off: the inline card does not scroll" : "sits below the first screen"}.`,
|
|
7165
|
+
fix: "Fit the composition to the canvas: fewer and shorter sections, one compact row of chips, no hero taller than a third of the box, no fixed min-heights or tall paddings \u2014 measure against the canvas, not the page."
|
|
7166
|
+
};
|
|
7167
|
+
}
|
|
6705
7168
|
function resolveUiGenPackageDir() {
|
|
6706
7169
|
const selfDir = dirname(fileURLToPath(import.meta.url));
|
|
6707
7170
|
return resolve(selfDir, "..", "..");
|
|
6708
7171
|
}
|
|
7172
|
+
function resolveWirePackageDir() {
|
|
7173
|
+
const uiGenDir = resolveUiGenPackageDir();
|
|
7174
|
+
const candidates = [
|
|
7175
|
+
resolve(uiGenDir, "node_modules", "@ggui-ai", "wire"),
|
|
7176
|
+
// workspace / nested install
|
|
7177
|
+
resolve(uiGenDir, "..", "wire"),
|
|
7178
|
+
// pnpm deploy: `.pnpm/<ui-gen>@<ver>/node_modules/@ggui-ai/{ui-gen,wire}`
|
|
7179
|
+
resolve(uiGenDir, "..", "..", "@ggui-ai", "wire")
|
|
7180
|
+
// hoisted flat install beside the scope dir
|
|
7181
|
+
];
|
|
7182
|
+
for (const dir of candidates) {
|
|
7183
|
+
if (existsSync(resolve(dir, "package.json"))) return dir;
|
|
7184
|
+
}
|
|
7185
|
+
try {
|
|
7186
|
+
const entry = fileURLToPath(import.meta.resolve("@ggui-ai/wire"));
|
|
7187
|
+
let dir = dirname(entry);
|
|
7188
|
+
for (let i = 0; i < 6; i++) {
|
|
7189
|
+
if (existsSync(resolve(dir, "package.json"))) return dir;
|
|
7190
|
+
dir = dirname(dir);
|
|
7191
|
+
}
|
|
7192
|
+
} catch {
|
|
7193
|
+
}
|
|
7194
|
+
return null;
|
|
7195
|
+
}
|
|
6709
7196
|
function resolveDesignPackageDir() {
|
|
6710
|
-
|
|
6711
|
-
|
|
7197
|
+
try {
|
|
7198
|
+
return dirname(createRequire(import.meta.url).resolve("@ggui-ai/design/package.json"));
|
|
7199
|
+
} catch {
|
|
7200
|
+
const selfDir = dirname(fileURLToPath(import.meta.url));
|
|
7201
|
+
return resolve(selfDir, "..", "..", "..", "design");
|
|
7202
|
+
}
|
|
6712
7203
|
}
|
|
6713
|
-
async function bundleForRendering(compiledCode, sampleProps) {
|
|
7204
|
+
async function bundleForRendering(compiledCode, sampleProps, designSrc = resolve(resolveDesignPackageDir(), "src")) {
|
|
6714
7205
|
const tmpDir = resolve(tmpdir(), "ggui-visual-eval-" + Date.now());
|
|
6715
7206
|
mkdirSync(tmpDir, { recursive: true });
|
|
6716
7207
|
const componentFile = resolve(tmpDir, "component.tsx");
|
|
6717
7208
|
const entryFile = resolve(tmpDir, "entry.tsx");
|
|
6718
|
-
const designPkgDir =
|
|
7209
|
+
const designPkgDir = resolve(designSrc, "..");
|
|
7210
|
+
const wirePkgDir = resolveWirePackageDir();
|
|
6719
7211
|
writeFileSync(componentFile, compiledCode);
|
|
6720
7212
|
const entryCode = `
|
|
6721
7213
|
import React from 'react';
|
|
@@ -6763,6 +7255,10 @@ try {
|
|
|
6763
7255
|
jsxImportSource: "react",
|
|
6764
7256
|
external: ["react", "react-dom", "react-dom/client", "react/jsx-runtime"],
|
|
6765
7257
|
alias: {
|
|
7258
|
+
// The wire hooks: resolved from ui-gen's own module graph so the
|
|
7259
|
+
// alias holds in a workspace checkout AND in a `pnpm deploy` image
|
|
7260
|
+
// (where deps are siblings, not children, of this package).
|
|
7261
|
+
...wirePkgDir !== null ? { "@ggui-ai/wire": wirePkgDir } : {},
|
|
6766
7262
|
// D1: generated code imports the single `@ggui-ai/design` barrel.
|
|
6767
7263
|
// The subpath aliases stay for any first-party / legacy code.
|
|
6768
7264
|
"@ggui-ai/design": resolve(designPkgDir, "src", "index.ts"),
|
|
@@ -6774,7 +7270,10 @@ try {
|
|
|
6774
7270
|
// Bare imports in the generated component resolve against ui-gen's
|
|
6775
7271
|
// own node_modules — the temp entry dir has none. Without this every
|
|
6776
7272
|
// wire-bearing component (`@ggui-ai/wire`) failed to bundle.
|
|
6777
|
-
|
|
7273
|
+
// Workspace layout (deps under this package's node_modules) and the
|
|
7274
|
+
// `pnpm deploy` layout (deps as siblings of this package) — both
|
|
7275
|
+
// searched for any other allowlisted bare import.
|
|
7276
|
+
nodePaths: [resolve(resolveUiGenPackageDir(), "node_modules"), resolve(resolveUiGenPackageDir(), "..")],
|
|
6778
7277
|
logLevel: "silent"
|
|
6779
7278
|
});
|
|
6780
7279
|
return result.outputFiles[0]?.text ?? "";
|
|
@@ -6789,7 +7288,7 @@ try {
|
|
|
6789
7288
|
}
|
|
6790
7289
|
}
|
|
6791
7290
|
}
|
|
6792
|
-
function buildRenderHTML(bundledCode, cssTokens) {
|
|
7291
|
+
function buildRenderHTML(bundledCode, cssTokens, fit) {
|
|
6793
7292
|
const tokens = cssTokens ?? getCssTokens();
|
|
6794
7293
|
return `<!DOCTYPE html>
|
|
6795
7294
|
<html>
|
|
@@ -6805,10 +7304,11 @@ function buildRenderHTML(bundledCode, cssTokens) {
|
|
|
6805
7304
|
color: var(--ggui-color-neutral-900, #111827);
|
|
6806
7305
|
}
|
|
6807
7306
|
.error { color: #dc2626; padding: 16px; font-family: monospace; white-space: pre-wrap; }
|
|
7307
|
+
${fit === "fill" ? fillFitRule(JUDGE_SCOPE_CLASS) : ""}
|
|
6808
7308
|
</style>
|
|
6809
7309
|
</head>
|
|
6810
7310
|
<body>
|
|
6811
|
-
<div id="root"></div>
|
|
7311
|
+
<div id="root"${fit === "fill" ? ` class="${JUDGE_SCOPE_CLASS}"` : ""}></div>
|
|
6812
7312
|
<script type="importmap">
|
|
6813
7313
|
{
|
|
6814
7314
|
"imports": {
|
|
@@ -6854,7 +7354,17 @@ async function launchWithPuppeteer(options) {
|
|
|
6854
7354
|
const puppeteer = await import('puppeteer-core');
|
|
6855
7355
|
return puppeteer.default.launch(options);
|
|
6856
7356
|
}
|
|
6857
|
-
|
|
7357
|
+
var CONTENT_HEIGHT_EXPRESSION = "Math.max(document.documentElement.scrollHeight, document.body ? document.body.scrollHeight : 0)";
|
|
7358
|
+
async function measureContentHeight(page) {
|
|
7359
|
+
try {
|
|
7360
|
+
const h = await page.evaluate(CONTENT_HEIGHT_EXPRESSION);
|
|
7361
|
+
return typeof h === "number" && Number.isFinite(h) ? Math.round(h) : null;
|
|
7362
|
+
} catch (e) {
|
|
7363
|
+
console.warn(`[visual-eval] content height unavailable: ${e instanceof Error ? e.message : String(e)}`);
|
|
7364
|
+
return null;
|
|
7365
|
+
}
|
|
7366
|
+
}
|
|
7367
|
+
async function captureScreenshotDetailed(html, viewport = DEFAULT_SCREENSHOT_VIEWPORT, deps = {}, capture = "full-page") {
|
|
6858
7368
|
try {
|
|
6859
7369
|
const launchOptions = await resolveLaunchOptions(viewport, deps);
|
|
6860
7370
|
const browser = await (deps.launch ?? launchWithPuppeteer)(launchOptions);
|
|
@@ -6866,16 +7376,21 @@ async function captureScreenshot(html, viewport = DEFAULT_SCREENSHOT_VIEWPORT, d
|
|
|
6866
7376
|
await page.waitForSelector("#root > *", { timeout: 1e4 }).catch(() => {
|
|
6867
7377
|
});
|
|
6868
7378
|
await new Promise((r) => setTimeout(r, deps.settleMs ?? 1e3));
|
|
6869
|
-
const
|
|
6870
|
-
|
|
7379
|
+
const contentHeight = await measureContentHeight(page);
|
|
7380
|
+
const screenshot = await page.screenshot({ type: "png", fullPage: capture === "full-page" });
|
|
7381
|
+
return { png: Buffer.from(screenshot), contentHeight };
|
|
6871
7382
|
} finally {
|
|
6872
7383
|
await browser.close();
|
|
6873
7384
|
}
|
|
6874
7385
|
} catch (e) {
|
|
6875
|
-
|
|
6876
|
-
|
|
7386
|
+
const reason = `screenshot failed: ${e instanceof Error ? e.message : String(e)}`;
|
|
7387
|
+
console.warn(`[visual-eval] ${reason}`);
|
|
7388
|
+
return { png: null, reason, contentHeight: null };
|
|
6877
7389
|
}
|
|
6878
7390
|
}
|
|
7391
|
+
async function captureScreenshot(html, viewport = DEFAULT_SCREENSHOT_VIEWPORT, deps = {}) {
|
|
7392
|
+
return (await captureScreenshotDetailed(html, viewport, deps)).png;
|
|
7393
|
+
}
|
|
6879
7394
|
var VISUAL_EVAL_PROMPT = `You are a visual quality evaluator for ggui \u2014 a platform where AI agents generate React UI components on demand.
|
|
6880
7395
|
|
|
6881
7396
|
## Context
|
|
@@ -6903,6 +7418,8 @@ The screenshot shows a generated React component built from a design system with
|
|
|
6903
7418
|
- Error messages like "Render error" or blank white space = rendering score 0
|
|
6904
7419
|
- A component that renders but is just a wall of text with no structure should score low on layout and hierarchy
|
|
6905
7420
|
|
|
7421
|
+
List AT MOST 6 issues, most severe first, one sentence each for "description" and "fix" \u2014 a longer list is cut off and lost.
|
|
7422
|
+
|
|
6906
7423
|
Respond with ONLY a JSON object (no markdown, no explanation):
|
|
6907
7424
|
{
|
|
6908
7425
|
"completeness": <0-100>,
|
|
@@ -6914,11 +7431,16 @@ Respond with ONLY a JSON object (no markdown, no explanation):
|
|
|
6914
7431
|
],
|
|
6915
7432
|
"critique": "<2-3 sentences: what's good, what needs improvement, does it achieve the user's goal?>"
|
|
6916
7433
|
}`;
|
|
6917
|
-
|
|
7434
|
+
var VISUAL_JUDGE_MAX_OUTPUT_TOKENS = 4096;
|
|
7435
|
+
var VISUAL_JUDGE_PROMPT_VERSION = "v2-bounded-issues";
|
|
7436
|
+
var VISUAL_JUDGE_PROMPT_DIGEST = createHash("sha256").update(VISUAL_EVAL_PROMPT, "utf8").digest("hex");
|
|
7437
|
+
async function callMultimodalLLM(config, model, prompt, screenshot, originalPrompt, profileBlock = "") {
|
|
6918
7438
|
const userPrompt = `## Original Request
|
|
6919
7439
|
${originalPrompt}
|
|
6920
7440
|
|
|
6921
|
-
|
|
7441
|
+
` + (profileBlock.length > 0 ? `${profileBlock}
|
|
7442
|
+
|
|
7443
|
+
` : "") + "Evaluate the screenshot of the generated component.";
|
|
6922
7444
|
const agent = createVisionAgent({
|
|
6923
7445
|
provider: config.provider === "claude" ? "anthropic" : config.provider,
|
|
6924
7446
|
routeOverride: config.routeOverride,
|
|
@@ -6929,16 +7451,69 @@ Evaluate the screenshot of the generated component.`;
|
|
|
6929
7451
|
prompt,
|
|
6930
7452
|
userPrompt,
|
|
6931
7453
|
{ mediaType: "image/png", base64: screenshot.toString("base64") },
|
|
6932
|
-
|
|
7454
|
+
VISUAL_JUDGE_MAX_OUTPUT_TOKENS
|
|
6933
7455
|
);
|
|
6934
7456
|
}
|
|
7457
|
+
function judgeCountFor(config, canvas) {
|
|
7458
|
+
const k = Math.max(1, Math.floor(config.judgeK ?? 1));
|
|
7459
|
+
if (k === 1) return 1;
|
|
7460
|
+
return config.judgeKCanvases === void 0 || config.judgeKCanvases.includes(canvas) ? k : 1;
|
|
7461
|
+
}
|
|
7462
|
+
function medianOf(values) {
|
|
7463
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
7464
|
+
return sorted[Math.floor((sorted.length - 1) / 2)];
|
|
7465
|
+
}
|
|
7466
|
+
function populationSigma(values) {
|
|
7467
|
+
if (values.length < 2) return 0;
|
|
7468
|
+
const mean = values.reduce((a, b) => a + b, 0) / values.length;
|
|
7469
|
+
const variance = values.reduce((a, b) => a + (b - mean) ** 2, 0) / values.length;
|
|
7470
|
+
return Math.round(Math.sqrt(variance) * 10) / 10;
|
|
7471
|
+
}
|
|
7472
|
+
async function judgeAndParse(judge, config, model, screenshot, originalPrompt, profileBlock, canvas) {
|
|
7473
|
+
let firstReason;
|
|
7474
|
+
for (let attempt = 1; attempt <= 2; attempt++) {
|
|
7475
|
+
const response = await judge(config, model, VISUAL_EVAL_PROMPT, screenshot, originalPrompt, profileBlock);
|
|
7476
|
+
try {
|
|
7477
|
+
const result = parseVisualResponse(response.text, config.passThreshold);
|
|
7478
|
+
return { kind: "ok", result, inputTokens: response.inputTokens, outputTokens: response.outputTokens };
|
|
7479
|
+
} catch (e) {
|
|
7480
|
+
const salvaged = salvageTruncatedVisualAnswer(response.text, config.passThreshold);
|
|
7481
|
+
if (salvaged !== null) {
|
|
7482
|
+
console.warn(`[visual-eval] visual_judge_truncated${canvas ? ` canvas=${canvas}` : ""}: answer cut after ${response.text.length} chars \u2014 score recovered from the closed prefix, issues list partial`);
|
|
7483
|
+
return { kind: "ok", result: salvaged, inputTokens: response.inputTokens, outputTokens: response.outputTokens };
|
|
7484
|
+
}
|
|
7485
|
+
const reason = `judge answer unparsable: ${e instanceof Error ? e.message : String(e)}`;
|
|
7486
|
+
if (attempt === 1) {
|
|
7487
|
+
firstReason = reason;
|
|
7488
|
+
console.warn(`[visual-eval] visual_judge_retry${canvas ? ` canvas=${canvas}` : ""}: ${reason}`);
|
|
7489
|
+
} else {
|
|
7490
|
+
console.warn(`[visual-eval] visual_judge_retry failed again${canvas ? ` canvas=${canvas}` : ""}: ${reason}`);
|
|
7491
|
+
}
|
|
7492
|
+
}
|
|
7493
|
+
}
|
|
7494
|
+
return { kind: "unparsable", reason: firstReason ?? "judge answer unparsable" };
|
|
7495
|
+
}
|
|
6935
7496
|
async function runVisualEvaluation(context, config, deps = {}) {
|
|
7497
|
+
return (await runVisualEvaluationDetailed(context, config, deps)).result;
|
|
7498
|
+
}
|
|
7499
|
+
async function runVisualEvaluationDetailed(context, config, deps = {}) {
|
|
6936
7500
|
const startTime = Date.now();
|
|
6937
7501
|
const judge = deps.judge ?? callMultimodalLLM;
|
|
6938
|
-
const
|
|
6939
|
-
|
|
6940
|
-
|
|
6941
|
-
|
|
7502
|
+
const designSrc = config.designSrcDir ?? resolve(resolveDesignPackageDir(), "src");
|
|
7503
|
+
const design = judgeDesignIdentity(designSrc);
|
|
7504
|
+
const stamp = (r) => ({
|
|
7505
|
+
...r,
|
|
7506
|
+
...design !== null ? { design } : {},
|
|
7507
|
+
...config.themeMode !== void 0 ? { themeMode: config.themeMode } : {}
|
|
7508
|
+
});
|
|
7509
|
+
let bundledCode;
|
|
7510
|
+
try {
|
|
7511
|
+
bundledCode = await bundleForRendering(context.compiledCode, config.sampleProps ?? {}, designSrc);
|
|
7512
|
+
} catch (e) {
|
|
7513
|
+
const unavailableReason = `bundle failed: ${e instanceof Error ? e.message : String(e)}`;
|
|
7514
|
+
console.warn(`[visual-eval] ${unavailableReason}`);
|
|
7515
|
+
return { result: null, unavailableReason };
|
|
7516
|
+
}
|
|
6942
7517
|
console.log(`[visual-eval] bundled: ${bundledCode.length}B (${Date.now() - startTime}ms)`);
|
|
6943
7518
|
const html = buildRenderHTML(bundledCode, context.cssTokens);
|
|
6944
7519
|
const model = config.model ?? getDefaultVisualModel(config.provider);
|
|
@@ -6947,25 +7522,62 @@ async function runVisualEvaluation(context, config, deps = {}) {
|
|
|
6947
7522
|
const perCanvasResults = [];
|
|
6948
7523
|
for (const canvas of config.canvases) {
|
|
6949
7524
|
const viewport = CANVAS_VIEWPORTS[canvas];
|
|
6950
|
-
const
|
|
7525
|
+
const policy = canvasFitPolicy(canvas);
|
|
7526
|
+
const fit = canvasFit(canvas);
|
|
7527
|
+
const canvasHtml = fit !== void 0 ? buildRenderHTML(bundledCode, context.cssTokens, fit) : html;
|
|
7528
|
+
const attempt2 = await captureScreenshotDetailed(canvasHtml, viewport, deps, policy.capture);
|
|
7529
|
+
const screenshot2 = attempt2.png;
|
|
6951
7530
|
if (!screenshot2) {
|
|
6952
|
-
|
|
6953
|
-
|
|
7531
|
+
const unavailableReason = attempt2.reason ?? "no browser available";
|
|
7532
|
+
console.warn(`[visual-eval] ${unavailableReason} at canvas ${canvas} \u2014 skipping visual evaluation`);
|
|
7533
|
+
return { result: null, unavailableReason, canvas };
|
|
7534
|
+
}
|
|
7535
|
+
const k = judgeCountFor(config, canvas);
|
|
7536
|
+
const profileBlock = buildStylingProfileJudgeBlock(context.profile);
|
|
7537
|
+
const answers = await Promise.all(
|
|
7538
|
+
Array.from({ length: k }, () => judgeAndParse(judge, config, model, screenshot2, context.originalPrompt, profileBlock, canvas))
|
|
7539
|
+
);
|
|
7540
|
+
const parsed = answers.filter((a) => a.kind === "ok");
|
|
7541
|
+
if (parsed.length === 0) {
|
|
7542
|
+
const first = answers.find((a) => a.kind === "unparsable");
|
|
7543
|
+
return { result: null, unavailableReason: first?.reason ?? "judge answer unparsable", canvas };
|
|
7544
|
+
}
|
|
7545
|
+
const samples = parsed.map((a) => a.result.finalScore);
|
|
7546
|
+
const median = medianOf(samples);
|
|
7547
|
+
const representative = parsed[samples.indexOf(median)];
|
|
7548
|
+
const result2 = representative.result;
|
|
7549
|
+
result2.finalScore = median;
|
|
7550
|
+
result2.passed = median >= config.passThreshold;
|
|
7551
|
+
result2.inputTokens = parsed.reduce((sum, a) => sum + a.inputTokens, 0);
|
|
7552
|
+
result2.outputTokens = parsed.reduce((sum, a) => sum + a.outputTokens, 0);
|
|
7553
|
+
const response2 = { inputTokens: result2.inputTokens, outputTokens: result2.outputTokens };
|
|
7554
|
+
const judgeRecord = {
|
|
7555
|
+
k,
|
|
7556
|
+
rule: "median",
|
|
7557
|
+
samples,
|
|
7558
|
+
sigma: populationSigma(samples),
|
|
7559
|
+
notes: parsed.map((a) => a.result.critique ?? "")
|
|
7560
|
+
};
|
|
7561
|
+
const contentHeight = attempt2.contentHeight;
|
|
7562
|
+
const overflow = contentHeight !== null && contentHeight > viewport.height;
|
|
7563
|
+
if (contentHeight !== null && contentHeight > viewport.height && policy.overflow !== "none") {
|
|
7564
|
+
result2.issues.push(canvasOverflowIssue(canvas, viewport, contentHeight, policy.overflow));
|
|
7565
|
+
if (policy.overflow === "fail") result2.passed = false;
|
|
6954
7566
|
}
|
|
6955
|
-
const response2 = await judge(config, model, VISUAL_EVAL_PROMPT, screenshot2, context.originalPrompt);
|
|
6956
|
-
const result2 = parseVisualResponse(response2.text, config.passThreshold);
|
|
6957
|
-
result2.inputTokens = response2.inputTokens;
|
|
6958
|
-
result2.outputTokens = response2.outputTokens;
|
|
6959
7567
|
perCanvasResults.push(result2);
|
|
6960
7568
|
perCanvas.push({
|
|
6961
7569
|
canvas,
|
|
6962
7570
|
viewport,
|
|
6963
7571
|
score: result2.finalScore,
|
|
6964
7572
|
passed: result2.passed,
|
|
6965
|
-
screenshotPng: screenshot2
|
|
7573
|
+
screenshotPng: screenshot2,
|
|
7574
|
+
contentHeight,
|
|
7575
|
+
overflow,
|
|
7576
|
+
judge: judgeRecord,
|
|
7577
|
+
...fit !== void 0 ? { fit } : {}
|
|
6966
7578
|
});
|
|
6967
7579
|
console.log(
|
|
6968
|
-
`[visual-eval] canvas=${canvas} ${viewport.width}\xD7${viewport.height} score=${result2.finalScore} ${result2.passed ? "pass" : "FAIL"} | in=${response2.inputTokens} out=${response2.outputTokens}`
|
|
7580
|
+
`[visual-eval] canvas=${canvas} ${viewport.width}\xD7${viewport.height} score=${result2.finalScore}${k > 1 ? ` (median of ${judgeRecord.samples.length}/${k}: ${judgeRecord.samples.join(",")} \u03C3=${judgeRecord.sigma})` : ""} ${result2.passed ? "pass" : "FAIL"} | content=${contentHeight ?? "?"}px${overflow ? ` OVERFLOW (${policy.overflow})` : ""} | in=${response2.inputTokens} out=${response2.outputTokens}`
|
|
6969
7581
|
);
|
|
6970
7582
|
}
|
|
6971
7583
|
const aggregate = aggregateCanvasResults(perCanvas, perCanvasResults);
|
|
@@ -6973,27 +7585,34 @@ async function runVisualEvaluation(context, config, deps = {}) {
|
|
|
6973
7585
|
console.log(
|
|
6974
7586
|
`[visual-eval] score=${aggregate.finalScore} (mean of ${perCanvas.length} canvases; ${aggregate.passed ? "every canvas passed" : `${perCanvas.filter((c) => !c.passed).length} failed`}) (${elapsed2}ms)`
|
|
6975
7587
|
);
|
|
6976
|
-
return aggregate;
|
|
7588
|
+
return { result: stamp(aggregate) };
|
|
6977
7589
|
}
|
|
6978
|
-
const
|
|
7590
|
+
const attempt = await captureScreenshotDetailed(html, config.viewport, deps);
|
|
7591
|
+
const screenshot = attempt.png;
|
|
6979
7592
|
if (!screenshot) {
|
|
6980
|
-
|
|
6981
|
-
|
|
7593
|
+
const unavailableReason = attempt.reason ?? "no browser available";
|
|
7594
|
+
console.warn(`[visual-eval] ${unavailableReason} \u2014 skipping visual evaluation`);
|
|
7595
|
+
return { result: null, unavailableReason };
|
|
6982
7596
|
}
|
|
6983
7597
|
console.log(`[visual-eval] screenshot: ${screenshot.length}B (${Date.now() - startTime}ms)`);
|
|
6984
|
-
const
|
|
7598
|
+
const answer = await judgeAndParse(
|
|
7599
|
+
judge,
|
|
6985
7600
|
config,
|
|
6986
7601
|
model,
|
|
6987
|
-
VISUAL_EVAL_PROMPT,
|
|
6988
7602
|
screenshot,
|
|
6989
|
-
context.originalPrompt
|
|
7603
|
+
context.originalPrompt,
|
|
7604
|
+
buildStylingProfileJudgeBlock(context.profile)
|
|
6990
7605
|
);
|
|
6991
|
-
|
|
6992
|
-
|
|
6993
|
-
|
|
7606
|
+
if (answer.kind === "unparsable") {
|
|
7607
|
+
return { result: null, unavailableReason: answer.reason };
|
|
7608
|
+
}
|
|
7609
|
+
const result = answer.result;
|
|
7610
|
+
result.inputTokens = answer.inputTokens;
|
|
7611
|
+
result.outputTokens = answer.outputTokens;
|
|
7612
|
+
const response = { inputTokens: answer.inputTokens, outputTokens: answer.outputTokens };
|
|
6994
7613
|
const elapsed = Date.now() - startTime;
|
|
6995
7614
|
console.log(`[visual-eval] score=${result.finalScore} (${elapsed}ms) | in=${response.inputTokens} out=${response.outputTokens}`);
|
|
6996
|
-
return result;
|
|
7615
|
+
return { result: stamp(result) };
|
|
6997
7616
|
}
|
|
6998
7617
|
function aggregateCanvasResults(perCanvas, results) {
|
|
6999
7618
|
const n = results.length;
|
|
@@ -7033,9 +7652,57 @@ function summarizeVisualResult(result) {
|
|
|
7033
7652
|
canvas: c.canvas,
|
|
7034
7653
|
viewport: c.viewport,
|
|
7035
7654
|
score: c.score,
|
|
7036
|
-
passed: c.passed
|
|
7655
|
+
passed: c.passed,
|
|
7656
|
+
contentHeight: c.contentHeight,
|
|
7657
|
+
overflow: c.overflow,
|
|
7658
|
+
judge: c.judge,
|
|
7659
|
+
...c.fit !== void 0 ? { fit: c.fit } : {}
|
|
7037
7660
|
}));
|
|
7038
|
-
return {
|
|
7661
|
+
return {
|
|
7662
|
+
score: result.finalScore,
|
|
7663
|
+
passed: result.passed,
|
|
7664
|
+
canvases,
|
|
7665
|
+
...result.design !== void 0 ? { design: result.design } : {},
|
|
7666
|
+
...result.themeMode !== void 0 ? { themeMode: result.themeMode } : {}
|
|
7667
|
+
};
|
|
7668
|
+
}
|
|
7669
|
+
function salvageTruncatedVisualAnswer(text, passThreshold) {
|
|
7670
|
+
const dim = (name) => {
|
|
7671
|
+
const m = text.match(new RegExp(`"${name}"\\s*:\\s*(\\d+(?:\\.\\d+)?)`));
|
|
7672
|
+
return m ? Number(m[1]) : null;
|
|
7673
|
+
};
|
|
7674
|
+
const completeness = dim("completeness");
|
|
7675
|
+
const layout = dim("layout");
|
|
7676
|
+
const hierarchy = dim("hierarchy");
|
|
7677
|
+
const aesthetics = dim("aesthetics");
|
|
7678
|
+
if (completeness === null || layout === null || hierarchy === null || aesthetics === null) return null;
|
|
7679
|
+
const issues = [];
|
|
7680
|
+
const issuesAt = text.indexOf('"issues"');
|
|
7681
|
+
if (issuesAt >= 0) {
|
|
7682
|
+
for (const m of text.slice(issuesAt).matchAll(/\{[^{}]*\}/g)) {
|
|
7683
|
+
try {
|
|
7684
|
+
const o = JSON.parse(m[0]);
|
|
7685
|
+
if (typeof o.description === "string") {
|
|
7686
|
+
issues.push({
|
|
7687
|
+
dimension: typeof o.dimension === "string" ? o.dimension : "layout",
|
|
7688
|
+
severity: typeof o.severity === "string" ? o.severity : "minor",
|
|
7689
|
+
description: o.description,
|
|
7690
|
+
fix: typeof o.fix === "string" ? o.fix : ""
|
|
7691
|
+
});
|
|
7692
|
+
}
|
|
7693
|
+
} catch {
|
|
7694
|
+
}
|
|
7695
|
+
}
|
|
7696
|
+
}
|
|
7697
|
+
const rebuilt = JSON.stringify({
|
|
7698
|
+
completeness,
|
|
7699
|
+
layout,
|
|
7700
|
+
hierarchy,
|
|
7701
|
+
aesthetics,
|
|
7702
|
+
issues,
|
|
7703
|
+
critique: `[truncated judge answer \u2014 score recovered from the closed prefix; ${issues.length} issue(s) kept, the rest and the critique were cut]`
|
|
7704
|
+
});
|
|
7705
|
+
return parseVisualResponse(rebuilt, passThreshold);
|
|
7039
7706
|
}
|
|
7040
7707
|
function parseVisualResponse(text, passThreshold) {
|
|
7041
7708
|
const jsonMatch = text.match(/\{[\s\S]*\}/);
|
|
@@ -7097,7 +7764,11 @@ async function runVisualEval(context, config) {
|
|
|
7097
7764
|
const summary = summarizeVisualResult(result);
|
|
7098
7765
|
return summary === void 0 ? { issues } : { issues, summary };
|
|
7099
7766
|
}
|
|
7767
|
+
function cssTokensForAppTheme(theme, mode = "light") {
|
|
7768
|
+
if (theme === void 0) return getCssTokens(mode);
|
|
7769
|
+
return composeThemeCss({ layer: "page", mode: theme.mode ?? mode, appTheme: theme });
|
|
7770
|
+
}
|
|
7100
7771
|
|
|
7101
|
-
export { CANVAS_BREAKPOINTS, CANVAS_CLASSES, CANVAS_DESCRIPTORS, CANVAS_VIEWPORTS, CRITERIA, DEFAULT_EVAL_MAX_ROUNDS, DEFAULT_QUALITY_CONFIG, LLM_EVAL_STATIC_CRITERIA, MAX_EVAL_ROUNDS_HARD_LIMIT, buildCodingCriteriaSummary, buildFixPrompt, callMultimodalLLM, canvasDisplayModeForShell, canvasForRendering, canvasForViewportWidth, captureScreenshot, computeEvaluationScore, createEvaluationToolsServer, criteriaFor, describeCanvas, extractCompiledCode, extractCompiledCodeFromMessage, extractEvalResult, extractSourceCode, extractSourceCodeFromMessage, extractToolResultTexts, getActionableIssues, getCriteriaByPriority, getCriterionById, getEvaluatorSystemPrompt, getLLMCriteria, isBlocked, matches, notApplicableCoverage, priorityForIssue, resolveLaunchOptions, runAxisChecks, runEvaluation, runEvaluationLoop, runVisualEval, runVisualEvaluation, summarizeVisualResult };
|
|
7772
|
+
export { CANVAS_BREAKPOINTS, CANVAS_CLASSES, CANVAS_DESCRIPTORS, CANVAS_VIEWPORTS, CONTENT_HEIGHT_EXPRESSION, CRITERIA, DEFAULT_EVAL_MAX_ROUNDS, DEFAULT_QUALITY_CONFIG, LLM_EVAL_STATIC_CRITERIA, MAX_EVAL_ROUNDS_HARD_LIMIT, VISUAL_EVAL_PROMPT, VISUAL_JUDGE_MAX_OUTPUT_TOKENS, VISUAL_JUDGE_PROMPT_DIGEST, VISUAL_JUDGE_PROMPT_VERSION, buildCodingCriteriaSummary, buildFixPrompt, callMultimodalLLM, canvasDisplayModeForShell, canvasFitPolicy, canvasForRendering, canvasForViewportWidth, canvasOverflowIssue, captureScreenshot, captureScreenshotDetailed, computeEvaluationScore, createEvaluationToolsServer, criteriaFor, cssTokensForAppTheme, describeCanvas, designTreeSha256, extractCompiledCode, extractCompiledCodeFromMessage, extractEvalResult, extractSourceCode, extractSourceCodeFromMessage, extractToolResultTexts, getActionableIssues, getCriteriaByPriority, getCriterionById, getEvaluatorSystemPrompt, getLLMCriteria, isBlocked, judgeDesignIdentity, matches, notApplicableCoverage, priorityForIssue, resetJudgeDesignIdentityCache, resolveDesignPackageDir, resolveLaunchOptions, runAxisChecks, runEvaluation, runEvaluationLoop, runVisualEval, runVisualEvaluation, runVisualEvaluationDetailed, summarizeVisualResult };
|
|
7102
7773
|
//# sourceMappingURL=index.js.map
|
|
7103
7774
|
//# sourceMappingURL=index.js.map
|