coderio 0.1.0-alpha.9 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +60 -48
- package/dist/cli.js +200 -141
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +505 -65
- package/dist/index.js +273 -4342
- package/dist/index.js.map +1 -1
- package/dist/tools/report-tool/template/assets/{index-I6pJsBi9.js → index-BXtNLUQ4.js} +16 -16
- package/dist/tools/report-tool/template/index.html +1 -1
- package/package.json +29 -4
package/dist/index.js
CHANGED
|
@@ -11,10 +11,6 @@ var __name = (target, value) => __defProp(target, "name", { value, configurable:
|
|
|
11
11
|
var __esm = (fn, res) => function __init() {
|
|
12
12
|
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
13
13
|
};
|
|
14
|
-
var __export = (target, all) => {
|
|
15
|
-
for (var name in all)
|
|
16
|
-
__defProp(target, name, { get: all[name], enumerable: true });
|
|
17
|
-
};
|
|
18
14
|
var __decoratorStart = (base) => [, , , __create(base?.[__knownSymbol("metadata")] ?? null)];
|
|
19
15
|
var __decoratorStrings = ["class", "method", "getter", "setter", "accessor", "field", "value", "get", "set"];
|
|
20
16
|
var __expectFn = (fn) => fn !== void 0 && typeof fn !== "function" ? __typeError("Function expected") : fn;
|
|
@@ -25,7 +21,7 @@ var __runInitializers = (array, flags, self, value) => {
|
|
|
25
21
|
return value;
|
|
26
22
|
};
|
|
27
23
|
var __decorateElement = (array, flags, name, decorators, target, extra) => {
|
|
28
|
-
var fn, it, done, ctx,
|
|
24
|
+
var fn, it, done, ctx, access, k = flags & 7, s = !!(flags & 8), p = !!(flags & 16);
|
|
29
25
|
var j = k > 3 ? array.length + 1 : k ? s ? 1 : 2 : 0, key = __decoratorStrings[k + 5];
|
|
30
26
|
var initializers = k > 3 && (array[j - 1] = []), extraInitializers = array[j] || (array[j] = []);
|
|
31
27
|
var desc = k && (!p && !s && (target = target.prototype), k < 5 && (k > 3 || !p) && __getOwnPropDesc(k < 4 ? target : { get [name]() {
|
|
@@ -37,9 +33,9 @@ var __decorateElement = (array, flags, name, decorators, target, extra) => {
|
|
|
37
33
|
for (var i = decorators.length - 1; i >= 0; i--) {
|
|
38
34
|
ctx = __decoratorContext(k, name, done = {}, array[3], extraInitializers);
|
|
39
35
|
if (k) {
|
|
40
|
-
ctx.static = s, ctx.private = p,
|
|
41
|
-
if (k ^ 3)
|
|
42
|
-
if (k > 2)
|
|
36
|
+
ctx.static = s, ctx.private = p, access = ctx.access = { has: p ? (x) => __privateIn(target, x) : (x) => name in x };
|
|
37
|
+
if (k ^ 3) access.get = p ? (x) => (k ^ 1 ? __privateGet : __privateMethod)(x, target, k ^ 4 ? extra : desc.get) : (x) => x[name];
|
|
38
|
+
if (k > 2) access.set = p ? (x, y) => __privateSet(x, target, y, k ^ 4 ? extra : desc.set) : (x, y) => x[name] = y;
|
|
43
39
|
}
|
|
44
40
|
it = (0, decorators[i])(k ? k < 4 ? p ? extra : desc[key] : k > 4 ? void 0 : { get: desc.get, set: desc.set } : target, ctx), done._ = 1;
|
|
45
41
|
if (k ^ 4 || it === void 0) __expectFn(it) && (k > 4 ? initializers.unshift(it) : k ? p ? extra = it : desc[key] = it : target = it);
|
|
@@ -54,74 +50,7 @@ var __privateGet = (obj, member, getter) => (__accessCheck(obj, member, "read fr
|
|
|
54
50
|
var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "write to private field"), setter ? setter.call(obj, value) : member.set(obj, value), value);
|
|
55
51
|
var __privateMethod = (obj, member, method) => (__accessCheck(obj, member, "access private method"), method);
|
|
56
52
|
|
|
57
|
-
// src/utils/logger.ts
|
|
58
|
-
import chalk from "chalk";
|
|
59
|
-
function getTimestamp() {
|
|
60
|
-
const now = /* @__PURE__ */ new Date();
|
|
61
|
-
const year = now.getFullYear();
|
|
62
|
-
const month = String(now.getMonth() + 1).padStart(2, "0");
|
|
63
|
-
const day = String(now.getDate()).padStart(2, "0");
|
|
64
|
-
const hours = String(now.getHours()).padStart(2, "0");
|
|
65
|
-
const minutes = String(now.getMinutes()).padStart(2, "0");
|
|
66
|
-
const seconds = String(now.getSeconds()).padStart(2, "0");
|
|
67
|
-
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
|
|
68
|
-
}
|
|
69
|
-
var logger;
|
|
70
|
-
var init_logger = __esm({
|
|
71
|
-
"src/utils/logger.ts"() {
|
|
72
|
-
"use strict";
|
|
73
|
-
logger = {
|
|
74
|
-
/**
|
|
75
|
-
* Print standard log (alias for info logs).
|
|
76
|
-
*
|
|
77
|
-
* Many subsystems (e.g. validation) use `printLog()` as the default logging method.
|
|
78
|
-
*/
|
|
79
|
-
printLog(message) {
|
|
80
|
-
console.log(message);
|
|
81
|
-
},
|
|
82
|
-
/**
|
|
83
|
-
* Print info log in blue
|
|
84
|
-
*/
|
|
85
|
-
printInfoLog(message) {
|
|
86
|
-
console.log(chalk.blue(`[${getTimestamp()}] [INFO] ${message}`));
|
|
87
|
-
},
|
|
88
|
-
/**
|
|
89
|
-
* Print warning log in yellow
|
|
90
|
-
*/
|
|
91
|
-
printWarnLog(message) {
|
|
92
|
-
console.warn(chalk.yellow(`[${getTimestamp()}] [WARNING] ${message}`));
|
|
93
|
-
},
|
|
94
|
-
/**
|
|
95
|
-
* Print error log in red
|
|
96
|
-
*/
|
|
97
|
-
printErrorLog(message) {
|
|
98
|
-
console.error(chalk.red(`[${getTimestamp()}] [ERROR] \u2716 ${message}`));
|
|
99
|
-
},
|
|
100
|
-
/**
|
|
101
|
-
* Print test/debug log in gray
|
|
102
|
-
* Only logs in development/test environments
|
|
103
|
-
*/
|
|
104
|
-
printTestLog(message) {
|
|
105
|
-
if (process.env.NODE_ENV === "development") {
|
|
106
|
-
console.log(chalk.gray(`[${getTimestamp()}] [DEBUG] ${message}`));
|
|
107
|
-
}
|
|
108
|
-
},
|
|
109
|
-
/**
|
|
110
|
-
* Print success log in green
|
|
111
|
-
*/
|
|
112
|
-
printSuccessLog(message) {
|
|
113
|
-
console.log(chalk.green(`[${getTimestamp()}] [SUCCESS] \u2714 ${message}`));
|
|
114
|
-
}
|
|
115
|
-
};
|
|
116
|
-
}
|
|
117
|
-
});
|
|
118
|
-
|
|
119
53
|
// src/nodes/process/structure/prompt.ts
|
|
120
|
-
var prompt_exports = {};
|
|
121
|
-
__export(prompt_exports, {
|
|
122
|
-
extractDataListPrompt: () => extractDataListPrompt,
|
|
123
|
-
generateStructurePrompt: () => generateStructurePrompt
|
|
124
|
-
});
|
|
125
54
|
var generateStructurePrompt, extractDataListPrompt;
|
|
126
55
|
var init_prompt = __esm({
|
|
127
56
|
"src/nodes/process/structure/prompt.ts"() {
|
|
@@ -426,47 +355,6 @@ Example Output JSON Structure (for reference only):
|
|
|
426
355
|
}
|
|
427
356
|
});
|
|
428
357
|
|
|
429
|
-
// src/tools/position-tool/utils/fetch-thumbnail-dimensions.ts
|
|
430
|
-
var fetch_thumbnail_dimensions_exports = {};
|
|
431
|
-
__export(fetch_thumbnail_dimensions_exports, {
|
|
432
|
-
fetchThumbnailDimensions: () => fetchThumbnailDimensions
|
|
433
|
-
});
|
|
434
|
-
import axios3 from "axios";
|
|
435
|
-
import sharp4 from "sharp";
|
|
436
|
-
async function fetchThumbnailDimensions(figmaThumbnailUrl) {
|
|
437
|
-
if (!figmaThumbnailUrl) {
|
|
438
|
-
return void 0;
|
|
439
|
-
}
|
|
440
|
-
try {
|
|
441
|
-
const response = await axios3.get(figmaThumbnailUrl, {
|
|
442
|
-
responseType: "arraybuffer",
|
|
443
|
-
timeout: 3e4
|
|
444
|
-
});
|
|
445
|
-
const imageBuffer = Buffer.from(response.data);
|
|
446
|
-
const metadata = await sharp4(imageBuffer).metadata();
|
|
447
|
-
if (!metadata.width || !metadata.height) {
|
|
448
|
-
return void 0;
|
|
449
|
-
}
|
|
450
|
-
return {
|
|
451
|
-
width: metadata.width,
|
|
452
|
-
height: metadata.height
|
|
453
|
-
};
|
|
454
|
-
} catch (error) {
|
|
455
|
-
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
456
|
-
logger.printWarnLog(`Failed to fetch Figma thumbnail: ${errorMsg}`);
|
|
457
|
-
return void 0;
|
|
458
|
-
}
|
|
459
|
-
}
|
|
460
|
-
var init_fetch_thumbnail_dimensions = __esm({
|
|
461
|
-
"src/tools/position-tool/utils/fetch-thumbnail-dimensions.ts"() {
|
|
462
|
-
"use strict";
|
|
463
|
-
init_logger();
|
|
464
|
-
}
|
|
465
|
-
});
|
|
466
|
-
|
|
467
|
-
// src/graph.ts
|
|
468
|
-
import { StateGraph, START, END } from "@langchain/langgraph";
|
|
469
|
-
|
|
470
358
|
// src/types/graph-types.ts
|
|
471
359
|
var GraphNode = /* @__PURE__ */ ((GraphNode2) => {
|
|
472
360
|
GraphNode2["INITIAL"] = "initial";
|
|
@@ -476,20 +364,23 @@ var GraphNode = /* @__PURE__ */ ((GraphNode2) => {
|
|
|
476
364
|
GraphNode2["CODE"] = "code";
|
|
477
365
|
return GraphNode2;
|
|
478
366
|
})(GraphNode || {});
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
367
|
+
var ValidationMode = /* @__PURE__ */ ((ValidationMode2) => {
|
|
368
|
+
ValidationMode2["CodeOnly"] = "codeOnly";
|
|
369
|
+
ValidationMode2["ReportOnly"] = "reportOnly";
|
|
370
|
+
ValidationMode2["Full"] = "full";
|
|
371
|
+
return ValidationMode2;
|
|
372
|
+
})(ValidationMode || {});
|
|
373
|
+
|
|
374
|
+
// src/types/figma-types.ts
|
|
375
|
+
var FigmaImageFormat = /* @__PURE__ */ ((FigmaImageFormat2) => {
|
|
376
|
+
FigmaImageFormat2["PNG"] = "png";
|
|
377
|
+
FigmaImageFormat2["JPG"] = "jpg";
|
|
378
|
+
FigmaImageFormat2["SVG"] = "svg";
|
|
379
|
+
FigmaImageFormat2["PDF"] = "pdf";
|
|
380
|
+
FigmaImageFormat2["EPS"] = "eps";
|
|
381
|
+
FigmaImageFormat2["WEBP"] = "webp";
|
|
382
|
+
return FigmaImageFormat2;
|
|
383
|
+
})(FigmaImageFormat || {});
|
|
493
384
|
|
|
494
385
|
// src/agents/initial-agent/prompt.ts
|
|
495
386
|
var INITIAL_AGENT_SYSTEM_PROMPT = `
|
|
@@ -522,6 +413,7 @@ var INITIAL_AGENT_SYSTEM_PROMPT = `
|
|
|
522
413
|
- "dev": "pnpm exec vite"
|
|
523
414
|
- "build": "pnpm exec tsc && pnpm exec vite build"
|
|
524
415
|
- "preview": "pnpm exec vite preview"
|
|
416
|
+
* IMPORTANT: Do NOT add "coderio" as a dependency - it's only a build tool, not a runtime dependency.
|
|
525
417
|
- \`vite.config.ts\`: Configure React and TailwindCSS V4 plugins. MUST include path alias configuration:
|
|
526
418
|
* Add \`resolve.alias\` with \`@\` pointing to \`path.resolve(__dirname, './src')\`
|
|
527
419
|
* Import \`path\` from 'node:path'
|
|
@@ -548,23 +440,46 @@ var INITIAL_AGENT_SYSTEM_PROMPT = `
|
|
|
548
440
|
</system_instructions>
|
|
549
441
|
`.trim();
|
|
550
442
|
|
|
551
|
-
// src/agents/initial-agent/
|
|
552
|
-
function
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
verbose: true
|
|
561
|
-
});
|
|
443
|
+
// src/agents/initial-agent/instruction.ts
|
|
444
|
+
function initialAgentInstruction(params) {
|
|
445
|
+
return `
|
|
446
|
+
appPath: ${params.appPath}
|
|
447
|
+
appName: ${params.appName}
|
|
448
|
+
|
|
449
|
+
TASK:
|
|
450
|
+
Scaffold a clean React V18 + TS + Vite + TailwindCSS V4 + Less project.
|
|
451
|
+
`.trim();
|
|
562
452
|
}
|
|
563
453
|
|
|
564
|
-
// src/
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
454
|
+
// src/utils/url-parser.ts
|
|
455
|
+
var parseFigmaUrl = (url) => {
|
|
456
|
+
let fileId = null;
|
|
457
|
+
let name = "untitled";
|
|
458
|
+
let nodeId = null;
|
|
459
|
+
try {
|
|
460
|
+
const urlObj = new URL(decodeURIComponent(url));
|
|
461
|
+
const pathParts = urlObj.pathname.split("/").filter(Boolean);
|
|
462
|
+
if (pathParts.length >= 3) {
|
|
463
|
+
fileId = pathParts[pathParts.length - 2] || null;
|
|
464
|
+
const fileName = pathParts[pathParts.length - 1];
|
|
465
|
+
name = fileName ? encodeURI(fileName).toLowerCase() : "untitled";
|
|
466
|
+
name = name.length > 20 ? name.substring(0, 20) : name;
|
|
467
|
+
}
|
|
468
|
+
nodeId = urlObj.searchParams.get("node-id") || null;
|
|
469
|
+
nodeId = nodeId ? nodeId.replace(/-/g, ":") : null;
|
|
470
|
+
} catch {
|
|
471
|
+
}
|
|
472
|
+
if (!fileId || !nodeId) {
|
|
473
|
+
throw new Error("Invalid Figma URL");
|
|
474
|
+
}
|
|
475
|
+
return { fileId, name, nodeId, projectName: `${name}_${nodeId}` };
|
|
476
|
+
};
|
|
477
|
+
|
|
478
|
+
// src/tools/figma-tool/index.ts
|
|
479
|
+
import { tools as tools2 } from "evoltagent";
|
|
480
|
+
|
|
481
|
+
// src/utils/axios.ts
|
|
482
|
+
import axios from "axios";
|
|
568
483
|
|
|
569
484
|
// src/utils/config.ts
|
|
570
485
|
import { readFileSync, existsSync } from "fs";
|
|
@@ -597,13 +512,6 @@ Please create the configuration file.`);
|
|
|
597
512
|
throw error;
|
|
598
513
|
}
|
|
599
514
|
}
|
|
600
|
-
function getModelConfig() {
|
|
601
|
-
const config = loadConfig();
|
|
602
|
-
if (!config.model) {
|
|
603
|
-
throw new Error("Model configuration not found in config.yaml");
|
|
604
|
-
}
|
|
605
|
-
return config.model;
|
|
606
|
-
}
|
|
607
515
|
function getFigmaConfig() {
|
|
608
516
|
const config = loadConfig();
|
|
609
517
|
if (!config.figma) {
|
|
@@ -616,71 +524,80 @@ function getDebugConfig() {
|
|
|
616
524
|
return config.debug || { enabled: false };
|
|
617
525
|
}
|
|
618
526
|
|
|
619
|
-
// src/
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
// src/agents/initial-agent/instruction.ts
|
|
624
|
-
function initialAgentInstruction(params) {
|
|
625
|
-
return `
|
|
626
|
-
appPath: ${params.appPath}
|
|
627
|
-
appName: ${params.appName}
|
|
527
|
+
// src/utils/file.ts
|
|
528
|
+
import fs from "fs";
|
|
529
|
+
import path from "path";
|
|
628
530
|
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
531
|
+
// src/utils/logger.ts
|
|
532
|
+
import chalk from "chalk";
|
|
533
|
+
function getTimestamp() {
|
|
534
|
+
const now = /* @__PURE__ */ new Date();
|
|
535
|
+
const year = now.getFullYear();
|
|
536
|
+
const month = String(now.getMonth() + 1).padStart(2, "0");
|
|
537
|
+
const day = String(now.getDate()).padStart(2, "0");
|
|
538
|
+
const hours = String(now.getHours()).padStart(2, "0");
|
|
539
|
+
const minutes = String(now.getMinutes()).padStart(2, "0");
|
|
540
|
+
const seconds = String(now.getSeconds()).padStart(2, "0");
|
|
541
|
+
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
|
|
632
542
|
}
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
}
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
543
|
+
var logger = {
|
|
544
|
+
/**
|
|
545
|
+
* Print standard log (alias for info logs).
|
|
546
|
+
*
|
|
547
|
+
* Many subsystems (e.g. validation) use `printLog()` as the default logging method.
|
|
548
|
+
*/
|
|
549
|
+
printLog(message) {
|
|
550
|
+
console.log(message);
|
|
551
|
+
},
|
|
552
|
+
/**
|
|
553
|
+
* Print info log in blue
|
|
554
|
+
*/
|
|
555
|
+
printInfoLog(message) {
|
|
556
|
+
console.log(chalk.blue(`[${getTimestamp()}] [INFO] ${message}`));
|
|
557
|
+
},
|
|
558
|
+
/**
|
|
559
|
+
* Print warning log in yellow
|
|
560
|
+
*/
|
|
561
|
+
printWarnLog(message) {
|
|
562
|
+
console.warn(chalk.yellow(`[${getTimestamp()}] [WARNING] ${message}`));
|
|
563
|
+
},
|
|
564
|
+
/**
|
|
565
|
+
* Print error log in red
|
|
566
|
+
*/
|
|
567
|
+
printErrorLog(message) {
|
|
568
|
+
console.error(chalk.red(`[${getTimestamp()}] [ERROR] \u2716 ${message}`));
|
|
569
|
+
},
|
|
570
|
+
/**
|
|
571
|
+
* Print test/debug log in gray
|
|
572
|
+
* Only logs in development/test environments
|
|
573
|
+
*/
|
|
574
|
+
printTestLog(message) {
|
|
575
|
+
if (process.env.NODE_ENV === "development") {
|
|
576
|
+
console.log(chalk.gray(`[${getTimestamp()}] [DEBUG] ${message}`));
|
|
655
577
|
}
|
|
578
|
+
},
|
|
579
|
+
/**
|
|
580
|
+
* Print success log in green
|
|
581
|
+
*/
|
|
582
|
+
printSuccessLog(message) {
|
|
583
|
+
console.log(chalk.green(`[${getTimestamp()}] [SUCCESS] \u2714 ${message}`));
|
|
656
584
|
}
|
|
657
|
-
logger.printSuccessLog(result);
|
|
658
|
-
return {};
|
|
659
585
|
};
|
|
660
586
|
|
|
661
|
-
// src/tools/figma-tool/index.ts
|
|
662
|
-
import { tools as tools2 } from "evoltagent";
|
|
663
|
-
|
|
664
|
-
// src/utils/axios.ts
|
|
665
|
-
import axios from "axios";
|
|
666
|
-
|
|
667
587
|
// src/utils/file.ts
|
|
668
|
-
init_logger();
|
|
669
|
-
import fs2 from "fs";
|
|
670
|
-
import path2 from "path";
|
|
671
588
|
var writeFile = (folderPath, filePath, content) => {
|
|
672
589
|
if (!folderPath || !filePath || !content) {
|
|
673
590
|
return;
|
|
674
591
|
}
|
|
675
|
-
if (!
|
|
676
|
-
|
|
592
|
+
if (!fs.existsSync(folderPath)) {
|
|
593
|
+
fs.mkdirSync(folderPath, { recursive: true });
|
|
677
594
|
}
|
|
678
|
-
|
|
595
|
+
fs.writeFileSync(path.join(folderPath, filePath), content);
|
|
679
596
|
};
|
|
680
597
|
function createFiles({ files, filePath }) {
|
|
681
598
|
try {
|
|
682
599
|
for (const file of files) {
|
|
683
|
-
const dirPath =
|
|
600
|
+
const dirPath = path.dirname(filePath);
|
|
684
601
|
writeFile(dirPath, file.filename, file.content);
|
|
685
602
|
}
|
|
686
603
|
} catch (error) {
|
|
@@ -690,9 +607,8 @@ function createFiles({ files, filePath }) {
|
|
|
690
607
|
}
|
|
691
608
|
|
|
692
609
|
// src/utils/workspace.ts
|
|
693
|
-
|
|
694
|
-
import
|
|
695
|
-
import fs3 from "fs";
|
|
610
|
+
import path2 from "path";
|
|
611
|
+
import fs2 from "fs";
|
|
696
612
|
var Workspace = class {
|
|
697
613
|
path = null;
|
|
698
614
|
initWorkspace(subPath, rootPath, appName) {
|
|
@@ -700,21 +616,21 @@ var Workspace = class {
|
|
|
700
616
|
return this.path;
|
|
701
617
|
}
|
|
702
618
|
const root = rootPath || (process.env.CODERIO_CLI_USER_CWD ?? process.cwd());
|
|
703
|
-
const coderioRoot =
|
|
704
|
-
const finalRoot =
|
|
619
|
+
const coderioRoot = path2.join(root, "coderio");
|
|
620
|
+
const finalRoot = path2.resolve(coderioRoot, subPath);
|
|
705
621
|
const app = appName || "my-app";
|
|
706
|
-
const absoluteRoot =
|
|
707
|
-
const processDir =
|
|
708
|
-
const checkpointDir =
|
|
709
|
-
const debugDir =
|
|
622
|
+
const absoluteRoot = path2.resolve(finalRoot);
|
|
623
|
+
const processDir = path2.join(absoluteRoot, "process");
|
|
624
|
+
const checkpointDir = path2.join(absoluteRoot, "checkpoint");
|
|
625
|
+
const debugDir = path2.join(absoluteRoot, "debug");
|
|
710
626
|
this.path = {
|
|
711
627
|
root: absoluteRoot,
|
|
712
|
-
app:
|
|
628
|
+
app: path2.join(absoluteRoot, app),
|
|
713
629
|
process: processDir,
|
|
714
630
|
debug: debugDir,
|
|
715
|
-
reports:
|
|
716
|
-
db:
|
|
717
|
-
checkpoint:
|
|
631
|
+
reports: path2.join(absoluteRoot, "reports.html"),
|
|
632
|
+
db: path2.join(checkpointDir, "coderio-cli.db"),
|
|
633
|
+
checkpoint: path2.join(checkpointDir, "checkpoint.json")
|
|
718
634
|
};
|
|
719
635
|
return this.path;
|
|
720
636
|
}
|
|
@@ -723,11 +639,11 @@ var Workspace = class {
|
|
|
723
639
|
*/
|
|
724
640
|
deleteWorkspace(workspace) {
|
|
725
641
|
try {
|
|
726
|
-
if (
|
|
727
|
-
const entries =
|
|
642
|
+
if (fs2.existsSync(workspace.root)) {
|
|
643
|
+
const entries = fs2.readdirSync(workspace.root);
|
|
728
644
|
for (const entry of entries) {
|
|
729
|
-
const fullPath =
|
|
730
|
-
|
|
645
|
+
const fullPath = path2.join(workspace.root, entry);
|
|
646
|
+
fs2.rmSync(fullPath, { recursive: true, force: true });
|
|
731
647
|
}
|
|
732
648
|
}
|
|
733
649
|
} catch (error) {
|
|
@@ -742,7 +658,7 @@ var Workspace = class {
|
|
|
742
658
|
* @returns The absolute path to the source code directory
|
|
743
659
|
*/
|
|
744
660
|
resolveAppSrc(paths, srcSubPath) {
|
|
745
|
-
return
|
|
661
|
+
return path2.join(paths.app, "src", srcSubPath);
|
|
746
662
|
}
|
|
747
663
|
/**
|
|
748
664
|
* Resolve component alias path to absolute filesystem path.
|
|
@@ -797,7 +713,6 @@ async function get(url, config) {
|
|
|
797
713
|
}
|
|
798
714
|
|
|
799
715
|
// src/tools/figma-tool/figma.ts
|
|
800
|
-
init_logger();
|
|
801
716
|
var fetchFigmaNode = async (fileId, nodeId, token) => {
|
|
802
717
|
const url = `https://api.figma.com/v1/files/${fileId}/nodes?ids=${nodeId}`;
|
|
803
718
|
try {
|
|
@@ -851,8 +766,8 @@ var checkBorder = (node) => {
|
|
|
851
766
|
};
|
|
852
767
|
|
|
853
768
|
// src/tools/figma-tool/images.ts
|
|
854
|
-
import
|
|
855
|
-
import
|
|
769
|
+
import fs3 from "fs";
|
|
770
|
+
import path3 from "path";
|
|
856
771
|
import axios2 from "axios";
|
|
857
772
|
|
|
858
773
|
// src/utils/promise-pool.ts
|
|
@@ -894,7 +809,6 @@ var BASE_RETRY_DELAY_MS = 1e3;
|
|
|
894
809
|
var DEFAULT_DOWNLOAD_CONCURRENCY = 5;
|
|
895
810
|
|
|
896
811
|
// src/tools/figma-tool/images.ts
|
|
897
|
-
init_logger();
|
|
898
812
|
var fetchImages = async (nodes, fileId, token) => {
|
|
899
813
|
if (!fileId || !nodes?.length) {
|
|
900
814
|
return [];
|
|
@@ -1072,11 +986,11 @@ async function downloadImage(url, filename, imageDir, base64) {
|
|
|
1072
986
|
if (!imageDir || !filename) {
|
|
1073
987
|
return "";
|
|
1074
988
|
}
|
|
1075
|
-
if (!
|
|
1076
|
-
|
|
989
|
+
if (!fs3.existsSync(imageDir)) {
|
|
990
|
+
fs3.mkdirSync(imageDir, { recursive: true });
|
|
1077
991
|
}
|
|
1078
|
-
const filepath =
|
|
1079
|
-
|
|
992
|
+
const filepath = path3.join(imageDir, filename);
|
|
993
|
+
fs3.writeFileSync(filepath, Buffer.from(response.data));
|
|
1080
994
|
return filepath;
|
|
1081
995
|
}
|
|
1082
996
|
} catch (error) {
|
|
@@ -1535,21 +1449,21 @@ var FigmaTool = class {
|
|
|
1535
1449
|
if (!fileId || !nodeId || !token) {
|
|
1536
1450
|
return void 0;
|
|
1537
1451
|
}
|
|
1538
|
-
const
|
|
1539
|
-
if (!
|
|
1452
|
+
const document = await fetchFigmaNode(fileId, nodeId, token);
|
|
1453
|
+
if (!document || !document?.children?.length) {
|
|
1540
1454
|
return void 0;
|
|
1541
1455
|
}
|
|
1542
1456
|
const images = await fetchFigmaImages(fileId, nodeId, token);
|
|
1543
1457
|
const thumbnail = images?.[nodeId] || "";
|
|
1544
|
-
|
|
1545
|
-
const cleanedDocument = cleanFigma(
|
|
1458
|
+
document.thumbnailUrl = thumbnail;
|
|
1459
|
+
const cleanedDocument = cleanFigma(document);
|
|
1546
1460
|
return cleanedDocument;
|
|
1547
1461
|
}
|
|
1548
|
-
async downloadImages(fileId, token, imageDir,
|
|
1462
|
+
async downloadImages(fileId, token, imageDir, document) {
|
|
1549
1463
|
if (!fileId) {
|
|
1550
1464
|
return { successCount: 0, failCount: 0, imageNodesMap: /* @__PURE__ */ new Map() };
|
|
1551
1465
|
}
|
|
1552
|
-
const imageNodes = findImageNodes(
|
|
1466
|
+
const imageNodes = findImageNodes(document?.children || [], document?.absoluteBoundingBox);
|
|
1553
1467
|
const fetchedImages = await fetchImages(imageNodes, fileId, token);
|
|
1554
1468
|
if (!fetchedImages.length) {
|
|
1555
1469
|
return { successCount: 0, failCount: 0, imageNodesMap: /* @__PURE__ */ new Map() };
|
|
@@ -1596,101 +1510,17 @@ FigmaTool = __decorateElement(_init2, 0, "FigmaTool", _FigmaTool_decorators, Fig
|
|
|
1596
1510
|
__runInitializers(_init2, 1, FigmaTool);
|
|
1597
1511
|
var figmaTool = new FigmaTool();
|
|
1598
1512
|
|
|
1599
|
-
// src/nodes/process/index.ts
|
|
1600
|
-
init_logger();
|
|
1601
|
-
|
|
1602
1513
|
// src/utils/call-model.ts
|
|
1603
|
-
init_logger();
|
|
1604
1514
|
import { HumanMessage } from "@langchain/core/messages";
|
|
1605
1515
|
import { ChatOpenAI } from "@langchain/openai";
|
|
1606
|
-
function validateModelConfig(config) {
|
|
1607
|
-
if (!config || !config.provider || !config.model || !config.baseUrl || !config.apiKey) {
|
|
1608
|
-
throw new Error(
|
|
1609
|
-
`Invalid model configuration. Required fields: provider, model, baseUrl, apiKey. Please check your config.yaml file.`
|
|
1610
|
-
);
|
|
1611
|
-
}
|
|
1612
|
-
}
|
|
1613
|
-
async function callModel(options) {
|
|
1614
|
-
const { question, imageUrls, streaming = false, responseFormat, maxTokens } = options;
|
|
1615
|
-
if (!question || !question.trim()) {
|
|
1616
|
-
throw new Error("Question must be a non-empty string");
|
|
1617
|
-
}
|
|
1618
|
-
let config;
|
|
1619
|
-
try {
|
|
1620
|
-
config = getModelConfig();
|
|
1621
|
-
validateModelConfig(config);
|
|
1622
|
-
} catch (error) {
|
|
1623
|
-
if (error instanceof Error) {
|
|
1624
|
-
logger.printErrorLog(`Configuration error: ${error.message}`);
|
|
1625
|
-
}
|
|
1626
|
-
throw error;
|
|
1627
|
-
}
|
|
1628
|
-
try {
|
|
1629
|
-
const requestConfig = {
|
|
1630
|
-
modelName: config.model,
|
|
1631
|
-
apiKey: config.apiKey,
|
|
1632
|
-
configuration: {
|
|
1633
|
-
baseURL: config.baseUrl
|
|
1634
|
-
},
|
|
1635
|
-
...maxTokens && { maxTokens },
|
|
1636
|
-
temperature: 0.1,
|
|
1637
|
-
streaming,
|
|
1638
|
-
...streaming && {
|
|
1639
|
-
streamingOptions: {
|
|
1640
|
-
includeUsage: true
|
|
1641
|
-
}
|
|
1642
|
-
},
|
|
1643
|
-
...!streaming && { streamUsage: true },
|
|
1644
|
-
...responseFormat && { modelKwargs: { response_format: responseFormat } }
|
|
1645
|
-
};
|
|
1646
|
-
const agentModel = new ChatOpenAI(requestConfig);
|
|
1647
|
-
const contentParts = [];
|
|
1648
|
-
contentParts.push({ type: "text", text: question });
|
|
1649
|
-
if (imageUrls) {
|
|
1650
|
-
const urls = Array.isArray(imageUrls) ? imageUrls : [imageUrls];
|
|
1651
|
-
for (const url of urls) {
|
|
1652
|
-
if (url && typeof url === "string" && url.trim()) {
|
|
1653
|
-
contentParts.push({ type: "image_url", image_url: { url: url.trim() } });
|
|
1654
|
-
}
|
|
1655
|
-
}
|
|
1656
|
-
}
|
|
1657
|
-
const userMessage = new HumanMessage({
|
|
1658
|
-
content: contentParts.length > 1 ? contentParts : question
|
|
1659
|
-
});
|
|
1660
|
-
const message = await agentModel.invoke([userMessage]);
|
|
1661
|
-
if (!message.text) {
|
|
1662
|
-
throw new Error("Model returned empty response");
|
|
1663
|
-
}
|
|
1664
|
-
const debugConfig = getDebugConfig();
|
|
1665
|
-
const isDebugEnabled = debugConfig.enabled;
|
|
1666
|
-
if (isDebugEnabled) {
|
|
1667
|
-
const debugContent = [
|
|
1668
|
-
"------------config------------",
|
|
1669
|
-
JSON.stringify(requestConfig, null, 2),
|
|
1670
|
-
"------------request------------",
|
|
1671
|
-
JSON.stringify(contentParts, null, 2),
|
|
1672
|
-
"------------response------------",
|
|
1673
|
-
JSON.stringify(message.text, null, 2)
|
|
1674
|
-
].join("\n");
|
|
1675
|
-
writeFile(workspaceManager.path?.debug ?? "", `model_${(/* @__PURE__ */ new Date()).toISOString()}.md`, debugContent);
|
|
1676
|
-
}
|
|
1677
|
-
return message.text;
|
|
1678
|
-
} catch (error) {
|
|
1679
|
-
if (error instanceof Error) {
|
|
1680
|
-
logger.printErrorLog(`[${config.model}] Error details: ${error.message}`);
|
|
1681
|
-
if (error.stack) {
|
|
1682
|
-
logger.printTestLog(`[${config.model}] Stack trace: ${error.stack}`);
|
|
1683
|
-
}
|
|
1684
|
-
}
|
|
1685
|
-
throw new Error(`${config.model} model request failed: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
1686
|
-
}
|
|
1687
|
-
}
|
|
1688
1516
|
|
|
1689
1517
|
// src/nodes/process/structure/index.ts
|
|
1690
|
-
init_logger();
|
|
1691
1518
|
init_prompt();
|
|
1692
1519
|
|
|
1693
1520
|
// src/utils/naming.ts
|
|
1521
|
+
function toPascalCase(str) {
|
|
1522
|
+
return str.replace(/[^a-zA-Z0-9]+/g, " ").trim().split(/\s+/).map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()).join("");
|
|
1523
|
+
}
|
|
1694
1524
|
function toKebabCase(str) {
|
|
1695
1525
|
return str.replace(/([a-z0-9])([A-Z])/g, "$1-$2").replace(/[^a-zA-Z0-9]+/g, "-").toLowerCase().replace(/^-+|-+$/g, "");
|
|
1696
1526
|
}
|
|
@@ -1732,7 +1562,6 @@ function extractFiles(content) {
|
|
|
1732
1562
|
}
|
|
1733
1563
|
|
|
1734
1564
|
// src/nodes/process/structure/utils.ts
|
|
1735
|
-
init_logger();
|
|
1736
1565
|
function simplifyFigmaNodeForContent(node) {
|
|
1737
1566
|
const simple = {
|
|
1738
1567
|
id: node.id,
|
|
@@ -1825,7 +1654,7 @@ function extractHierarchicalNodesByIds(tree, idList, options) {
|
|
|
1825
1654
|
}
|
|
1826
1655
|
return false;
|
|
1827
1656
|
};
|
|
1828
|
-
const
|
|
1657
|
+
const processNode = (node) => {
|
|
1829
1658
|
if (!hasDescendantInList(node)) {
|
|
1830
1659
|
return [];
|
|
1831
1660
|
}
|
|
@@ -1835,7 +1664,7 @@ function extractHierarchicalNodesByIds(tree, idList, options) {
|
|
|
1835
1664
|
const filteredChildren = [];
|
|
1836
1665
|
for (const child of node.children) {
|
|
1837
1666
|
if (typeof child === "object" && child !== null && "id" in child) {
|
|
1838
|
-
const processedChildren =
|
|
1667
|
+
const processedChildren = processNode(child);
|
|
1839
1668
|
filteredChildren.push(...processedChildren);
|
|
1840
1669
|
}
|
|
1841
1670
|
}
|
|
@@ -1849,7 +1678,7 @@ function extractHierarchicalNodesByIds(tree, idList, options) {
|
|
|
1849
1678
|
if (node.children && Array.isArray(node.children)) {
|
|
1850
1679
|
for (const child of node.children) {
|
|
1851
1680
|
if (typeof child === "object" && child !== null && "id" in child) {
|
|
1852
|
-
const processedChildren =
|
|
1681
|
+
const processedChildren = processNode(child);
|
|
1853
1682
|
matchingDescendants.push(...processedChildren);
|
|
1854
1683
|
}
|
|
1855
1684
|
}
|
|
@@ -1859,7 +1688,7 @@ function extractHierarchicalNodesByIds(tree, idList, options) {
|
|
|
1859
1688
|
};
|
|
1860
1689
|
const nodeArray = Array.isArray(tree) ? tree : [tree];
|
|
1861
1690
|
for (const node of nodeArray) {
|
|
1862
|
-
const processedNodes =
|
|
1691
|
+
const processedNodes = processNode(node);
|
|
1863
1692
|
result.push(...processedNodes);
|
|
1864
1693
|
}
|
|
1865
1694
|
return result;
|
|
@@ -1929,8 +1758,8 @@ function postProcessStructure(structure, frames) {
|
|
|
1929
1758
|
traverse(node, void 0, 0);
|
|
1930
1759
|
});
|
|
1931
1760
|
}
|
|
1932
|
-
|
|
1933
|
-
if (!node || !node.children || node.children.length === 0) return;
|
|
1761
|
+
function extractComponentGroups(node) {
|
|
1762
|
+
if (!node || !node.children || node.children.length === 0) return /* @__PURE__ */ new Map();
|
|
1934
1763
|
const componentGroups = /* @__PURE__ */ new Map();
|
|
1935
1764
|
const validChildren = node.children.filter((c) => c && c.data);
|
|
1936
1765
|
validChildren.forEach((child) => {
|
|
@@ -1942,132 +1771,47 @@ async function populateComponentProps(node, frames, thumbnailUrl) {
|
|
|
1942
1771
|
componentGroups.get(name).push(child);
|
|
1943
1772
|
}
|
|
1944
1773
|
});
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
figmaData: figmaDataJson
|
|
1958
|
-
});
|
|
1959
|
-
const result = await callModel({
|
|
1960
|
-
question: prompt,
|
|
1961
|
-
imageUrls: thumbnailUrl,
|
|
1962
|
-
responseFormat: { type: "json_object" }
|
|
1774
|
+
return componentGroups;
|
|
1775
|
+
}
|
|
1776
|
+
function applyPropsAndStateToProtocol(parsed, node, compName, group, isList) {
|
|
1777
|
+
if (parsed && parsed.state && Array.isArray(parsed.state)) {
|
|
1778
|
+
if (isList) {
|
|
1779
|
+
if (!node.data.states) {
|
|
1780
|
+
node.data.states = [];
|
|
1781
|
+
}
|
|
1782
|
+
node.data.states.push({
|
|
1783
|
+
state: parsed.state,
|
|
1784
|
+
componentName: compName,
|
|
1785
|
+
componentPath: group[0]?.data.componentPath || ""
|
|
1963
1786
|
});
|
|
1964
|
-
const
|
|
1965
|
-
const
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
const processedComponentNames = /* @__PURE__ */ new Set();
|
|
1979
|
-
for (const child of originalChildren) {
|
|
1980
|
-
const childName = child.data.componentName;
|
|
1981
|
-
if (childName === compName) {
|
|
1982
|
-
if (!processedComponentNames.has(childName)) {
|
|
1983
|
-
child.data.name = childName;
|
|
1984
|
-
child.id = childName;
|
|
1985
|
-
const cleanKebabName = toKebabCase(childName);
|
|
1986
|
-
child.data.kebabName = cleanKebabName;
|
|
1987
|
-
delete child.data.path;
|
|
1988
|
-
if (parsed.props && Array.isArray(parsed.props)) {
|
|
1989
|
-
child.data.props = parsed.props;
|
|
1990
|
-
}
|
|
1991
|
-
newChildren.push(child);
|
|
1992
|
-
processedComponentNames.add(childName);
|
|
1993
|
-
}
|
|
1994
|
-
} else {
|
|
1995
|
-
newChildren.push(child);
|
|
1787
|
+
const originalChildren = node.children || [];
|
|
1788
|
+
const newChildren = [];
|
|
1789
|
+
const processedComponentNames = /* @__PURE__ */ new Set();
|
|
1790
|
+
for (const child of originalChildren) {
|
|
1791
|
+
const childName = child.data.componentName;
|
|
1792
|
+
if (childName === compName) {
|
|
1793
|
+
if (!processedComponentNames.has(childName)) {
|
|
1794
|
+
child.data.name = childName;
|
|
1795
|
+
child.id = childName;
|
|
1796
|
+
const cleanKebabName = toKebabCase(childName);
|
|
1797
|
+
child.data.kebabName = cleanKebabName;
|
|
1798
|
+
delete child.data.path;
|
|
1799
|
+
if (parsed.props && Array.isArray(parsed.props)) {
|
|
1800
|
+
child.data.props = parsed.props;
|
|
1996
1801
|
}
|
|
1802
|
+
newChildren.push(child);
|
|
1803
|
+
processedComponentNames.add(childName);
|
|
1997
1804
|
}
|
|
1998
|
-
|
|
1805
|
+
} else {
|
|
1806
|
+
newChildren.push(child);
|
|
1999
1807
|
}
|
|
2000
1808
|
}
|
|
2001
|
-
|
|
2002
|
-
logger.printErrorLog(
|
|
2003
|
-
`Failed to extract data list for ${compName} in ${containerName}: ${e instanceof Error ? e.message : String(e)}`
|
|
2004
|
-
);
|
|
1809
|
+
node.children = newChildren;
|
|
2005
1810
|
}
|
|
2006
1811
|
}
|
|
2007
|
-
for (const child of node.children) {
|
|
2008
|
-
await populateComponentProps(child, frames, thumbnailUrl);
|
|
2009
|
-
}
|
|
2010
1812
|
}
|
|
2011
1813
|
|
|
2012
|
-
// src/nodes/process/structure/index.ts
|
|
2013
|
-
var generateStructure = async (figma) => {
|
|
2014
|
-
const frames = figma.frames || figma.children;
|
|
2015
|
-
const imageWidth = figma.absoluteBoundingBox?.width;
|
|
2016
|
-
const thumbnailUrl = figma.thumbnailUrl;
|
|
2017
|
-
if (!frames || frames.length === 0) {
|
|
2018
|
-
logger.printErrorLog("No processed frames found in state");
|
|
2019
|
-
throw new Error("No processed frames found");
|
|
2020
|
-
}
|
|
2021
|
-
logger.printInfoLog("Starting structure analysis...");
|
|
2022
|
-
try {
|
|
2023
|
-
const positions = extractNodePositionsHierarchical(frames);
|
|
2024
|
-
const positionsJson = JSON.stringify(positions);
|
|
2025
|
-
const prompt = generateStructurePrompt({
|
|
2026
|
-
positions: positionsJson,
|
|
2027
|
-
width: imageWidth ? String(imageWidth) : "1440"
|
|
2028
|
-
});
|
|
2029
|
-
logger.printInfoLog("Calling AI model to generate component structure...");
|
|
2030
|
-
const structureResult = await callModel({
|
|
2031
|
-
question: prompt,
|
|
2032
|
-
imageUrls: thumbnailUrl,
|
|
2033
|
-
responseFormat: { type: "json_object" },
|
|
2034
|
-
maxTokens: 20240
|
|
2035
|
-
});
|
|
2036
|
-
const jsonContent = extractJSON(structureResult);
|
|
2037
|
-
const parsedStructure = JSON.parse(jsonContent);
|
|
2038
|
-
logger.printInfoLog("Processing structure tree...");
|
|
2039
|
-
postProcessStructure(parsedStructure, frames);
|
|
2040
|
-
const protocol = Array.isArray(parsedStructure) ? parsedStructure[0] : parsedStructure;
|
|
2041
|
-
if (frames && protocol) {
|
|
2042
|
-
logger.printInfoLog("Extracting component properties and states...");
|
|
2043
|
-
await populateComponentProps(protocol, frames, thumbnailUrl);
|
|
2044
|
-
}
|
|
2045
|
-
logger.printSuccessLog("Component structure generated successfully");
|
|
2046
|
-
return protocol;
|
|
2047
|
-
} catch (error) {
|
|
2048
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
2049
|
-
logger.printErrorLog(`Error generating component structure: ${errorMessage}`);
|
|
2050
|
-
throw new Error(`Failed to parse component structure: ${errorMessage}`);
|
|
2051
|
-
}
|
|
2052
|
-
};
|
|
2053
|
-
|
|
2054
1814
|
// src/nodes/process/index.ts
|
|
2055
|
-
var generateProtocol = async (state) => {
|
|
2056
|
-
const assetsDir = workspaceManager.resolveAppSrc(state.workspace, "assets");
|
|
2057
|
-
const processDir = state.workspace.process;
|
|
2058
|
-
const { document: document2, imageNodesMap } = await executeFigmaAndImagesActions(state.urlInfo, assetsDir, processDir);
|
|
2059
|
-
const simplifiedDocument = figmaTool.simplifyImageNodes(document2, imageNodesMap);
|
|
2060
|
-
const processedStyleDocument = figmaTool.processedStyle(simplifiedDocument);
|
|
2061
|
-
const protocol = await generateStructure(processedStyleDocument);
|
|
2062
|
-
writeFile(state.workspace.process, "protocol.json", JSON.stringify(protocol, null, 2));
|
|
2063
|
-
logger.printInfoLog(`Please check the output in the workspace: ${state.workspace.process}`);
|
|
2064
|
-
return {
|
|
2065
|
-
protocol,
|
|
2066
|
-
figmaInfo: {
|
|
2067
|
-
thumbnail: processedStyleDocument?.thumbnailUrl || document2?.thumbnailUrl || ""
|
|
2068
|
-
}
|
|
2069
|
-
};
|
|
2070
|
-
};
|
|
2071
1815
|
var executeFigmaAndImagesActions = async (urlInfo, assetsDir, processDir) => {
|
|
2072
1816
|
const { fileId, nodeId } = urlInfo;
|
|
2073
1817
|
if (!fileId || !nodeId) {
|
|
@@ -2077,13 +1821,13 @@ var executeFigmaAndImagesActions = async (urlInfo, assetsDir, processDir) => {
|
|
|
2077
1821
|
if (!token) {
|
|
2078
1822
|
throw new Error("Figma API token is required");
|
|
2079
1823
|
}
|
|
2080
|
-
const
|
|
2081
|
-
if (!
|
|
1824
|
+
const document = await figmaTool.fetchAndClean(fileId, nodeId, token);
|
|
1825
|
+
if (!document) {
|
|
2082
1826
|
throw new Error("Failed to fetch and clean Figma document");
|
|
2083
1827
|
}
|
|
2084
|
-
writeFile(processDir, "figma.json", JSON.stringify(
|
|
1828
|
+
writeFile(processDir, "figma.json", JSON.stringify(document, null, 2));
|
|
2085
1829
|
logger.printSuccessLog(`Figma document fetched and cleaned successfully`);
|
|
2086
|
-
const downloadResult = await figmaTool.downloadImages(fileId, token, assetsDir,
|
|
1830
|
+
const downloadResult = await figmaTool.downloadImages(fileId, token, assetsDir, document);
|
|
2087
1831
|
const { successCount, failCount, imageNodesMap } = downloadResult;
|
|
2088
1832
|
if (successCount) {
|
|
2089
1833
|
logger.printSuccessLog(`Downloaded ${successCount} images`);
|
|
@@ -2094,3525 +1838,65 @@ var executeFigmaAndImagesActions = async (urlInfo, assetsDir, processDir) => {
|
|
|
2094
1838
|
const resultsArray = Array.from(imageNodesMap.values());
|
|
2095
1839
|
writeFile(processDir, "images.json", JSON.stringify(resultsArray, null, 2));
|
|
2096
1840
|
return {
|
|
2097
|
-
document
|
|
1841
|
+
document,
|
|
2098
1842
|
imageNodesMap
|
|
2099
1843
|
};
|
|
2100
1844
|
};
|
|
2101
1845
|
|
|
2102
|
-
// src/
|
|
2103
|
-
|
|
2104
|
-
import path11 from "path";
|
|
2105
|
-
|
|
2106
|
-
// src/nodes/validation/constants.ts
|
|
2107
|
-
var TARGET_MAE = 3;
|
|
2108
|
-
var POSITION_THRESHOLD = 1;
|
|
2109
|
-
var DEFAULT_TIMEOUT = 3e4;
|
|
2110
|
-
var METRIC_DECIMAL_PLACES = 2;
|
|
2111
|
-
var DEFAULT_VIEWPORT = { width: 1440, height: 900 };
|
|
2112
|
-
var MAX_ITERATIONS = 3;
|
|
2113
|
-
var HEADLESS = true;
|
|
2114
|
-
var SELECTOR_WAIT_TIMEOUT = 5e3;
|
|
2115
|
-
var DEFAULT_VALIDATION_LOOP_CONFIG = {
|
|
2116
|
-
maxIterations: MAX_ITERATIONS,
|
|
2117
|
-
targetMae: TARGET_MAE,
|
|
2118
|
-
positionThreshold: POSITION_THRESHOLD,
|
|
2119
|
-
browserTimeout: DEFAULT_TIMEOUT,
|
|
2120
|
-
defaultViewportWidth: DEFAULT_VIEWPORT.width,
|
|
2121
|
-
defaultViewportHeight: DEFAULT_VIEWPORT.height,
|
|
2122
|
-
headless: HEADLESS
|
|
2123
|
-
};
|
|
2124
|
-
var DEFAULT_PORT = 5173;
|
|
2125
|
-
function buildDevServerUrl(port) {
|
|
2126
|
-
return `http://localhost:${port}`;
|
|
2127
|
-
}
|
|
2128
|
-
|
|
2129
|
-
// src/nodes/validation/index.ts
|
|
2130
|
-
init_logger();
|
|
2131
|
-
|
|
2132
|
-
// src/nodes/validation/core/validation-loop.ts
|
|
2133
|
-
import * as fs7 from "fs";
|
|
2134
|
-
import * as path10 from "path";
|
|
2135
|
-
init_logger();
|
|
2136
|
-
|
|
2137
|
-
// src/agents/commit-agent/index.ts
|
|
2138
|
-
import { Agent as Agent2 } from "evoltagent";
|
|
1846
|
+
// src/skill-api.ts
|
|
1847
|
+
init_prompt();
|
|
2139
1848
|
|
|
2140
|
-
// src/
|
|
2141
|
-
var
|
|
1849
|
+
// src/nodes/code/prompt.ts
|
|
1850
|
+
var STYLING_GUIDELINES = `
|
|
1851
|
+
- **Style Consistency**: Implement styles using the technical stack and libraries listed in <styling>.
|
|
1852
|
+
- **Strict Restriction**: Absolutely ONLY use the technical stack and libraries listed in <styling>. Do NOT use any other styling methods, libraries, or frameworks (e.g., if clsx is not listed, do not use clsx).
|
|
1853
|
+
- **Default Styling**: If <styling> is empty or does not contain specific libraries, DEFAULT to standard vanilla CSS.
|
|
1854
|
+
|
|
1855
|
+
- **Tailwind CSS + CSS Modules (CRITICAL)**:
|
|
1856
|
+
- If the stack includes BOTH Tailwind and CSS Modules (Less/SCSS), use them correctly:
|
|
1857
|
+
1. **Tailwind utilities**: Use DIRECTLY in JSX className (e.g., \`className="flex items-center gap-4"\`)
|
|
1858
|
+
2. **CSS Modules**: Use ONLY for complex styles that can't be expressed with Tailwind utilities (e.g., gradients, animations, pseudo-elements)
|
|
1859
|
+
3. **NEVER use \`@apply\` in CSS Module files** - it's a Tailwind-specific directive that doesn't work in Less/SCSS
|
|
1860
|
+
4. Example correct usage:
|
|
1861
|
+
TSX: \`<div className={\`flex \${styles.customGradient}\`}>\`
|
|
1862
|
+
Less: \`.customGradient { background: linear-gradient(...); }\`
|
|
1863
|
+
|
|
1864
|
+
- **CSS Modules Only**: If the tech stack specifies CSS Modules without Tailwind:
|
|
1865
|
+
1. Create a corresponding style file (e.g., \`index.module.less\`, \`index.module.scss\`, or \`index.module.css\`)
|
|
1866
|
+
2. Import it as \`import styles from './index.module.[ext]';\` in the TSX
|
|
1867
|
+
3. Define all styles in the style file using standard CSS/Less/SCSS syntax
|
|
1868
|
+
4. Use \`styles.className\` in JSX`;
|
|
1869
|
+
var ASSETS_HANDLING = `
|
|
1870
|
+
- **CRITICAL**: For any image URL starting with \`@/assets\`, you MUST import it at the top of the file.
|
|
1871
|
+
- **Asset Name Matching**:
|
|
1872
|
+
- Check the \`<available_assets>\` list for actual filenames in the project.
|
|
1873
|
+
- Asset filenames follow the pattern: \`kebab-case-name-id1-id2.ext\` (e.g., "Star 2.svg" \u2192 "star-2-1-2861.svg")
|
|
1874
|
+
- Match the base name (ignoring spaces, case, and ID suffix): "@/assets/arXiv.svg" \u2192 look for "arxiv-*.svg" in the list
|
|
1875
|
+
- Use the EXACT filename from the available assets list in your import.
|
|
1876
|
+
- Example: If available_assets contains "arxiv-1-2956.svg", use:
|
|
1877
|
+
\`import ArXivIcon from '@/assets/arxiv-1-2956.svg';\`
|
|
1878
|
+
- **Usage**: \`<img src={ArXivIcon} />\`, do not use backgroundImage property.
|
|
1879
|
+
- **NEVER** use the string path directly in JSX or styles.`;
|
|
1880
|
+
var DOM_IDS_REQUIREMENT = `
|
|
1881
|
+
- Assign \`id\` attributes to the main container and any internal elements, matching \`frame_details\`.`;
|
|
1882
|
+
var REACT_IMPORT_RULE = `
|
|
1883
|
+
- Do **NOT** include \`import React from 'react';\` at the top of the file.`;
|
|
1884
|
+
var FILE_NAMING_CONVENTION = `
|
|
1885
|
+
- ALWAYS name the main component file \`index.tsx\`.
|
|
1886
|
+
- ALWAYS name the style file (if applicable) \`index.module.[css|less|scss]\`.
|
|
1887
|
+
- NEVER use PascalCase or other names for filenames (e.g., DO NOT use \`MainFrame.tsx\` or \`Button.tsx\`).`;
|
|
1888
|
+
var OUTPUT_FORMAT = `
|
|
1889
|
+
<output_format>
|
|
1890
|
+
If only one file (TSX) is needed:
|
|
1891
|
+
\`\`\`tsx
|
|
1892
|
+
// code...
|
|
1893
|
+
\`\`\`
|
|
2142
1894
|
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
1. Validate repository:
|
|
2149
|
-
- Run GitTool.status(cwd=appPath)
|
|
2150
|
-
- If not a git repository, run GitTool.init(cwd=appPath), then re-run status
|
|
2151
|
-
|
|
2152
|
-
2. Check for changes:
|
|
2153
|
-
- Run GitTool.diff(cwd=appPath) to see unstaged changes
|
|
2154
|
-
- If working tree is clean (no changes):
|
|
2155
|
-
* Stop and return: "No changes to commit"
|
|
2156
|
-
* DO NOT create an empty commit
|
|
2157
|
-
|
|
2158
|
-
3. Stage all changes:
|
|
2159
|
-
- Run GitTool.add(files=".", cwd=appPath)
|
|
2160
|
-
|
|
2161
|
-
4. Analyze changes and generate commit message:
|
|
2162
|
-
Run GitTool.diff(cwd=appPath) after staging to analyze what changed.
|
|
2163
|
-
|
|
2164
|
-
Generate a conventional commit message following these rules:
|
|
2165
|
-
|
|
2166
|
-
**Message format:**
|
|
2167
|
-
- If iteration is undefined: "feat: [Initial] first commit"
|
|
2168
|
-
- If iteration is defined: "fix: [Iteration N] <description>"
|
|
2169
|
-
|
|
2170
|
-
**Description guidelines:**
|
|
2171
|
-
Analyze the diff to determine what type of changes were made:
|
|
2172
|
-
|
|
2173
|
-
a) Component layout/style fixes:
|
|
2174
|
-
- Look for changes in component files (src/components/*.tsx, etc.)
|
|
2175
|
-
- Extract component names from file paths (e.g., src/components/Header.tsx \u2192 Header)
|
|
2176
|
-
- Format: "fix misaligned components ComponentA, ComponentB"
|
|
2177
|
-
- List up to 3 components, use "and N more" if more than 3
|
|
2178
|
-
- Example: "fix: [Iteration 1] fix misaligned components Header, Footer, and 2 more"
|
|
2179
|
-
|
|
2180
|
-
b) Build/compilation error fixes:
|
|
2181
|
-
- Look for package.json changes \u2192 "resolve dependency issues"
|
|
2182
|
-
- Look for import/export errors \u2192 "resolve import errors"
|
|
2183
|
-
- Look for TypeScript errors \u2192 "resolve type errors"
|
|
2184
|
-
- Example: "fix: [Iteration 2] resolve build error"
|
|
2185
|
-
|
|
2186
|
-
c) Mixed changes (both component fixes AND build fixes):
|
|
2187
|
-
- Prioritize component fixes in the message
|
|
2188
|
-
- Example: "fix: [Iteration 1] fix misaligned components xxx and resolve yyy error"
|
|
2189
|
-
|
|
2190
|
-
5. Create commit:
|
|
2191
|
-
- Use temporary identity (do NOT modify global git config):
|
|
2192
|
-
* user.name = "CodeRio"
|
|
2193
|
-
* user.email = ""
|
|
2194
|
-
- Run GitTool.commit(message, cwd=appPath, config={...})
|
|
2195
|
-
</workflow>
|
|
2196
|
-
|
|
2197
|
-
<example>
|
|
2198
|
-
Example tool call format:
|
|
2199
|
-
|
|
2200
|
-
GitTool.commit({
|
|
2201
|
-
"message": "feat: [Initial] generate webpage",
|
|
2202
|
-
"cwd": "/absolute/path/to/app",
|
|
2203
|
-
"config": {"user.name": "CodeRio", "user.email": ""}
|
|
2204
|
-
})
|
|
2205
|
-
|
|
2206
|
-
IMPORTANT: The config parameter must be a JSON object with keys like "user.name" and "user.email".
|
|
2207
|
-
DO NOT use XML-style tags like <user.name>CodeRio</user.name>.
|
|
2208
|
-
</example>
|
|
2209
|
-
|
|
2210
|
-
<output>
|
|
2211
|
-
Respond with a short summary in <TaskCompletion> tags.
|
|
2212
|
-
Include whether a commit was created and the final commit message.
|
|
2213
|
-
</output>`;
|
|
2214
|
-
|
|
2215
|
-
// src/agents/commit-agent/index.ts
|
|
2216
|
-
function createCommitAgent() {
|
|
2217
|
-
const modelConfig = {
|
|
2218
|
-
...getModelConfig(),
|
|
2219
|
-
contextWindowTokens: AGENT_CONTEXT_WINDOW_TOKENS,
|
|
2220
|
-
maxOutputTokens: MAX_OUTPUT_TOKENS
|
|
2221
|
-
};
|
|
2222
|
-
return new Agent2({
|
|
2223
|
-
name: "CommitAgent",
|
|
2224
|
-
profile: "A helpful assistant that commits local changes in a repository.",
|
|
2225
|
-
system: COMMIT_AGENT_SYSTEM_PROMPT,
|
|
2226
|
-
tools: ["GitTool.init", "GitTool.status", "GitTool.add", "GitTool.diff", "GitTool.commit"],
|
|
2227
|
-
modelConfig,
|
|
2228
|
-
verbose: 1,
|
|
2229
|
-
toolcallManagerPoolSize: 1
|
|
2230
|
-
});
|
|
2231
|
-
}
|
|
2232
|
-
|
|
2233
|
-
// src/agents/commit-agent/instruction.ts
|
|
2234
|
-
function formatGitCommitInstruction(params) {
|
|
2235
|
-
const { appPath, iteration } = params;
|
|
2236
|
-
return `appPath: ${appPath}
|
|
2237
|
-
iteration: ${iteration ?? "undefined"}
|
|
2238
|
-
|
|
2239
|
-
TASK:
|
|
2240
|
-
Check for changes and commit if there are any modifications in the app directory.`;
|
|
2241
|
-
}
|
|
2242
|
-
|
|
2243
|
-
// src/nodes/validation/commit/index.ts
|
|
2244
|
-
init_logger();
|
|
2245
|
-
import path5 from "path";
|
|
2246
|
-
async function commit(options) {
|
|
2247
|
-
try {
|
|
2248
|
-
if (!options?.appPath) {
|
|
2249
|
-
throw new Error("commit() requires options.appPath");
|
|
2250
|
-
}
|
|
2251
|
-
const appPath = path5.resolve(options.appPath);
|
|
2252
|
-
const agent = createCommitAgent();
|
|
2253
|
-
const instruction = formatGitCommitInstruction({
|
|
2254
|
-
appPath,
|
|
2255
|
-
iteration: options.iteration
|
|
2256
|
-
});
|
|
2257
|
-
await agent.run(instruction);
|
|
2258
|
-
logger.printSuccessLog("Commit completed!");
|
|
2259
|
-
return {
|
|
2260
|
-
success: true,
|
|
2261
|
-
message: "Commit workflow completed"
|
|
2262
|
-
};
|
|
2263
|
-
} catch (error) {
|
|
2264
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
2265
|
-
logger.printErrorLog(`Commit failed: ${errorMessage}`);
|
|
2266
|
-
return {
|
|
2267
|
-
success: false,
|
|
2268
|
-
message: errorMessage
|
|
2269
|
-
};
|
|
2270
|
-
}
|
|
2271
|
-
}
|
|
2272
|
-
|
|
2273
|
-
// src/agents/judger-agent/index.ts
|
|
2274
|
-
import { Agent as Agent3 } from "evoltagent";
|
|
2275
|
-
|
|
2276
|
-
// src/tools/history-tool/index.ts
|
|
2277
|
-
import { tools as tools3 } from "evoltagent";
|
|
2278
|
-
var _HistoryTool_decorators, _init3;
|
|
2279
|
-
_HistoryTool_decorators = [tools3({
|
|
2280
|
-
getComponentHistory: {
|
|
2281
|
-
description: "Get position and fix history for a component. Shows how the component's position and error evolved across iterations, and what fixes were applied. Helps identify if previous fixes helped or hurt",
|
|
2282
|
-
params: [{ name: "componentId", type: "string", description: "Component ID to get history for" }],
|
|
2283
|
-
returns: { type: "string", description: "Formatted string with complete history of positions, errors, and fixes" },
|
|
2284
|
-
examples: [
|
|
2285
|
-
`<HistoryTool.getComponentHistory>
|
|
2286
|
-
<componentId>HeroSection</componentId>
|
|
2287
|
-
</HistoryTool.getComponentHistory>`
|
|
2288
|
-
]
|
|
2289
|
-
},
|
|
2290
|
-
getIterationSummary: {
|
|
2291
|
-
description: "Get summary of what was changed in a specific iteration. Shows all components that were fixed in that iteration and what fixes were applied. Useful for understanding what went wrong in a previous iteration",
|
|
2292
|
-
params: [{ name: "iteration", type: "number", description: "Iteration number (1-indexed)" }],
|
|
2293
|
-
returns: { type: "string", description: "Formatted string with summary of changes in that iteration" },
|
|
2294
|
-
examples: [
|
|
2295
|
-
`<HistoryTool.getIterationSummary>
|
|
2296
|
-
<iteration>2</iteration>
|
|
2297
|
-
</HistoryTool.getIterationSummary>`
|
|
2298
|
-
]
|
|
2299
|
-
}
|
|
2300
|
-
})];
|
|
2301
|
-
var HistoryTool = class {
|
|
2302
|
-
_history = {};
|
|
2303
|
-
/**
|
|
2304
|
-
* Initialize with component history from previous iterations.
|
|
2305
|
-
*
|
|
2306
|
-
* @param history - Dict mapping component_id to list of iteration records
|
|
2307
|
-
*/
|
|
2308
|
-
setContext(history) {
|
|
2309
|
-
this._history = history;
|
|
2310
|
-
}
|
|
2311
|
-
/**
|
|
2312
|
-
* Get position and fix history for a component.
|
|
2313
|
-
*
|
|
2314
|
-
* Shows how the component's position and error evolved across iterations,
|
|
2315
|
-
* and what fixes were applied. Helps identify if previous fixes helped or hurt.
|
|
2316
|
-
*
|
|
2317
|
-
* @param componentId - Component ID to get history for
|
|
2318
|
-
* @returns Formatted string with complete history of positions, errors, and fixes
|
|
2319
|
-
*/
|
|
2320
|
-
getComponentHistory(componentId) {
|
|
2321
|
-
if (!this._history || Object.keys(this._history).length === 0) {
|
|
2322
|
-
return "No history available (this is iteration 1)";
|
|
2323
|
-
}
|
|
2324
|
-
if (!(componentId in this._history)) {
|
|
2325
|
-
return `No history found for component '${componentId}' (first time misaligned)`;
|
|
2326
|
-
}
|
|
2327
|
-
const historyEntries = this._history[componentId];
|
|
2328
|
-
if (!historyEntries || historyEntries.length === 0) {
|
|
2329
|
-
return `Empty history for component '${componentId}'`;
|
|
2330
|
-
}
|
|
2331
|
-
const lines = [`History for ${componentId}:`, ""];
|
|
2332
|
-
for (let i = 0; i < historyEntries.length; i++) {
|
|
2333
|
-
const entry = historyEntries[i];
|
|
2334
|
-
if (!entry) continue;
|
|
2335
|
-
const iteration = entry.iteration ?? "?";
|
|
2336
|
-
const position = entry.position ?? [0, 0];
|
|
2337
|
-
const error = entry.error ?? [0, 0];
|
|
2338
|
-
const fixApplied = entry.fixApplied ? entry.fixApplied.join("\n ") : "None";
|
|
2339
|
-
lines.push(`Iteration ${iteration}:`);
|
|
2340
|
-
lines.push(` Position: (${position[0].toFixed(1)}, ${position[1].toFixed(1)}) px`);
|
|
2341
|
-
lines.push(` Error: (${error[0].toFixed(1)}, ${error[1].toFixed(1)}) px`);
|
|
2342
|
-
lines.push(` Fix Applied:
|
|
2343
|
-
${fixApplied}`);
|
|
2344
|
-
if (i > 0) {
|
|
2345
|
-
const prevEntry = historyEntries[i - 1];
|
|
2346
|
-
if (prevEntry) {
|
|
2347
|
-
const prevErrorMagnitude = prevEntry.error[0] + prevEntry.error[1];
|
|
2348
|
-
const currErrorMagnitude = error[0] + error[1];
|
|
2349
|
-
if (currErrorMagnitude > prevErrorMagnitude) {
|
|
2350
|
-
lines.push(
|
|
2351
|
-
` Warning: REGRESSION: Error increased from ${prevErrorMagnitude.toFixed(1)}px to ${currErrorMagnitude.toFixed(1)}px`
|
|
2352
|
-
);
|
|
2353
|
-
}
|
|
2354
|
-
}
|
|
2355
|
-
}
|
|
2356
|
-
lines.push("");
|
|
2357
|
-
}
|
|
2358
|
-
return lines.join("\n");
|
|
2359
|
-
}
|
|
2360
|
-
/**
|
|
2361
|
-
* Get summary of what was changed in a specific iteration.
|
|
2362
|
-
*
|
|
2363
|
-
* Shows all components that were fixed in that iteration and what fixes
|
|
2364
|
-
* were applied. Useful for understanding what went wrong in a previous iteration.
|
|
2365
|
-
*
|
|
2366
|
-
* @param iteration - Iteration number (1-indexed)
|
|
2367
|
-
* @returns Formatted string with summary of changes in that iteration
|
|
2368
|
-
*/
|
|
2369
|
-
getIterationSummary(iteration) {
|
|
2370
|
-
if (!this._history || Object.keys(this._history).length === 0) {
|
|
2371
|
-
return `No history available (iteration ${iteration} has not completed yet)`;
|
|
2372
|
-
}
|
|
2373
|
-
const changes = [];
|
|
2374
|
-
for (const [componentId, entries] of Object.entries(this._history)) {
|
|
2375
|
-
for (const entry of entries) {
|
|
2376
|
-
if (entry.iteration === iteration) {
|
|
2377
|
-
changes.push({
|
|
2378
|
-
componentId,
|
|
2379
|
-
position: entry.position ?? [0, 0],
|
|
2380
|
-
error: entry.error ?? [0, 0],
|
|
2381
|
-
fixApplied: entry.fixApplied ?? ["None"]
|
|
2382
|
-
});
|
|
2383
|
-
break;
|
|
2384
|
-
}
|
|
2385
|
-
}
|
|
2386
|
-
}
|
|
2387
|
-
if (changes.length === 0) {
|
|
2388
|
-
return `No changes recorded for iteration ${iteration}`;
|
|
2389
|
-
}
|
|
2390
|
-
const lines = [`Iteration ${iteration} Summary:`, `Total components modified: ${changes.length}`, ""];
|
|
2391
|
-
for (const change of changes) {
|
|
2392
|
-
lines.push(`${change.componentId}:`);
|
|
2393
|
-
lines.push(` Error: (${change.error[0].toFixed(1)}, ${change.error[1].toFixed(1)}) px`);
|
|
2394
|
-
lines.push(` Fix:
|
|
2395
|
-
${change.fixApplied.join("\n ")}`);
|
|
2396
|
-
lines.push("");
|
|
2397
|
-
}
|
|
2398
|
-
return lines.join("\n");
|
|
2399
|
-
}
|
|
2400
|
-
};
|
|
2401
|
-
_init3 = __decoratorStart(null);
|
|
2402
|
-
HistoryTool = __decorateElement(_init3, 0, "HistoryTool", _HistoryTool_decorators, HistoryTool);
|
|
2403
|
-
__runInitializers(_init3, 1, HistoryTool);
|
|
2404
|
-
|
|
2405
|
-
// src/tools/hierarchy-tool/index.ts
|
|
2406
|
-
import { tools as tools4 } from "evoltagent";
|
|
2407
|
-
|
|
2408
|
-
// src/nodes/validation/utils/tree/tree-traversal.ts
|
|
2409
|
-
function getNodeId(node) {
|
|
2410
|
-
return node?.componentId || node?.id || void 0;
|
|
2411
|
-
}
|
|
2412
|
-
function findInTree(data, targetId, idKeys = ["id", "componentId"]) {
|
|
2413
|
-
if (data === null || data === void 0) {
|
|
2414
|
-
return void 0;
|
|
2415
|
-
}
|
|
2416
|
-
if (typeof data === "object" && !Array.isArray(data)) {
|
|
2417
|
-
const dict = data;
|
|
2418
|
-
for (const idKey of idKeys) {
|
|
2419
|
-
if (dict[idKey] === targetId) {
|
|
2420
|
-
return dict;
|
|
2421
|
-
}
|
|
2422
|
-
}
|
|
2423
|
-
for (const value of Object.values(dict)) {
|
|
2424
|
-
const result = findInTree(value, targetId, idKeys);
|
|
2425
|
-
if (result) {
|
|
2426
|
-
return result;
|
|
2427
|
-
}
|
|
2428
|
-
}
|
|
2429
|
-
} else if (Array.isArray(data)) {
|
|
2430
|
-
for (const item of data) {
|
|
2431
|
-
const result = findInTree(item, targetId, idKeys);
|
|
2432
|
-
if (result) {
|
|
2433
|
-
return result;
|
|
2434
|
-
}
|
|
2435
|
-
}
|
|
2436
|
-
}
|
|
2437
|
-
return void 0;
|
|
2438
|
-
}
|
|
2439
|
-
|
|
2440
|
-
// src/tools/hierarchy-tool/index.ts
|
|
2441
|
-
var _HierarchyTool_decorators, _init4;
|
|
2442
|
-
_HierarchyTool_decorators = [tools4({
|
|
2443
|
-
lookup: {
|
|
2444
|
-
description: "Get file path, parent, siblings, and children for a component. Use this to find the correct file path before reading component files.",
|
|
2445
|
-
params: [{ name: "componentId", type: "string", description: "Component ID to look up" }],
|
|
2446
|
-
returns: { type: "string", description: "Formatted string with file path, parent (with its file path), siblings, and children" },
|
|
2447
|
-
examples: [
|
|
2448
|
-
`<HierarchyTool.lookup>
|
|
2449
|
-
<componentId>HeroSection</componentId>
|
|
2450
|
-
</HierarchyTool.lookup>`
|
|
2451
|
-
]
|
|
2452
|
-
},
|
|
2453
|
-
getSharedInstances: {
|
|
2454
|
-
description: "Find all component instances using a specific file path. Useful for identifying which components share the same implementation file (e.g., StartButton and ApiButton both using button/index.tsx). ALWAYS check this before editing a shared file.",
|
|
2455
|
-
params: [{ name: "filePath", type: "string", description: "Filesystem path to search for (can be relative or absolute)" }],
|
|
2456
|
-
returns: { type: "string", description: "Formatted string with all component instances using the file" },
|
|
2457
|
-
examples: [
|
|
2458
|
-
`<HierarchyTool.getSharedInstances>
|
|
2459
|
-
<filePath>button/index.tsx</filePath>
|
|
2460
|
-
</HierarchyTool.getSharedInstances>`
|
|
2461
|
-
]
|
|
2462
|
-
}
|
|
2463
|
-
})];
|
|
2464
|
-
var HierarchyTool = class {
|
|
2465
|
-
_structureTree = {};
|
|
2466
|
-
_componentPaths = {};
|
|
2467
|
-
constructor() {
|
|
2468
|
-
this._structureTree = {};
|
|
2469
|
-
this._componentPaths = {};
|
|
2470
|
-
}
|
|
2471
|
-
/**
|
|
2472
|
-
* Initialize with page structure and path mapping.
|
|
2473
|
-
*/
|
|
2474
|
-
setContext(structureTree, componentPaths) {
|
|
2475
|
-
this._structureTree = structureTree;
|
|
2476
|
-
this._componentPaths = componentPaths;
|
|
2477
|
-
}
|
|
2478
|
-
lookup(componentId) {
|
|
2479
|
-
const node = findInTree(this._structureTree, componentId);
|
|
2480
|
-
if (!node) {
|
|
2481
|
-
return `Component '${componentId}' not found in structure tree`;
|
|
2482
|
-
}
|
|
2483
|
-
const parentInfo = this._findParent(this._structureTree, componentId);
|
|
2484
|
-
const parentStr = parentInfo ? `Parent: ${parentInfo.id}` : "Parent: None (root component)";
|
|
2485
|
-
const parentPathStr = parentInfo && this._componentPaths[parentInfo.id] ? `Parent File: ${this._componentPaths[parentInfo.id]}` : "";
|
|
2486
|
-
let siblings = [];
|
|
2487
|
-
if (parentInfo) {
|
|
2488
|
-
const parentNode = findInTree(this._structureTree, parentInfo.id);
|
|
2489
|
-
if (parentNode) {
|
|
2490
|
-
const children2 = parentNode.children || [];
|
|
2491
|
-
siblings = children2.map((child) => getNodeId(child)).filter((id) => id !== void 0 && id !== componentId);
|
|
2492
|
-
}
|
|
2493
|
-
}
|
|
2494
|
-
const siblingsStr = siblings.length > 0 ? `Siblings: ${siblings.join(", ")}` : "Siblings: None";
|
|
2495
|
-
const nodeChildren = node.children || [];
|
|
2496
|
-
const children = nodeChildren.map((child) => getNodeId(child)).filter((id) => id !== void 0);
|
|
2497
|
-
const childrenStr = children.length > 0 ? `Children: ${children.join(", ")}` : "Children: None";
|
|
2498
|
-
const filePath = this._componentPaths[componentId];
|
|
2499
|
-
const fileStr = filePath ? `File: ${filePath}` : "";
|
|
2500
|
-
const lines = [`Component: ${componentId}`, fileStr, parentStr, parentPathStr, siblingsStr, childrenStr].filter(Boolean);
|
|
2501
|
-
return lines.join("\n");
|
|
2502
|
-
}
|
|
2503
|
-
getSharedInstances(filePath) {
|
|
2504
|
-
const instances = [];
|
|
2505
|
-
for (const [compId, compPath] of Object.entries(this._componentPaths)) {
|
|
2506
|
-
if (compPath.includes(filePath) || compPath.endsWith(filePath)) {
|
|
2507
|
-
instances.push(compId);
|
|
2508
|
-
}
|
|
2509
|
-
}
|
|
2510
|
-
if (instances.length === 0) {
|
|
2511
|
-
return `No components found using file: ${filePath}`;
|
|
2512
|
-
}
|
|
2513
|
-
const instancesList = instances.map((inst) => ` - ${inst}`).join("\n");
|
|
2514
|
-
return `Components using ${filePath}:
|
|
2515
|
-
${instancesList}
|
|
2516
|
-
|
|
2517
|
-
Warning: Changes to this file will affect ALL ${instances.length} instance(s)`;
|
|
2518
|
-
}
|
|
2519
|
-
_findParent(node, targetId, parent = null) {
|
|
2520
|
-
const nodeId = getNodeId(node);
|
|
2521
|
-
if (nodeId === targetId) {
|
|
2522
|
-
if (parent) {
|
|
2523
|
-
const parentId = getNodeId(parent);
|
|
2524
|
-
if (parentId) {
|
|
2525
|
-
return { id: parentId };
|
|
2526
|
-
}
|
|
2527
|
-
}
|
|
2528
|
-
return void 0;
|
|
2529
|
-
}
|
|
2530
|
-
const children = node.children || [];
|
|
2531
|
-
for (const child of children) {
|
|
2532
|
-
const result = this._findParent(child, targetId, node);
|
|
2533
|
-
if (result !== void 0) {
|
|
2534
|
-
return result;
|
|
2535
|
-
}
|
|
2536
|
-
}
|
|
2537
|
-
return void 0;
|
|
2538
|
-
}
|
|
2539
|
-
};
|
|
2540
|
-
_init4 = __decoratorStart(null);
|
|
2541
|
-
HierarchyTool = __decorateElement(_init4, 0, "HierarchyTool", _HierarchyTool_decorators, HierarchyTool);
|
|
2542
|
-
__runInitializers(_init4, 1, HierarchyTool);
|
|
2543
|
-
|
|
2544
|
-
// src/agents/judger-agent/prompt.ts
|
|
2545
|
-
var JUDGER_PROMPT = `You are a React Layout Diagnosis Expert. Analyze position misalignments and provide precise fix instructions.
|
|
2546
|
-
|
|
2547
|
-
<workflow>
|
|
2548
|
-
1. Check HistoryTool.getComponentHistory - avoid repeating failed fixes
|
|
2549
|
-
2. Use HierarchyTool.lookup to get component file path, parent, siblings, and children
|
|
2550
|
-
3. Use FileEditor.read to examine code, FileEditor.find to locate patterns
|
|
2551
|
-
4. For parent fixes: HierarchyTool.lookup returns parent file path - use it directly
|
|
2552
|
-
5. Before editing shared files: Use HierarchyTool.getSharedInstances to check impact
|
|
2553
|
-
6. If image provided: previous iteration
|
|
2554
|
-
7. Respond with JSON diagnosis wrapped in \`\`\`json\`\`\`
|
|
2555
|
-
</workflow>
|
|
2556
|
-
|
|
2557
|
-
<error_types>
|
|
2558
|
-
- pixel_misalignment: Wrong spacing values (gap-2 vs gap-4, mt-[20px] vs mt-[50px])
|
|
2559
|
-
- positioning_strategy: Wrong layout method (absolute vs flex, wrong parent)
|
|
2560
|
-
- parent_spacing: Parent gap/padding causes child misalignment
|
|
2561
|
-
- sibling_cascade: Sibling margin shifts others (fix topmost only)
|
|
2562
|
-
- none: Position already correct
|
|
2563
|
-
</error_types>
|
|
2564
|
-
|
|
2565
|
-
<rules>
|
|
2566
|
-
CRITICAL CONSTRAINTS:
|
|
2567
|
-
1. Fix POSITION only (x,y) - validation does NOT measure dimensions
|
|
2568
|
-
2. FORBIDDEN: w-*, h-*, max-w-*, max-h-*, min-w-*, min-h-*, aspect-*, flex-row, flex-col
|
|
2569
|
-
3. ALLOWED: mt-*, mb-*, ml-*, mr-*, pt-*, pb-*, pl-*, pr-*, gap-*, space-*, top-*, left-*, right-*, bottom-*, translate-*
|
|
2570
|
-
|
|
2571
|
-
FLEX LAYOUT (prevents MAE oscillation):
|
|
2572
|
-
- Adding mt-[X] to component A shifts ALL siblings below by X pixels (additive effect)
|
|
2573
|
-
- For sibling components: Fix ONLY the topmost misaligned one per iteration (topmost = first in DOM order)
|
|
2574
|
-
- For other siblings: Set refineInstructions to [] with rootCause: "Fix deferred - waiting for topmost sibling fix to propagate"
|
|
2575
|
-
- Prefer parent gap/padding over child margins
|
|
2576
|
-
- If component oscillates "too high" \u2194 "too low" \u2192 fix parent or topmost sibling instead
|
|
2577
|
-
|
|
2578
|
-
SPECIAL CASES:
|
|
2579
|
-
- Already correct: errorType="none", refineInstructions=[]
|
|
2580
|
-
- Wrong dimensions/flex-direction: Note in rootCause but do NOT fix
|
|
2581
|
-
- Wrong X (x=0 vs x=320): Check parent centering, do NOT change w-full to w-[640px]
|
|
2582
|
-
</rules>
|
|
2583
|
-
|
|
2584
|
-
<output_format>
|
|
2585
|
-
Return JSON with camelCase fields: errorType, rootCause, visualEvidence, codeEvidence, refineInstructions, toolsUsed
|
|
2586
|
-
|
|
2587
|
-
refineInstructions format: "In [FULL_PATH] line [N], change '[OLD_CODE]' to '[NEW_CODE]'"
|
|
2588
|
-
- Use EXACT code strings from FileEditor.read (copy-paste)
|
|
2589
|
-
- OLD_CODE \u2260 NEW_CODE (never "keep unchanged")
|
|
2590
|
-
- Check HierarchyTool.getSharedInstances before editing shared files
|
|
2591
|
-
|
|
2592
|
-
Wrap output:
|
|
2593
|
-
<TaskCompletion>
|
|
2594
|
-
\`\`\`json
|
|
2595
|
-
{your json here}
|
|
2596
|
-
\`\`\`
|
|
2597
|
-
</TaskCompletion>
|
|
2598
|
-
</output_format>
|
|
2599
|
-
|
|
2600
|
-
<example>
|
|
2601
|
-
<TaskCompletion>
|
|
2602
|
-
\`\`\`json
|
|
2603
|
-
{
|
|
2604
|
-
"errorType": "pixel_misalignment",
|
|
2605
|
-
"rootCause": "Gap is 8px but should be 16px",
|
|
2606
|
-
"visualEvidence": "Elements too close in screenshot",
|
|
2607
|
-
"codeEvidence": "Line 23: className='flex gap-2'",
|
|
2608
|
-
"refineInstructions": ["In /path/Card.tsx line 23, change 'gap-2' to 'gap-4'"],
|
|
2609
|
-
"toolsUsed": ["FileEditor.read", "HistoryTool.getComponentHistory"]
|
|
2610
|
-
}
|
|
2611
|
-
\`\`\`
|
|
2612
|
-
</TaskCompletion>
|
|
2613
|
-
</example>
|
|
2614
|
-
`;
|
|
2615
|
-
|
|
2616
|
-
// src/agents/judger-agent/utils.ts
|
|
2617
|
-
init_logger();
|
|
2618
|
-
import { SystemToolStore, FunctionCallingStore } from "evoltagent";
|
|
2619
|
-
function parseJudgerResult(response) {
|
|
2620
|
-
if (!response || response.trim().length === 0) {
|
|
2621
|
-
logger.printInfoLog("Judger agent returned empty response, skipping refinement for this iteration");
|
|
2622
|
-
return Promise.resolve({
|
|
2623
|
-
errorType: "pixel_misalignment",
|
|
2624
|
-
rootCause: "Agent analysis unavailable, do not apply any edits",
|
|
2625
|
-
visualEvidence: "N/A",
|
|
2626
|
-
codeEvidence: "N/A",
|
|
2627
|
-
refineInstructions: [],
|
|
2628
|
-
toolsUsed: []
|
|
2629
|
-
});
|
|
2630
|
-
}
|
|
2631
|
-
const jsonMatch = response.match(/```json\n([\s\S]*?)\n```/);
|
|
2632
|
-
if (!jsonMatch) {
|
|
2633
|
-
logger.printInfoLog("Judger agent response missing JSON block, skipping refinement for this iteration");
|
|
2634
|
-
return Promise.resolve({
|
|
2635
|
-
errorType: "empty_response",
|
|
2636
|
-
rootCause: "Agent analysis unavailable, do not apply any edits",
|
|
2637
|
-
visualEvidence: "N/A",
|
|
2638
|
-
codeEvidence: "N/A",
|
|
2639
|
-
refineInstructions: [],
|
|
2640
|
-
toolsUsed: []
|
|
2641
|
-
});
|
|
2642
|
-
}
|
|
2643
|
-
const jsonStr = jsonMatch[1];
|
|
2644
|
-
if (!jsonStr || jsonStr.trim().length === 0) {
|
|
2645
|
-
logger.printInfoLog("Judger agent response has empty JSON, skipping refinement for this iteration");
|
|
2646
|
-
return Promise.resolve({
|
|
2647
|
-
errorType: "empty_response",
|
|
2648
|
-
rootCause: "Agent analysis unavailable, do not apply any edits",
|
|
2649
|
-
visualEvidence: "N/A",
|
|
2650
|
-
codeEvidence: "N/A",
|
|
2651
|
-
refineInstructions: [],
|
|
2652
|
-
toolsUsed: []
|
|
2653
|
-
});
|
|
2654
|
-
}
|
|
2655
|
-
try {
|
|
2656
|
-
return Promise.resolve(JSON.parse(jsonStr));
|
|
2657
|
-
} catch (error) {
|
|
2658
|
-
logger.printInfoLog(`Judger agent response JSON parse failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
2659
|
-
return Promise.resolve({
|
|
2660
|
-
errorType: "empty_response",
|
|
2661
|
-
rootCause: "Agent analysis unavailable",
|
|
2662
|
-
visualEvidence: "N/A",
|
|
2663
|
-
codeEvidence: "N/A",
|
|
2664
|
-
refineInstructions: [],
|
|
2665
|
-
toolsUsed: []
|
|
2666
|
-
});
|
|
2667
|
-
}
|
|
2668
|
-
}
|
|
2669
|
-
function updateToolContext(toolInstance, toolName, methodNames) {
|
|
2670
|
-
const updatedNames = [];
|
|
2671
|
-
for (const methodName of methodNames) {
|
|
2672
|
-
const raw = toolInstance[methodName];
|
|
2673
|
-
if (!raw) {
|
|
2674
|
-
continue;
|
|
2675
|
-
}
|
|
2676
|
-
const method = raw.bind(toolInstance);
|
|
2677
|
-
const fullName = `${toolName}.${methodName}`;
|
|
2678
|
-
const systemTool = SystemToolStore.getTool(fullName);
|
|
2679
|
-
if (systemTool) {
|
|
2680
|
-
systemTool.execute = method;
|
|
2681
|
-
updatedNames.push(fullName);
|
|
2682
|
-
}
|
|
2683
|
-
const functionCallingName = `${toolName}-${methodName}`;
|
|
2684
|
-
const userTool = FunctionCallingStore.getTool(functionCallingName);
|
|
2685
|
-
if (userTool) {
|
|
2686
|
-
userTool.execute = method;
|
|
2687
|
-
}
|
|
2688
|
-
}
|
|
2689
|
-
return updatedNames;
|
|
2690
|
-
}
|
|
2691
|
-
|
|
2692
|
-
// src/agents/judger-agent/index.ts
|
|
2693
|
-
function createJudgerAgent(options) {
|
|
2694
|
-
const { workspaceDir, structureTree, componentPaths, history } = options;
|
|
2695
|
-
let systemPrompt = JUDGER_PROMPT;
|
|
2696
|
-
if (workspaceDir) {
|
|
2697
|
-
systemPrompt += `
|
|
2698
|
-
|
|
2699
|
-
WORKSPACE: ${workspaceDir}
|
|
2700
|
-
Only access files within this workspace.`;
|
|
2701
|
-
}
|
|
2702
|
-
const systemTools = ["FileEditor.read", "FileEditor.find"];
|
|
2703
|
-
if (structureTree) {
|
|
2704
|
-
const hierarchyTool = new HierarchyTool();
|
|
2705
|
-
hierarchyTool.setContext(structureTree, componentPaths || {});
|
|
2706
|
-
const hierarchyToolNames = updateToolContext(
|
|
2707
|
-
hierarchyTool,
|
|
2708
|
-
"HierarchyTool",
|
|
2709
|
-
["lookup", "getSharedInstances"]
|
|
2710
|
-
);
|
|
2711
|
-
systemTools.push(...hierarchyToolNames);
|
|
2712
|
-
}
|
|
2713
|
-
const historyTool = new HistoryTool();
|
|
2714
|
-
historyTool.setContext(history || {});
|
|
2715
|
-
const historyToolNames = updateToolContext(
|
|
2716
|
-
historyTool,
|
|
2717
|
-
"HistoryTool",
|
|
2718
|
-
["getComponentHistory", "getIterationSummary"]
|
|
2719
|
-
);
|
|
2720
|
-
systemTools.push(...historyToolNames);
|
|
2721
|
-
const modelConfig = {
|
|
2722
|
-
...getModelConfig(),
|
|
2723
|
-
contextWindowTokens: AGENT_CONTEXT_WINDOW_TOKENS,
|
|
2724
|
-
maxOutputTokens: MAX_OUTPUT_TOKENS,
|
|
2725
|
-
stream: true
|
|
2726
|
-
};
|
|
2727
|
-
return new Agent3({
|
|
2728
|
-
name: "JudgerAgent",
|
|
2729
|
-
profile: "Layout diagnosis specialist",
|
|
2730
|
-
system: systemPrompt,
|
|
2731
|
-
tools: systemTools,
|
|
2732
|
-
modelConfig,
|
|
2733
|
-
postProcessor: parseJudgerResult,
|
|
2734
|
-
verbose: 2
|
|
2735
|
-
});
|
|
2736
|
-
}
|
|
2737
|
-
|
|
2738
|
-
// src/agents/judger-agent/instruction.ts
|
|
2739
|
-
function formatJudgerInstruction(component, figmaMetadata, componentPaths) {
|
|
2740
|
-
const vr = component.validationReport;
|
|
2741
|
-
const validationReportFormatted = `Current Position: (${vr.currentPosition[0].toFixed(1)}, ${vr.currentPosition[1].toFixed(1)}) px
|
|
2742
|
-
Target Position: (${vr.targetPosition[0].toFixed(1)}, ${vr.targetPosition[1].toFixed(1)}) px
|
|
2743
|
-
Absolute Error: (${vr.absoluteError[0].toFixed(1)}, ${vr.absoluteError[1].toFixed(1)}) px`;
|
|
2744
|
-
const padding = figmaMetadata.padding;
|
|
2745
|
-
const layoutMode = typeof figmaMetadata.layoutMode === "string" ? figmaMetadata.layoutMode : "NONE";
|
|
2746
|
-
const itemSpacing = typeof figmaMetadata.itemSpacing === "number" ? figmaMetadata.itemSpacing : 0;
|
|
2747
|
-
const primaryAxisAlignItems = typeof figmaMetadata.primaryAxisAlignItems === "string" ? figmaMetadata.primaryAxisAlignItems : "N/A";
|
|
2748
|
-
const counterAxisAlignItems = typeof figmaMetadata.counterAxisAlignItems === "string" ? figmaMetadata.counterAxisAlignItems : "N/A";
|
|
2749
|
-
const resolvedPath = componentPaths?.[component.componentId];
|
|
2750
|
-
if (!resolvedPath) {
|
|
2751
|
-
throw new Error(`Component ${component.componentId} not found in componentPaths mapping`);
|
|
2752
|
-
}
|
|
2753
|
-
return `Component ID: ${component.componentId}
|
|
2754
|
-
Element IDs: ${JSON.stringify(component.elementIds)}
|
|
2755
|
-
File: ${resolvedPath}
|
|
2756
|
-
|
|
2757
|
-
Validation Report:
|
|
2758
|
-
${validationReportFormatted}
|
|
2759
|
-
|
|
2760
|
-
Figma Layout:
|
|
2761
|
-
- Mode: ${layoutMode}
|
|
2762
|
-
- Item Spacing: ${itemSpacing}px
|
|
2763
|
-
- Padding: ${padding?.left ?? 0}px (left) ${padding?.top ?? 0}px (top) ${padding?.right ?? 0}px (right) ${padding?.bottom ?? 0}px (bottom)
|
|
2764
|
-
- Primary Axis Alignment: ${primaryAxisAlignItems}
|
|
2765
|
-
- Counter Axis Alignment: ${counterAxisAlignItems}
|
|
2766
|
-
|
|
2767
|
-
TASK:
|
|
2768
|
-
1. Read component file at: ${resolvedPath}
|
|
2769
|
-
2. Identify the layout error by comparing code with Figma metadata
|
|
2770
|
-
3. Locate exact code patterns with FileEditor.find
|
|
2771
|
-
4. Output JSON diagnosis with precise refine_instructions
|
|
2772
|
-
|
|
2773
|
-
IMPORTANT:
|
|
2774
|
-
- Use FileEditor.read("${resolvedPath}") to read the component code
|
|
2775
|
-
- The path is an absolute filesystem path (e.g., /workspace/src/components/button/index.tsx)
|
|
2776
|
-
- When using HierarchyTool.lookup or related tools, use the Component ID: ${component.componentId}
|
|
2777
|
-
|
|
2778
|
-
Remember: Use exact code strings and line numbers in refine_instructions.
|
|
2779
|
-
`;
|
|
2780
|
-
}
|
|
2781
|
-
|
|
2782
|
-
// src/agents/refiner-agent/index.ts
|
|
2783
|
-
import { Agent as Agent4 } from "evoltagent";
|
|
2784
|
-
|
|
2785
|
-
// src/agents/refiner-agent/prompt.ts
|
|
2786
|
-
var REFINER_PROMPT = `You are a React Code Editor. Apply fixes from judger's diagnosis.
|
|
2787
|
-
|
|
2788
|
-
<workflow>
|
|
2789
|
-
1. For each refineInstruction, parse format: "In [path] line [N], change '[old]' to '[new]'"
|
|
2790
|
-
2. VALIDATE before executing:
|
|
2791
|
-
- Skip if instruction says "keep unchanged", "no change needed", or lacks specific code
|
|
2792
|
-
- Skip if OLD and NEW are identical
|
|
2793
|
-
- Skip if OLD or NEW is empty
|
|
2794
|
-
- Skip if instruction is vague like "apply fix in parent" without specific code
|
|
2795
|
-
3. Read file with FileEditor.read to verify the OLD pattern exists
|
|
2796
|
-
4. Use FileEditor.findAndReplace(path, pattern=old, replacement=new)
|
|
2797
|
-
- IMPORTANT: Escape special regex chars in pattern: [ ] ( ) . * + ? $ ^ | \\
|
|
2798
|
-
- Example: "mt-[20px]" \u2192 pattern="mt-\\[20px\\]"
|
|
2799
|
-
5. Report results
|
|
2800
|
-
</workflow>
|
|
2801
|
-
|
|
2802
|
-
<validation_rules>
|
|
2803
|
-
SKIP instructions that:
|
|
2804
|
-
- Contain "keep unchanged" or "no change needed"
|
|
2805
|
-
- Don't have both '[old]' and '[new]' quoted strings
|
|
2806
|
-
- Have identical old and new values
|
|
2807
|
-
- Are vague suggestions without specific code
|
|
2808
|
-
|
|
2809
|
-
ALWAYS escape regex special characters in the pattern parameter:
|
|
2810
|
-
- Brackets: [ \u2192 \\[, ] \u2192 \\]
|
|
2811
|
-
- Parentheses: ( \u2192 \\(, ) \u2192 \\)
|
|
2812
|
-
- Other: . * + ? $ ^ | \u2192 prefix with \\
|
|
2813
|
-
</validation_rules>
|
|
2814
|
-
|
|
2815
|
-
<output>
|
|
2816
|
-
After edits, respond with a summary:
|
|
2817
|
-
- "Successfully applied: [description]" (per success)
|
|
2818
|
-
- "Failed to apply: [description]" (per failure)
|
|
2819
|
-
- "Skipped: [description]" (for invalid instructions)
|
|
2820
|
-
|
|
2821
|
-
Wrap summary in <TaskCompletion> tags:
|
|
2822
|
-
<TaskCompletion>
|
|
2823
|
-
[Your summary here]
|
|
2824
|
-
</TaskCompletion>
|
|
2825
|
-
</output>
|
|
2826
|
-
`;
|
|
2827
|
-
|
|
2828
|
-
// src/agents/refiner-agent/utils.ts
|
|
2829
|
-
function parseRefinerResult(response) {
|
|
2830
|
-
const fullResponse = response.trim();
|
|
2831
|
-
const lines = fullResponse.split("\n");
|
|
2832
|
-
const summaryLines = [];
|
|
2833
|
-
for (const line of lines) {
|
|
2834
|
-
const trimmedLine = line.trim();
|
|
2835
|
-
if (trimmedLine.startsWith("Successfully applied:") || trimmedLine.startsWith("Failed to apply:") || trimmedLine.startsWith("Skipped:")) {
|
|
2836
|
-
summaryLines.push(trimmedLine);
|
|
2837
|
-
}
|
|
2838
|
-
}
|
|
2839
|
-
const summary = summaryLines.length > 0 ? summaryLines : [fullResponse];
|
|
2840
|
-
const summaryText = summary.join(" ").toLowerCase();
|
|
2841
|
-
const successCount = (summaryText.match(/successfully applied:/g) || []).length;
|
|
2842
|
-
const failedCount = (summaryText.match(/failed to apply:/g) || []).length;
|
|
2843
|
-
return Promise.resolve({
|
|
2844
|
-
success: successCount > 0 && failedCount === 0,
|
|
2845
|
-
summary,
|
|
2846
|
-
editsApplied: successCount,
|
|
2847
|
-
error: failedCount > 0 ? `${failedCount} edit(s) failed` : void 0
|
|
2848
|
-
});
|
|
2849
|
-
}
|
|
2850
|
-
|
|
2851
|
-
// src/agents/refiner-agent/instruction.ts
|
|
2852
|
-
function formatRefinerInstruction(component, diagnosis, componentPaths) {
|
|
2853
|
-
const resolvedPath = componentPaths?.[component.componentId];
|
|
2854
|
-
if (!resolvedPath) {
|
|
2855
|
-
throw new Error(`Component ${component.componentId} not found in componentPaths mapping`);
|
|
2856
|
-
}
|
|
2857
|
-
const refineInstructionsList = (diagnosis.refineInstructions || []).map((instr, i) => `${i + 1}. ${instr}`).join("\n");
|
|
2858
|
-
return `Component ID: ${component.componentId}
|
|
2859
|
-
Element IDs: ${JSON.stringify(component.elementIds)}
|
|
2860
|
-
File: ${resolvedPath}
|
|
2861
|
-
|
|
2862
|
-
Diagnosis from judger:
|
|
2863
|
-
${JSON.stringify(diagnosis, null, 2)}
|
|
2864
|
-
|
|
2865
|
-
TASK:
|
|
2866
|
-
Apply each refineInstruction:
|
|
2867
|
-
${refineInstructionsList}
|
|
2868
|
-
|
|
2869
|
-
For each instruction:
|
|
2870
|
-
1. Parse the old_code and new_code
|
|
2871
|
-
2. Use FileEditor.findAndReplace to apply the change
|
|
2872
|
-
3. Report success or failure
|
|
2873
|
-
|
|
2874
|
-
IMPORTANT:
|
|
2875
|
-
- The file path is an absolute filesystem path (e.g., /workspace/src/components/button/index.tsx)
|
|
2876
|
-
- Use this exact path with FileEditor tools
|
|
2877
|
-
`;
|
|
2878
|
-
}
|
|
2879
|
-
|
|
2880
|
-
// src/agents/refiner-agent/index.ts
|
|
2881
|
-
function createRefinerAgent(workspaceDir) {
|
|
2882
|
-
let systemPrompt = REFINER_PROMPT;
|
|
2883
|
-
if (workspaceDir) {
|
|
2884
|
-
systemPrompt += `
|
|
2885
|
-
|
|
2886
|
-
WORKSPACE: ${workspaceDir}
|
|
2887
|
-
Only modify files within this workspace.`;
|
|
2888
|
-
}
|
|
2889
|
-
const modelConfig = {
|
|
2890
|
-
...getModelConfig(),
|
|
2891
|
-
contextWindowTokens: AGENT_CONTEXT_WINDOW_TOKENS,
|
|
2892
|
-
maxOutputTokens: MAX_OUTPUT_TOKENS,
|
|
2893
|
-
stream: true
|
|
2894
|
-
};
|
|
2895
|
-
return new Agent4({
|
|
2896
|
-
name: "RefinerAgent",
|
|
2897
|
-
profile: "Code editor specialist",
|
|
2898
|
-
system: systemPrompt,
|
|
2899
|
-
tools: ["FileEditor.read", "FileEditor.find", "FileEditor.findAndReplace"],
|
|
2900
|
-
modelConfig,
|
|
2901
|
-
postProcessor: parseRefinerResult,
|
|
2902
|
-
verbose: 2
|
|
2903
|
-
});
|
|
2904
|
-
}
|
|
2905
|
-
|
|
2906
|
-
// src/agents/launch-agent/index.ts
|
|
2907
|
-
import { Agent as Agent5 } from "evoltagent";
|
|
2908
|
-
|
|
2909
|
-
// src/agents/launch-agent/prompt.ts
|
|
2910
|
-
var LAUNCH_AGENT_PROMPT = `
|
|
2911
|
-
<task_goal>
|
|
2912
|
-
Prepare project for validation: install dependencies (if needed), build, fix errors, start dev server.
|
|
2913
|
-
Return server metadata for validation loop to use.
|
|
2914
|
-
</task_goal>
|
|
2915
|
-
|
|
2916
|
-
<workflow>
|
|
2917
|
-
Follow steps IN ORDER. Repeat Step 2-3 until build succeeds, then proceed to Step 4.
|
|
2918
|
-
|
|
2919
|
-
Step 1: Install Dependencies (Conditional)
|
|
2920
|
-
- Check instruction for skipInstall directive
|
|
2921
|
-
- If "Skip Step 1" appears in instruction: Skip to Step 2
|
|
2922
|
-
- Otherwise: Execute "pnpm i" in appPath
|
|
2923
|
-
- Parameters: CommandLineTool.execute(command="pnpm i", cwd=appPath, timeoutMs=180000)
|
|
2924
|
-
- Verify exitCode === 0 before proceeding
|
|
2925
|
-
- If installation fails, analyze the error output and fix issues before proceeding
|
|
2926
|
-
|
|
2927
|
-
Step 2: Build Project
|
|
2928
|
-
- Execute: CommandLineTool.execute(command="npm run build", cwd=appPath, timeoutMs=180000)
|
|
2929
|
-
- Captures all compilation errors in output
|
|
2930
|
-
|
|
2931
|
-
Step 3: Fix Compilation Errors (If exitCode !== 0)
|
|
2932
|
-
- Analyze error messages in 'error' and 'output' fields
|
|
2933
|
-
- Use FileEditor.read to examine problematic files
|
|
2934
|
-
- Use FileEditor.write to fix ONLY the specific broken lines
|
|
2935
|
-
- CRITICAL: Do NOT delete or rewrite entire files. Do NOT simplify complex CSS or logic.
|
|
2936
|
-
- Return to Step 2 and rebuild
|
|
2937
|
-
|
|
2938
|
-
Step 4: Start Dev Server & Check Runtime Errors
|
|
2939
|
-
- Only proceed here when build is successful
|
|
2940
|
-
- Execute: LaunchTool.startDevServer(appPath=appPath, runCommand="npm run dev", timeoutMs=60000)
|
|
2941
|
-
- Returns JSON: { success, url, port, serverKey, outputTail }
|
|
2942
|
-
- Parse and check outputTail for runtime error patterns EVEN IF success=true:
|
|
2943
|
-
* "Module not found", "Cannot find module"
|
|
2944
|
-
* "SyntaxError", "TypeError", "ReferenceError"
|
|
2945
|
-
* "Failed to compile", "Unhandled Runtime Error"
|
|
2946
|
-
- If errors found: Fix using FileEditor, then restart Step 2
|
|
2947
|
-
* CRITICAL: Do NOT delete or rewrite entire files. Do NOT simplify complex CSS or logic.
|
|
2948
|
-
- If clean: Store serverKey, url, port and proceed to Step 5
|
|
2949
|
-
|
|
2950
|
-
Step 5: Return Server Metadata
|
|
2951
|
-
- Format final response as:
|
|
2952
|
-
<TaskCompletion>
|
|
2953
|
-
\`\`\`json
|
|
2954
|
-
{
|
|
2955
|
-
"success": true,
|
|
2956
|
-
"serverKey": "launch:...",
|
|
2957
|
-
"url": "http://localhost:PORT",
|
|
2958
|
-
"port": PORT,
|
|
2959
|
-
"message": "Dev server started successfully"
|
|
2960
|
-
}
|
|
2961
|
-
\`\`\`
|
|
2962
|
-
</TaskCompletion>
|
|
2963
|
-
- On failure before server start:
|
|
2964
|
-
<TaskCompletion>
|
|
2965
|
-
\`\`\`json
|
|
2966
|
-
{ "success": false, "error": "Description" }
|
|
2967
|
-
\`\`\`
|
|
2968
|
-
</TaskCompletion>
|
|
2969
|
-
</workflow>
|
|
2970
|
-
|
|
2971
|
-
<principles>
|
|
2972
|
-
1. STRICT CONTENT PRESERVATION (MANDATORY):
|
|
2973
|
-
- NEVER modify CSS/Style content (colors, layouts, animations, etc.). If a CSS error exists, only fix the import/path or a specific syntax typo.
|
|
2974
|
-
- NEVER modify or delete Image assets or their references.
|
|
2975
|
-
- NEVER modify the DOM structure or JSX layout (adding/removing tags, changing classNames).
|
|
2976
|
-
- NEVER replace file content with "template" or "placeholder" code.
|
|
2977
|
-
2. ALLOWED CHANGES ONLY:
|
|
2978
|
-
- Fixing 'module not found' by installing missing packages or correcting import paths.
|
|
2979
|
-
- Fixing TypeScript/JavaScript syntax errors that prevent execution.
|
|
2980
|
-
- Fixing configuration issues (e.g., tailwind.config.js, tsconfig.json).
|
|
2981
|
-
3. BUILD FIRST: Dev server must not start until build succeeds (exitCode === 0).
|
|
2982
|
-
4. PORT CONSISTENCY: Always use LaunchTool.startDevServer() (workspace-preferred port, same port across runs).
|
|
2983
|
-
5. RUNTIME VALIDATION: Check outputTail in Step 4 for runtime errors even if success=true.
|
|
2984
|
-
</principles>
|
|
2985
|
-
|
|
2986
|
-
<available_tools>
|
|
2987
|
-
- FileEditor.read: Read file contents for analysis
|
|
2988
|
-
- FileEditor.write: Write file contents to fix errors
|
|
2989
|
-
- CommandLineTool.execute: Execute commands (pnpm i, npm run build only)
|
|
2990
|
-
- LaunchTool.startDevServer: Start dev server (returns JSON with serverKey/url/port/outputTail)
|
|
2991
|
-
- ThinkTool.execute: Think through complex problems
|
|
2992
|
-
</available_tools>
|
|
2993
|
-
`;
|
|
2994
|
-
|
|
2995
|
-
// src/agents/launch-agent/utils.ts
|
|
2996
|
-
init_logger();
|
|
2997
|
-
function parseLaunchResult(response) {
|
|
2998
|
-
if (!response || response.trim().length === 0) {
|
|
2999
|
-
logger.printInfoLog("Launch agent returned empty response");
|
|
3000
|
-
return Promise.resolve({
|
|
3001
|
-
success: false,
|
|
3002
|
-
error: "Agent returned empty response"
|
|
3003
|
-
});
|
|
3004
|
-
}
|
|
3005
|
-
const jsonMatch = response.match(/```json\n([\s\S]*?)\n```/);
|
|
3006
|
-
if (!jsonMatch) {
|
|
3007
|
-
logger.printInfoLog("Launch agent response missing JSON block");
|
|
3008
|
-
return Promise.resolve({
|
|
3009
|
-
success: false,
|
|
3010
|
-
error: "No JSON code block found in agent response"
|
|
3011
|
-
});
|
|
3012
|
-
}
|
|
3013
|
-
const jsonStr = jsonMatch[1];
|
|
3014
|
-
if (!jsonStr || jsonStr.trim().length === 0) {
|
|
3015
|
-
logger.printInfoLog("Launch agent response has empty JSON");
|
|
3016
|
-
return Promise.resolve({
|
|
3017
|
-
success: false,
|
|
3018
|
-
error: "Empty JSON content in code block"
|
|
3019
|
-
});
|
|
3020
|
-
}
|
|
3021
|
-
try {
|
|
3022
|
-
const parsed = JSON.parse(jsonStr);
|
|
3023
|
-
if (parsed.success === true && (!parsed.serverKey || !parsed.url || !parsed.port)) {
|
|
3024
|
-
logger.printInfoLog("Launch agent success response missing required fields");
|
|
3025
|
-
return Promise.resolve({
|
|
3026
|
-
success: false,
|
|
3027
|
-
error: "Missing required fields (serverKey, url, or port)"
|
|
3028
|
-
});
|
|
3029
|
-
}
|
|
3030
|
-
return Promise.resolve(parsed);
|
|
3031
|
-
} catch (error) {
|
|
3032
|
-
logger.printInfoLog(`Launch agent response JSON parse failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
3033
|
-
return Promise.resolve({
|
|
3034
|
-
success: false,
|
|
3035
|
-
error: error instanceof Error ? error.message : String(error)
|
|
3036
|
-
});
|
|
3037
|
-
}
|
|
3038
|
-
}
|
|
3039
|
-
|
|
3040
|
-
// src/agents/launch-agent/instruction.ts
|
|
3041
|
-
function launchAgentInstruction(params) {
|
|
3042
|
-
const skipNote = params.skipDependencyInstall ? "\n\nNOTE: Skip Step 1 (Install Dependencies). Dependencies already installed." : "";
|
|
3043
|
-
return `appPath: ${params.appPath}${skipNote}
|
|
3044
|
-
|
|
3045
|
-
TASK:
|
|
3046
|
-
Install dependencies, compile the project, fix any compilation errors, start the development server, and fix any runtime errors. The goal is to have a fully working, error-free project.`;
|
|
3047
|
-
}
|
|
3048
|
-
|
|
3049
|
-
// src/agents/launch-agent/index.ts
|
|
3050
|
-
function createLaunchAgent() {
|
|
3051
|
-
const modelConfig = {
|
|
3052
|
-
...getModelConfig(),
|
|
3053
|
-
contextWindowTokens: AGENT_CONTEXT_WINDOW_TOKENS,
|
|
3054
|
-
maxOutputTokens: MAX_OUTPUT_TOKENS
|
|
3055
|
-
};
|
|
3056
|
-
return new Agent5({
|
|
3057
|
-
name: "LaunchAgent",
|
|
3058
|
-
profile: "Expert DevOps and Frontend Engineer specialist in launching projects and troubleshooting build/runtime issues.",
|
|
3059
|
-
system: LAUNCH_AGENT_PROMPT,
|
|
3060
|
-
tools: [
|
|
3061
|
-
"CommandLineTool.execute",
|
|
3062
|
-
"CommandLineTool.list",
|
|
3063
|
-
"CommandLineTool.stop",
|
|
3064
|
-
"LaunchTool.startDevServer",
|
|
3065
|
-
"LaunchTool.stopDevServer",
|
|
3066
|
-
"FileEditor.read",
|
|
3067
|
-
"FileEditor.find",
|
|
3068
|
-
"FileEditor.findAndReplace",
|
|
3069
|
-
"FileEditor.write"
|
|
3070
|
-
],
|
|
3071
|
-
modelConfig,
|
|
3072
|
-
verbose: 2,
|
|
3073
|
-
postProcessor: parseLaunchResult
|
|
3074
|
-
});
|
|
3075
|
-
}
|
|
3076
|
-
|
|
3077
|
-
// src/nodes/validation/launch/index.ts
|
|
3078
|
-
var launch = async (appPath, config) => {
|
|
3079
|
-
if (!appPath) {
|
|
3080
|
-
throw new Error("appPath is required");
|
|
3081
|
-
}
|
|
3082
|
-
try {
|
|
3083
|
-
const agentResult = await createLaunchAgent().run(
|
|
3084
|
-
launchAgentInstruction({
|
|
3085
|
-
appPath,
|
|
3086
|
-
skipDependencyInstall: config?.skipDependencyInstall
|
|
3087
|
-
})
|
|
3088
|
-
);
|
|
3089
|
-
if (agentResult.success === false) {
|
|
3090
|
-
return {
|
|
3091
|
-
success: false,
|
|
3092
|
-
error: agentResult.error ?? "Launch agent failed without error message"
|
|
3093
|
-
};
|
|
3094
|
-
}
|
|
3095
|
-
return {
|
|
3096
|
-
success: true,
|
|
3097
|
-
message: agentResult.message ?? "Launch and quality assurance completed",
|
|
3098
|
-
serverKey: agentResult.serverKey,
|
|
3099
|
-
url: agentResult.url,
|
|
3100
|
-
port: agentResult.port
|
|
3101
|
-
};
|
|
3102
|
-
} catch (error) {
|
|
3103
|
-
return {
|
|
3104
|
-
success: false,
|
|
3105
|
-
error: error instanceof Error ? error.message : String(error)
|
|
3106
|
-
};
|
|
3107
|
-
}
|
|
3108
|
-
};
|
|
3109
|
-
|
|
3110
|
-
// src/nodes/validation/utils/extraction/extract-layout-metadata.ts
|
|
3111
|
-
var LAYOUT_SCORES = {
|
|
3112
|
-
FRAME_WITH_LAYOUT: 4,
|
|
3113
|
-
NODE_WITH_LAYOUT: 3,
|
|
3114
|
-
GROUP: 2,
|
|
3115
|
-
DEFAULT: 1
|
|
3116
|
-
};
|
|
3117
|
-
function hasLayoutMode(element) {
|
|
3118
|
-
return element.layoutMode !== "NONE" && element.layoutMode !== void 0;
|
|
3119
|
-
}
|
|
3120
|
-
function getLayoutScore(element) {
|
|
3121
|
-
if (element.type === "FRAME" && hasLayoutMode(element)) {
|
|
3122
|
-
return LAYOUT_SCORES.FRAME_WITH_LAYOUT;
|
|
3123
|
-
}
|
|
3124
|
-
if (hasLayoutMode(element)) {
|
|
3125
|
-
return LAYOUT_SCORES.NODE_WITH_LAYOUT;
|
|
3126
|
-
}
|
|
3127
|
-
if (element.type === "GROUP") {
|
|
3128
|
-
return LAYOUT_SCORES.GROUP;
|
|
3129
|
-
}
|
|
3130
|
-
return LAYOUT_SCORES.DEFAULT;
|
|
3131
|
-
}
|
|
3132
|
-
function findBestLayoutElement(elementIds, context) {
|
|
3133
|
-
let bestElement;
|
|
3134
|
-
let bestScore = -1;
|
|
3135
|
-
for (const elemId of elementIds) {
|
|
3136
|
-
const element = context.elements.get(elemId);
|
|
3137
|
-
if (!element) continue;
|
|
3138
|
-
const score = getLayoutScore(element);
|
|
3139
|
-
if (score > bestScore) {
|
|
3140
|
-
bestScore = score;
|
|
3141
|
-
bestElement = element;
|
|
3142
|
-
if (score === LAYOUT_SCORES.FRAME_WITH_LAYOUT) break;
|
|
3143
|
-
}
|
|
3144
|
-
}
|
|
3145
|
-
return bestElement;
|
|
3146
|
-
}
|
|
3147
|
-
function extractLayoutFromContext(context, elementIds) {
|
|
3148
|
-
const element = elementIds.length > 0 ? findBestLayoutElement(elementIds, context) : void 0;
|
|
3149
|
-
if (!element) {
|
|
3150
|
-
return {
|
|
3151
|
-
layoutMode: "NONE",
|
|
3152
|
-
primaryAxisAlignItems: "N/A",
|
|
3153
|
-
counterAxisAlignItems: "N/A",
|
|
3154
|
-
itemSpacing: 0,
|
|
3155
|
-
padding: { top: 0, right: 0, bottom: 0, left: 0 },
|
|
3156
|
-
constraints: {},
|
|
3157
|
-
absoluteBoundingBox: {}
|
|
3158
|
-
};
|
|
3159
|
-
}
|
|
3160
|
-
return {
|
|
3161
|
-
layoutMode: element.layoutMode ?? "NONE",
|
|
3162
|
-
primaryAxisAlignItems: element.primaryAxisAlignItems ?? "N/A",
|
|
3163
|
-
counterAxisAlignItems: element.counterAxisAlignItems ?? "N/A",
|
|
3164
|
-
itemSpacing: element.itemSpacing ?? 0,
|
|
3165
|
-
padding: {
|
|
3166
|
-
top: element.paddingTop ?? 0,
|
|
3167
|
-
right: element.paddingRight ?? 0,
|
|
3168
|
-
bottom: element.paddingBottom ?? 0,
|
|
3169
|
-
left: element.paddingLeft ?? 0
|
|
3170
|
-
},
|
|
3171
|
-
constraints: element.constraints ?? {},
|
|
3172
|
-
absoluteBoundingBox: {
|
|
3173
|
-
x: element.position.x,
|
|
3174
|
-
y: element.position.y,
|
|
3175
|
-
width: element.position.w,
|
|
3176
|
-
height: element.position.h
|
|
3177
|
-
}
|
|
3178
|
-
};
|
|
3179
|
-
}
|
|
3180
|
-
|
|
3181
|
-
// src/nodes/validation/report/index.ts
|
|
3182
|
-
init_logger();
|
|
3183
|
-
|
|
3184
|
-
// src/tools/report-tool/index.ts
|
|
3185
|
-
init_logger();
|
|
3186
|
-
import * as fs6 from "fs";
|
|
3187
|
-
import * as fsPromises from "fs/promises";
|
|
3188
|
-
import * as path8 from "path";
|
|
3189
|
-
import { fileURLToPath } from "url";
|
|
3190
|
-
import { tools as tools6 } from "evoltagent";
|
|
3191
|
-
|
|
3192
|
-
// src/tools/visualization-tool/index.ts
|
|
3193
|
-
import { tools as tools5 } from "evoltagent";
|
|
3194
|
-
import * as path7 from "path";
|
|
3195
|
-
|
|
3196
|
-
// src/tools/visualization-tool/utils/annotation-styles.ts
|
|
3197
|
-
var ANNOTATION_COLORS = {
|
|
3198
|
-
RED: "#ef4444",
|
|
3199
|
-
// Tailwind red-500 - for current/render positions
|
|
3200
|
-
GREEN: "#22c55e"
|
|
3201
|
-
// Tailwind green-500 - for target/expected positions
|
|
3202
|
-
};
|
|
3203
|
-
function createContainerStyle() {
|
|
3204
|
-
return `
|
|
3205
|
-
position: absolute;
|
|
3206
|
-
top: 0;
|
|
3207
|
-
left: 0;
|
|
3208
|
-
width: 100%;
|
|
3209
|
-
height: 100%;
|
|
3210
|
-
pointer-events: none;
|
|
3211
|
-
z-index: 999999;
|
|
3212
|
-
`;
|
|
3213
|
-
}
|
|
3214
|
-
function createBoxStyle(x, y, width, height, color) {
|
|
3215
|
-
return `
|
|
3216
|
-
position: absolute;
|
|
3217
|
-
left: ${x}px;
|
|
3218
|
-
top: ${y}px;
|
|
3219
|
-
width: ${width}px;
|
|
3220
|
-
height: ${height}px;
|
|
3221
|
-
border: 4px solid ${color};
|
|
3222
|
-
box-sizing: border-box;
|
|
3223
|
-
pointer-events: none;
|
|
3224
|
-
`;
|
|
3225
|
-
}
|
|
3226
|
-
function createCircleLabelStyle(x, y, color) {
|
|
3227
|
-
const labelX = Math.max(5, x - 22);
|
|
3228
|
-
const labelY = Math.max(5, y - 22);
|
|
3229
|
-
return `
|
|
3230
|
-
position: absolute;
|
|
3231
|
-
left: ${labelX}px;
|
|
3232
|
-
top: ${labelY}px;
|
|
3233
|
-
background: ${color};
|
|
3234
|
-
color: white;
|
|
3235
|
-
width: 32px;
|
|
3236
|
-
height: 32px;
|
|
3237
|
-
display: flex;
|
|
3238
|
-
align-items: center;
|
|
3239
|
-
justify-content: center;
|
|
3240
|
-
font: bold 18px Arial, sans-serif;
|
|
3241
|
-
border-radius: 50%;
|
|
3242
|
-
pointer-events: none;
|
|
3243
|
-
border: 2px solid white;
|
|
3244
|
-
box-shadow: 0 2px 4px rgba(0,0,0,0.3);
|
|
3245
|
-
`;
|
|
3246
|
-
}
|
|
3247
|
-
function createTextLabelStyle(x, y, height, color) {
|
|
3248
|
-
return `
|
|
3249
|
-
position: absolute;
|
|
3250
|
-
left: ${x}px;
|
|
3251
|
-
top: ${y + height + 4}px;
|
|
3252
|
-
background: ${color};
|
|
3253
|
-
color: white;
|
|
3254
|
-
padding: 4px 8px;
|
|
3255
|
-
font: 12px Arial, sans-serif;
|
|
3256
|
-
border-radius: 4px;
|
|
3257
|
-
pointer-events: none;
|
|
3258
|
-
white-space: nowrap;
|
|
3259
|
-
box-shadow: 0 2px 4px rgba(0,0,0,0.3);
|
|
3260
|
-
`;
|
|
3261
|
-
}
|
|
3262
|
-
|
|
3263
|
-
// src/tools/visualization-tool/utils/annotate-render.ts
|
|
3264
|
-
async function annotateRenderWithPlaywright(page, misalignedData) {
|
|
3265
|
-
await page.evaluate(
|
|
3266
|
-
(data) => {
|
|
3267
|
-
const { items, styles } = data;
|
|
3268
|
-
const container = document.createElement("div");
|
|
3269
|
-
container.id = "validation-annotations";
|
|
3270
|
-
container.style.cssText = styles.container;
|
|
3271
|
-
items.forEach((item) => {
|
|
3272
|
-
const box = document.createElement("div");
|
|
3273
|
-
const boxStyle = styles.boxes[item.index];
|
|
3274
|
-
if (boxStyle) {
|
|
3275
|
-
box.style.cssText = boxStyle;
|
|
3276
|
-
}
|
|
3277
|
-
container.appendChild(box);
|
|
3278
|
-
const label = document.createElement("div");
|
|
3279
|
-
label.textContent = `${item.index}`;
|
|
3280
|
-
const circleLabelStyle = styles.circleLabels[item.index];
|
|
3281
|
-
if (circleLabelStyle) {
|
|
3282
|
-
label.style.cssText = circleLabelStyle;
|
|
3283
|
-
}
|
|
3284
|
-
container.appendChild(label);
|
|
3285
|
-
const textLabel = document.createElement("div");
|
|
3286
|
-
textLabel.textContent = `${item.componentName} (${item.distance.toFixed(1)}px)`;
|
|
3287
|
-
const textLabelStyle = styles.textLabels[item.index];
|
|
3288
|
-
if (textLabelStyle) {
|
|
3289
|
-
textLabel.style.cssText = textLabelStyle;
|
|
3290
|
-
}
|
|
3291
|
-
container.appendChild(textLabel);
|
|
3292
|
-
});
|
|
3293
|
-
document.body.appendChild(container);
|
|
3294
|
-
},
|
|
3295
|
-
{
|
|
3296
|
-
items: misalignedData,
|
|
3297
|
-
styles: {
|
|
3298
|
-
container: createContainerStyle(),
|
|
3299
|
-
boxes: Object.fromEntries(
|
|
3300
|
-
misalignedData.map((item) => [
|
|
3301
|
-
item.index,
|
|
3302
|
-
createBoxStyle(item.currentX, item.currentY, item.currentWidth, item.currentHeight, ANNOTATION_COLORS.RED)
|
|
3303
|
-
])
|
|
3304
|
-
),
|
|
3305
|
-
circleLabels: Object.fromEntries(
|
|
3306
|
-
misalignedData.map((item) => [item.index, createCircleLabelStyle(item.currentX, item.currentY, ANNOTATION_COLORS.RED)])
|
|
3307
|
-
),
|
|
3308
|
-
textLabels: Object.fromEntries(
|
|
3309
|
-
misalignedData.map((item) => [
|
|
3310
|
-
item.index,
|
|
3311
|
-
createTextLabelStyle(item.currentX, item.currentY, item.currentHeight, ANNOTATION_COLORS.RED)
|
|
3312
|
-
])
|
|
3313
|
-
)
|
|
3314
|
-
}
|
|
3315
|
-
}
|
|
3316
|
-
);
|
|
3317
|
-
}
|
|
3318
|
-
|
|
3319
|
-
// src/tools/visualization-tool/utils/image-converter.ts
|
|
3320
|
-
import sharp from "sharp";
|
|
3321
|
-
var SCREENSHOT_WEBP_QUALITY = 88;
|
|
3322
|
-
async function captureAsWebP(page, options = {}) {
|
|
3323
|
-
const pngBuffer = await page.screenshot({
|
|
3324
|
-
type: "png",
|
|
3325
|
-
fullPage: options.fullPage ?? false
|
|
3326
|
-
});
|
|
3327
|
-
return await bufferToWebPDataUri(pngBuffer);
|
|
3328
|
-
}
|
|
3329
|
-
async function bufferToWebPDataUri(buffer) {
|
|
3330
|
-
const webpBuffer = await sharp(buffer).webp({ quality: SCREENSHOT_WEBP_QUALITY }).toBuffer();
|
|
3331
|
-
return `data:image/webp;base64,${webpBuffer.toString("base64")}`;
|
|
3332
|
-
}
|
|
3333
|
-
|
|
3334
|
-
// src/tools/visualization-tool/utils/annotate-target.ts
|
|
3335
|
-
async function annotateTargetWithPlaywright(browser, figmaThumbnailBase64, misalignedData, viewport, designOffset = { x: 0, y: 0 }, isThumbnailCropped = true) {
|
|
3336
|
-
const context = await browser.newContext({ viewport });
|
|
3337
|
-
const page = await context.newPage();
|
|
3338
|
-
try {
|
|
3339
|
-
const dataUri = figmaThumbnailBase64.startsWith("data:") ? figmaThumbnailBase64 : `data:image/png;base64,${figmaThumbnailBase64}`;
|
|
3340
|
-
const html = `
|
|
3341
|
-
<!DOCTYPE html>
|
|
3342
|
-
<html>
|
|
3343
|
-
<head>
|
|
3344
|
-
<style>
|
|
3345
|
-
* { margin: 0; padding: 0; }
|
|
3346
|
-
body {
|
|
3347
|
-
width: ${viewport.width}px;
|
|
3348
|
-
height: ${viewport.height}px;
|
|
3349
|
-
background-image: url('${dataUri}');
|
|
3350
|
-
background-size: 100% 100%;
|
|
3351
|
-
background-repeat: no-repeat;
|
|
3352
|
-
background-position: top left;
|
|
3353
|
-
position: relative;
|
|
3354
|
-
overflow: hidden;
|
|
3355
|
-
}
|
|
3356
|
-
</style>
|
|
3357
|
-
</head>
|
|
3358
|
-
<body></body>
|
|
3359
|
-
</html>
|
|
3360
|
-
`;
|
|
3361
|
-
await page.setContent(html);
|
|
3362
|
-
await page.waitForLoadState("domcontentloaded");
|
|
3363
|
-
const offsetX = isThumbnailCropped ? 0 : designOffset.x;
|
|
3364
|
-
const offsetY = isThumbnailCropped ? 0 : designOffset.y;
|
|
3365
|
-
const elementsData = misalignedData.map((item, i) => ({
|
|
3366
|
-
index: i + 1,
|
|
3367
|
-
componentName: item.componentName,
|
|
3368
|
-
targetX: item.targetX + offsetX,
|
|
3369
|
-
targetY: item.targetY + offsetY,
|
|
3370
|
-
width: item.targetWidth,
|
|
3371
|
-
height: item.targetHeight
|
|
3372
|
-
}));
|
|
3373
|
-
const styles = {
|
|
3374
|
-
container: createContainerStyle(),
|
|
3375
|
-
boxes: Object.fromEntries(
|
|
3376
|
-
elementsData.map((item) => [
|
|
3377
|
-
item.index,
|
|
3378
|
-
createBoxStyle(item.targetX, item.targetY, item.width, item.height, ANNOTATION_COLORS.GREEN)
|
|
3379
|
-
])
|
|
3380
|
-
),
|
|
3381
|
-
circleLabels: Object.fromEntries(
|
|
3382
|
-
elementsData.map((item) => [item.index, createCircleLabelStyle(item.targetX, item.targetY, ANNOTATION_COLORS.GREEN)])
|
|
3383
|
-
),
|
|
3384
|
-
textLabels: Object.fromEntries(
|
|
3385
|
-
elementsData.map((item) => [
|
|
3386
|
-
item.index,
|
|
3387
|
-
createTextLabelStyle(item.targetX, item.targetY, item.height, ANNOTATION_COLORS.GREEN)
|
|
3388
|
-
])
|
|
3389
|
-
)
|
|
3390
|
-
};
|
|
3391
|
-
await page.evaluate(
|
|
3392
|
-
(data) => {
|
|
3393
|
-
const { items, styles: styles2 } = data;
|
|
3394
|
-
const container = document.createElement("div");
|
|
3395
|
-
container.id = "target-annotations";
|
|
3396
|
-
container.style.cssText = styles2.container;
|
|
3397
|
-
items.forEach((item) => {
|
|
3398
|
-
const box = document.createElement("div");
|
|
3399
|
-
const boxStyle = styles2.boxes[item.index];
|
|
3400
|
-
if (boxStyle) {
|
|
3401
|
-
box.style.cssText = boxStyle;
|
|
3402
|
-
}
|
|
3403
|
-
container.appendChild(box);
|
|
3404
|
-
const label = document.createElement("div");
|
|
3405
|
-
label.textContent = `${item.index}`;
|
|
3406
|
-
const circleLabelStyle = styles2.circleLabels[item.index];
|
|
3407
|
-
if (circleLabelStyle) {
|
|
3408
|
-
label.style.cssText = circleLabelStyle;
|
|
3409
|
-
}
|
|
3410
|
-
container.appendChild(label);
|
|
3411
|
-
const textLabel = document.createElement("div");
|
|
3412
|
-
textLabel.textContent = `${item.componentName}`;
|
|
3413
|
-
const textLabelStyle = styles2.textLabels[item.index];
|
|
3414
|
-
if (textLabelStyle) {
|
|
3415
|
-
textLabel.style.cssText = textLabelStyle;
|
|
3416
|
-
}
|
|
3417
|
-
container.appendChild(textLabel);
|
|
3418
|
-
});
|
|
3419
|
-
document.body.appendChild(container);
|
|
3420
|
-
},
|
|
3421
|
-
{ items: elementsData, styles }
|
|
3422
|
-
);
|
|
3423
|
-
const screenshotBuffer = await page.screenshot({ type: "png", fullPage: false });
|
|
3424
|
-
return await bufferToWebPDataUri(screenshotBuffer);
|
|
3425
|
-
} finally {
|
|
3426
|
-
await page.close();
|
|
3427
|
-
await context.close();
|
|
3428
|
-
}
|
|
3429
|
-
}
|
|
3430
|
-
|
|
3431
|
-
// src/nodes/validation/utils/playwright/launcher.ts
|
|
3432
|
-
init_logger();
|
|
3433
|
-
import { chromium } from "playwright";
|
|
3434
|
-
import { spawn } from "child_process";
|
|
3435
|
-
import { createRequire } from "module";
|
|
3436
|
-
import { dirname, join } from "path";
|
|
3437
|
-
var MISSING_EXECUTABLE_MESSAGE = "Executable doesn't exist";
|
|
3438
|
-
function getLocalPlaywrightCliPath() {
|
|
3439
|
-
const require2 = createRequire(import.meta.url);
|
|
3440
|
-
const playwrightMain = require2.resolve("playwright");
|
|
3441
|
-
return join(dirname(playwrightMain), "cli.js");
|
|
3442
|
-
}
|
|
3443
|
-
var installPromise = null;
|
|
3444
|
-
var installCompleted = false;
|
|
3445
|
-
async function installChromiumBrowsers() {
|
|
3446
|
-
if (installCompleted) return;
|
|
3447
|
-
if (!installPromise) {
|
|
3448
|
-
logger.printInfoLog("Playwright Chromium binaries are missing. Installing them automatically...");
|
|
3449
|
-
installPromise = new Promise((resolve2, reject) => {
|
|
3450
|
-
const cliPath = getLocalPlaywrightCliPath();
|
|
3451
|
-
const child = spawn(process.execPath, [cliPath, "install", "chromium"], {
|
|
3452
|
-
stdio: "inherit",
|
|
3453
|
-
env: process.env
|
|
3454
|
-
});
|
|
3455
|
-
child.on("error", (error) => {
|
|
3456
|
-
installPromise = null;
|
|
3457
|
-
reject(error);
|
|
3458
|
-
});
|
|
3459
|
-
child.on("close", (code) => {
|
|
3460
|
-
installPromise = null;
|
|
3461
|
-
if (code === 0) {
|
|
3462
|
-
installCompleted = true;
|
|
3463
|
-
logger.printSuccessLog("Playwright Chromium installation finished.");
|
|
3464
|
-
resolve2();
|
|
3465
|
-
} else {
|
|
3466
|
-
reject(new Error(`Playwright install exited with code ${code}`));
|
|
3467
|
-
}
|
|
3468
|
-
});
|
|
3469
|
-
});
|
|
3470
|
-
}
|
|
3471
|
-
return installPromise;
|
|
3472
|
-
}
|
|
3473
|
-
function isMissingExecutableError(error) {
|
|
3474
|
-
return error instanceof Error && error.message.includes(MISSING_EXECUTABLE_MESSAGE);
|
|
3475
|
-
}
|
|
3476
|
-
async function launchChromiumWithAutoInstall(options = {}) {
|
|
3477
|
-
const launchOptions = { headless: true, ...options };
|
|
3478
|
-
try {
|
|
3479
|
-
return await chromium.launch(launchOptions);
|
|
3480
|
-
} catch (error) {
|
|
3481
|
-
if (isMissingExecutableError(error)) {
|
|
3482
|
-
try {
|
|
3483
|
-
await installChromiumBrowsers();
|
|
3484
|
-
} catch (installError) {
|
|
3485
|
-
throw new Error(
|
|
3486
|
-
`Failed to auto-install Playwright browsers required for validation.
|
|
3487
|
-
Please run "npx playwright install chromium" or "pnpm exec playwright install chromium" manually and retry.
|
|
3488
|
-
Installer error: ${installError.message}`
|
|
3489
|
-
);
|
|
3490
|
-
}
|
|
3491
|
-
return chromium.launch(launchOptions);
|
|
3492
|
-
}
|
|
3493
|
-
throw error;
|
|
3494
|
-
}
|
|
3495
|
-
}
|
|
3496
|
-
|
|
3497
|
-
// src/tools/visualization-tool/utils/browser-management.ts
|
|
3498
|
-
async function browserManagement(viewport, operation) {
|
|
3499
|
-
let browser = null;
|
|
3500
|
-
let page = null;
|
|
3501
|
-
try {
|
|
3502
|
-
browser = await launchChromiumWithAutoInstall({ headless: true });
|
|
3503
|
-
const context = await browser.newContext({ viewport });
|
|
3504
|
-
page = await context.newPage();
|
|
3505
|
-
return await operation(browser, page);
|
|
3506
|
-
} finally {
|
|
3507
|
-
if (page) await page.close().catch(() => {
|
|
3508
|
-
});
|
|
3509
|
-
if (browser) await browser.close().catch(() => {
|
|
3510
|
-
});
|
|
3511
|
-
}
|
|
3512
|
-
}
|
|
3513
|
-
|
|
3514
|
-
// src/tools/visualization-tool/utils/combine.ts
|
|
3515
|
-
import fs5 from "fs";
|
|
3516
|
-
import path6 from "path";
|
|
3517
|
-
import sharp2 from "sharp";
|
|
3518
|
-
var DEFAULT_OPTIONS = {
|
|
3519
|
-
leftHeader: "RENDER (Current)",
|
|
3520
|
-
rightHeader: "TARGET (Expected)",
|
|
3521
|
-
gapWidth: 40,
|
|
3522
|
-
headerHeight: 60
|
|
3523
|
-
};
|
|
3524
|
-
async function combineSideBySide(renderBase64, targetBase64, outputPath, options) {
|
|
3525
|
-
const opts = { ...DEFAULT_OPTIONS, ...options };
|
|
3526
|
-
try {
|
|
3527
|
-
const leftBase64 = extractBase64Data(renderBase64);
|
|
3528
|
-
const rightBase64 = extractBase64Data(targetBase64);
|
|
3529
|
-
const leftBuffer = Buffer.from(leftBase64, "base64");
|
|
3530
|
-
const rightBuffer = Buffer.from(rightBase64, "base64");
|
|
3531
|
-
const leftMeta = await sharp2(leftBuffer).metadata();
|
|
3532
|
-
const rightMeta = await sharp2(rightBuffer).metadata();
|
|
3533
|
-
if (!leftMeta.width || !leftMeta.height || !rightMeta.width || !rightMeta.height) {
|
|
3534
|
-
throw new Error("Failed to read image dimensions");
|
|
3535
|
-
}
|
|
3536
|
-
const scale = leftMeta.height / rightMeta.height;
|
|
3537
|
-
const rightResizedWidth = Math.round(rightMeta.width * scale);
|
|
3538
|
-
const rightResized = await sharp2(rightBuffer).resize({ height: leftMeta.height, width: rightResizedWidth, fit: "fill" }).toBuffer();
|
|
3539
|
-
const combinedWidth = leftMeta.width + opts.gapWidth + rightResizedWidth;
|
|
3540
|
-
const combinedHeight = opts.headerHeight + leftMeta.height;
|
|
3541
|
-
const centerX = leftMeta.width + opts.gapWidth / 2;
|
|
3542
|
-
const leftTextX = leftMeta.width / 2;
|
|
3543
|
-
const rightTextX = leftMeta.width + opts.gapWidth + rightResizedWidth / 2;
|
|
3544
|
-
const textY = opts.headerHeight / 2 + 8;
|
|
3545
|
-
const headerSvg = `
|
|
3546
|
-
<svg width="${combinedWidth}" height="${opts.headerHeight}">
|
|
3547
|
-
<rect width="${combinedWidth}" height="${opts.headerHeight}" fill="#F0F0F0"/>
|
|
3548
|
-
<line x1="${centerX}" y1="0" x2="${centerX}" y2="${combinedHeight}"
|
|
3549
|
-
stroke="#C8C8C8" stroke-width="2"/>
|
|
3550
|
-
<text x="${leftTextX}" y="${textY}"
|
|
3551
|
-
font-family="Arial" font-size="24" font-weight="bold" fill="#C80000"
|
|
3552
|
-
text-anchor="middle">
|
|
3553
|
-
${opts.leftHeader}
|
|
3554
|
-
</text>
|
|
3555
|
-
<text x="${rightTextX}" y="${textY}"
|
|
3556
|
-
font-family="Arial" font-size="24" font-weight="bold" fill="#009600"
|
|
3557
|
-
text-anchor="middle">
|
|
3558
|
-
${opts.rightHeader}
|
|
3559
|
-
</text>
|
|
3560
|
-
</svg>
|
|
3561
|
-
`;
|
|
3562
|
-
const headerBuffer = Buffer.from(headerSvg);
|
|
3563
|
-
const outputDir = path6.dirname(outputPath);
|
|
3564
|
-
if (!fs5.existsSync(outputDir)) {
|
|
3565
|
-
fs5.mkdirSync(outputDir, { recursive: true });
|
|
3566
|
-
}
|
|
3567
|
-
await sharp2({
|
|
3568
|
-
create: {
|
|
3569
|
-
width: combinedWidth,
|
|
3570
|
-
height: combinedHeight,
|
|
3571
|
-
channels: 3,
|
|
3572
|
-
background: { r: 255, g: 255, b: 255 }
|
|
3573
|
-
}
|
|
3574
|
-
}).composite([
|
|
3575
|
-
{ input: headerBuffer, top: 0, left: 0 },
|
|
3576
|
-
{ input: leftBuffer, top: opts.headerHeight, left: 0 },
|
|
3577
|
-
{ input: rightResized, top: opts.headerHeight, left: leftMeta.width + opts.gapWidth }
|
|
3578
|
-
]).webp({ quality: SCREENSHOT_WEBP_QUALITY }).toFile(outputPath);
|
|
3579
|
-
} catch (error) {
|
|
3580
|
-
throw new Error(`Failed to combine side-by-side screenshots: ${error instanceof Error ? error.message : String(error)}`);
|
|
3581
|
-
}
|
|
3582
|
-
}
|
|
3583
|
-
function extractBase64Data(base64String) {
|
|
3584
|
-
const dataUriMatch = base64String.match(/^data:image\/[a-z]+;base64,(.+)$/);
|
|
3585
|
-
if (dataUriMatch && dataUriMatch[1]) {
|
|
3586
|
-
return dataUriMatch[1];
|
|
3587
|
-
}
|
|
3588
|
-
return base64String;
|
|
3589
|
-
}
|
|
3590
|
-
|
|
3591
|
-
// src/tools/visualization-tool/utils/pixel-diff-heatmap.ts
|
|
3592
|
-
init_logger();
|
|
3593
|
-
import sharp3 from "sharp";
|
|
3594
|
-
import pixelmatch from "pixelmatch";
|
|
3595
|
-
async function generatePixelDiffHeatmap(renderSnap, targetSnap) {
|
|
3596
|
-
const img1Data = await loadImageData(renderSnap);
|
|
3597
|
-
const img2Data = await loadImageData(targetSnap);
|
|
3598
|
-
let { width: width1, height: height1, data: data1 } = img1Data;
|
|
3599
|
-
let { width: width2, height: height2, data: data2 } = img2Data;
|
|
3600
|
-
if (width1 !== width2 || height1 !== height2) {
|
|
3601
|
-
const resized = await sharp3(data2, {
|
|
3602
|
-
raw: { width: width2, height: height2, channels: 4 }
|
|
3603
|
-
}).resize(width1, height1, { fit: "fill" }).ensureAlpha().raw().toBuffer({ resolveWithObject: true });
|
|
3604
|
-
data2 = resized.data;
|
|
3605
|
-
width2 = resized.info.width;
|
|
3606
|
-
height2 = resized.info.height;
|
|
3607
|
-
}
|
|
3608
|
-
const MAX_DIMENSION = 2500;
|
|
3609
|
-
if (height1 > MAX_DIMENSION || width1 > MAX_DIMENSION) {
|
|
3610
|
-
const scale = MAX_DIMENSION / Math.max(height1, width1);
|
|
3611
|
-
const newWidth = Math.floor(width1 * scale);
|
|
3612
|
-
const newHeight = Math.floor(height1 * scale);
|
|
3613
|
-
const downsampled1 = await sharp3(data1, {
|
|
3614
|
-
raw: { width: width1, height: height1, channels: 4 }
|
|
3615
|
-
}).resize(newWidth, newHeight, { kernel: "lanczos3" }).ensureAlpha().raw().toBuffer({ resolveWithObject: true });
|
|
3616
|
-
const downsampled2 = await sharp3(data2, {
|
|
3617
|
-
raw: { width: width2, height: height2, channels: 4 }
|
|
3618
|
-
}).resize(newWidth, newHeight, { kernel: "lanczos3" }).ensureAlpha().raw().toBuffer({ resolveWithObject: true });
|
|
3619
|
-
data1 = downsampled1.data;
|
|
3620
|
-
data2 = downsampled2.data;
|
|
3621
|
-
width1 = downsampled1.info.width;
|
|
3622
|
-
height1 = downsampled1.info.height;
|
|
3623
|
-
width2 = downsampled2.info.width;
|
|
3624
|
-
height2 = downsampled2.info.height;
|
|
3625
|
-
}
|
|
3626
|
-
const diffBuffer = Buffer.alloc(width1 * height1 * 4);
|
|
3627
|
-
const numDiffPixels = pixelmatch(data1, data2, diffBuffer, width1, height1, {
|
|
3628
|
-
threshold: 0.1,
|
|
3629
|
-
includeAA: false,
|
|
3630
|
-
alpha: 0.1,
|
|
3631
|
-
diffColor: [255, 0, 0],
|
|
3632
|
-
diffMask: false
|
|
3633
|
-
});
|
|
3634
|
-
const totalPixels = width1 * height1;
|
|
3635
|
-
const diffPercentage = (numDiffPixels / totalPixels * 100).toFixed(2);
|
|
3636
|
-
logger.printTestLog(`[Pixelmatch] Different pixels: ${numDiffPixels} / ${totalPixels} (${diffPercentage}%)`);
|
|
3637
|
-
const heatmapBuffer = await generateRedGreenOverlay(data1, data2, diffBuffer, width1, height1);
|
|
3638
|
-
return await bufferToWebPDataUri(heatmapBuffer);
|
|
3639
|
-
}
|
|
3640
|
-
async function generateRedGreenOverlay(data1, data2, diffBuffer, width, height) {
|
|
3641
|
-
const outputData = Buffer.alloc(width * height * 3);
|
|
3642
|
-
const differences = new Float32Array(width * height);
|
|
3643
|
-
let maxDiff = 0;
|
|
3644
|
-
for (let i = 0; i < width * height; i++) {
|
|
3645
|
-
const r1 = data1[i * 4] ?? 0;
|
|
3646
|
-
const g1 = data1[i * 4 + 1] ?? 0;
|
|
3647
|
-
const b1 = data1[i * 4 + 2] ?? 0;
|
|
3648
|
-
const r2 = data2[i * 4] ?? 0;
|
|
3649
|
-
const g2 = data2[i * 4 + 1] ?? 0;
|
|
3650
|
-
const b2 = data2[i * 4 + 2] ?? 0;
|
|
3651
|
-
const diff = Math.sqrt(Math.pow(r1 - r2, 2) * 0.299 + Math.pow(g1 - g2, 2) * 0.587 + Math.pow(b1 - b2, 2) * 0.114);
|
|
3652
|
-
differences[i] = diff;
|
|
3653
|
-
maxDiff = Math.max(maxDiff, diff);
|
|
3654
|
-
}
|
|
3655
|
-
let pixelsWithDifferences = 0;
|
|
3656
|
-
for (let y = 0; y < height; y++) {
|
|
3657
|
-
for (let x = 0; x < width; x++) {
|
|
3658
|
-
const idx = y * width + x;
|
|
3659
|
-
const outIdx = idx * 3;
|
|
3660
|
-
const isDifferent = (diffBuffer[idx * 4] ?? 0) > 0;
|
|
3661
|
-
if (isDifferent) {
|
|
3662
|
-
pixelsWithDifferences++;
|
|
3663
|
-
const normalizedDiff = maxDiff > 0 ? (differences[idx] ?? 0) / maxDiff : 0;
|
|
3664
|
-
const [r, g, b] = getDiffColor(normalizedDiff);
|
|
3665
|
-
outputData[outIdx] = r;
|
|
3666
|
-
outputData[outIdx + 1] = g;
|
|
3667
|
-
outputData[outIdx + 2] = b;
|
|
3668
|
-
} else {
|
|
3669
|
-
outputData[outIdx] = 0;
|
|
3670
|
-
outputData[outIdx + 1] = 0;
|
|
3671
|
-
outputData[outIdx + 2] = 200;
|
|
3672
|
-
}
|
|
3673
|
-
}
|
|
3674
|
-
}
|
|
3675
|
-
logger.printTestLog(
|
|
3676
|
-
`[Heatmap] Pixels with differences: ${pixelsWithDifferences} / ${width * height} (${(pixelsWithDifferences / (width * height) * 100).toFixed(2)}%)`
|
|
3677
|
-
);
|
|
3678
|
-
return sharp3(outputData, { raw: { width, height, channels: 3 } }).png().toBuffer();
|
|
3679
|
-
}
|
|
3680
|
-
function getDiffColor(normalizedDiff) {
|
|
3681
|
-
normalizedDiff = Math.max(0, Math.min(1, normalizedDiff));
|
|
3682
|
-
if (normalizedDiff < 0.33) {
|
|
3683
|
-
const t = normalizedDiff / 0.33;
|
|
3684
|
-
return [0, Math.round(255 * t), 255];
|
|
3685
|
-
} else if (normalizedDiff < 0.67) {
|
|
3686
|
-
const t = (normalizedDiff - 0.33) / 0.34;
|
|
3687
|
-
return [Math.round(255 * t), 255, Math.round(255 * (1 - t))];
|
|
3688
|
-
} else {
|
|
3689
|
-
const t = (normalizedDiff - 0.67) / 0.33;
|
|
3690
|
-
return [255, Math.round(255 * (1 - t)), 0];
|
|
3691
|
-
}
|
|
3692
|
-
}
|
|
3693
|
-
async function loadImageData(dataUri) {
|
|
3694
|
-
if (!dataUri.startsWith("data:")) {
|
|
3695
|
-
const axios4 = (await import("axios")).default;
|
|
3696
|
-
const response = await axios4.get(dataUri, { responseType: "arraybuffer", timeout: 3e4 });
|
|
3697
|
-
const base64 = Buffer.from(response.data).toString("base64");
|
|
3698
|
-
dataUri = `data:image/png;base64,${base64}`;
|
|
3699
|
-
}
|
|
3700
|
-
const base64Data = dataUri.split(",")[1];
|
|
3701
|
-
if (!base64Data) {
|
|
3702
|
-
throw new Error("Invalid data URI format: missing base64 data");
|
|
3703
|
-
}
|
|
3704
|
-
const buffer = Buffer.from(base64Data, "base64");
|
|
3705
|
-
const image = sharp3(buffer);
|
|
3706
|
-
const { data, info } = await image.ensureAlpha().raw().toBuffer({ resolveWithObject: true });
|
|
3707
|
-
return { width: info.width, height: info.height, data };
|
|
3708
|
-
}
|
|
3709
|
-
|
|
3710
|
-
// src/tools/visualization-tool/index.ts
|
|
3711
|
-
init_logger();
|
|
3712
|
-
var _VisualizationTool_decorators, _init5;
|
|
3713
|
-
_VisualizationTool_decorators = [tools5({
|
|
3714
|
-
annotateRender: {
|
|
3715
|
-
description: "Navigate to the dev server, capture a clean render screenshot, inject RED annotation boxes for misaligned components, and capture the annotated render screenshot.",
|
|
3716
|
-
params: [
|
|
3717
|
-
{ name: "serverUrl", type: "string", description: "Dev server URL (e.g., http://localhost:5173)" },
|
|
3718
|
-
{
|
|
3719
|
-
name: "misalignedData",
|
|
3720
|
-
type: "object",
|
|
3721
|
-
description: "MisalignedComponentData[] used to draw RED boxes on the render."
|
|
3722
|
-
},
|
|
3723
|
-
{ name: "viewport", type: "object", description: "Viewport {width,height} to use in Playwright." }
|
|
3724
|
-
],
|
|
3725
|
-
returns: {
|
|
3726
|
-
type: "object",
|
|
3727
|
-
description: "AnnotateRenderResult containing renderSnap and renderMarked (WebP data URIs)."
|
|
3728
|
-
}
|
|
3729
|
-
},
|
|
3730
|
-
annotateTarget: {
|
|
3731
|
-
description: "Fetch the Figma thumbnail, inject GREEN annotation boxes for misaligned components, and return an annotated target screenshot.",
|
|
3732
|
-
params: [
|
|
3733
|
-
{ name: "figmaThumbnailUrl", type: "string", description: "Figma thumbnail CDN URL." },
|
|
3734
|
-
{
|
|
3735
|
-
name: "misalignedData",
|
|
3736
|
-
type: "object",
|
|
3737
|
-
description: "MisalignedComponentData[] used to draw GREEN boxes on the target."
|
|
3738
|
-
},
|
|
3739
|
-
{ name: "viewport", type: "object", description: "Viewport {width,height} to match the Figma thumbnail." },
|
|
3740
|
-
{
|
|
3741
|
-
name: "designOffset",
|
|
3742
|
-
type: "object",
|
|
3743
|
-
description: "Design offset {x,y} used when the thumbnail is not cropped (default behavior is cropped).",
|
|
3744
|
-
optional: true
|
|
3745
|
-
}
|
|
3746
|
-
],
|
|
3747
|
-
returns: { type: "string", description: "Annotated target screenshot as WebP data URI." }
|
|
3748
|
-
},
|
|
3749
|
-
annotateTargetFromBase64: {
|
|
3750
|
-
description: "Annotate target using pre-cached base64 thumbnail data. Use this to avoid redundant Figma thumbnail downloads.",
|
|
3751
|
-
params: [
|
|
3752
|
-
{ name: "figmaThumbnailBase64", type: "string", description: "Base64-encoded Figma thumbnail data." },
|
|
3753
|
-
{
|
|
3754
|
-
name: "misalignedData",
|
|
3755
|
-
type: "object",
|
|
3756
|
-
description: "MisalignedComponentData[] used to draw GREEN boxes on the target."
|
|
3757
|
-
},
|
|
3758
|
-
{ name: "viewport", type: "object", description: "Viewport {width,height} to match the Figma thumbnail." },
|
|
3759
|
-
{
|
|
3760
|
-
name: "designOffset",
|
|
3761
|
-
type: "object",
|
|
3762
|
-
description: "Design offset {x,y} used when the thumbnail is not cropped (default behavior is cropped).",
|
|
3763
|
-
optional: true
|
|
3764
|
-
}
|
|
3765
|
-
],
|
|
3766
|
-
returns: { type: "string", description: "Annotated target screenshot as WebP data URI." }
|
|
3767
|
-
},
|
|
3768
|
-
combine: {
|
|
3769
|
-
description: "Combine two base64 screenshots side-by-side with headers and write to outputPath (WebP).",
|
|
3770
|
-
params: [
|
|
3771
|
-
{ name: "renderMarked", type: "string", description: "Left image (render, typically annotated) as base64 data URI." },
|
|
3772
|
-
{ name: "targetMarked", type: "string", description: "Right image (target, typically annotated) as base64 data URI." },
|
|
3773
|
-
{ name: "outputPath", type: "string", description: "Filesystem output path for the combined WebP." },
|
|
3774
|
-
{ name: "options", type: "object", description: "Optional CombineOptions", optional: true }
|
|
3775
|
-
],
|
|
3776
|
-
returns: { type: "string", description: "The outputPath written." }
|
|
3777
|
-
},
|
|
3778
|
-
diffHeatmap: {
|
|
3779
|
-
description: "Generate pixel-diff heatmap between a render screenshot and the target thumbnail.",
|
|
3780
|
-
params: [
|
|
3781
|
-
{ name: "renderSnap", type: "string", description: "Render screenshot (WebP data URI)." },
|
|
3782
|
-
{ name: "targetSnap", type: "string", description: "Target screenshot (WebP data URI) OR URL." }
|
|
3783
|
-
],
|
|
3784
|
-
returns: { type: "string", description: "Heatmap image as WebP data URI." }
|
|
3785
|
-
},
|
|
3786
|
-
generateIterationScreenshot: {
|
|
3787
|
-
description: "Generate annotated comparison screenshot for validation iteration. Orchestrates annotate + combine for convenience.",
|
|
3788
|
-
params: [
|
|
3789
|
-
{ name: "misalignedComponents", type: "object", description: "MisalignedComponent[] from validation result" },
|
|
3790
|
-
{ name: "serverUrl", type: "string", description: "Dev server URL" },
|
|
3791
|
-
{ name: "figmaThumbnailUrl", type: "string", description: "Figma thumbnail URL" },
|
|
3792
|
-
{ name: "viewport", type: "object", description: "Viewport {width, height}" },
|
|
3793
|
-
{ name: "designOffset", type: "object", description: "Design offset {x, y}" },
|
|
3794
|
-
{ name: "outputPath", type: "string", description: "Output file path for combined screenshot" },
|
|
3795
|
-
{
|
|
3796
|
-
name: "cachedFigmaThumbnailBase64",
|
|
3797
|
-
type: "string",
|
|
3798
|
-
description: "Optional cached thumbnail base64",
|
|
3799
|
-
optional: true
|
|
3800
|
-
}
|
|
3801
|
-
],
|
|
3802
|
-
returns: { type: "object", description: "IterationScreenshotResult with renderMarked, targetMarked, and combinedPath" }
|
|
3803
|
-
}
|
|
3804
|
-
})];
|
|
3805
|
-
var VisualizationTool = class {
|
|
3806
|
-
async annotateRender(serverUrl, misalignedData, viewport) {
|
|
3807
|
-
return await browserManagement(viewport, async (_browser, page) => {
|
|
3808
|
-
await page.goto(serverUrl, { waitUntil: "domcontentloaded", timeout: 3e4 });
|
|
3809
|
-
await new Promise((r) => setTimeout(r, 1e3));
|
|
3810
|
-
const renderSnap = await captureAsWebP(page);
|
|
3811
|
-
await annotateRenderWithPlaywright(page, misalignedData);
|
|
3812
|
-
const renderMarked = await captureAsWebP(page);
|
|
3813
|
-
return { renderSnap, renderMarked };
|
|
3814
|
-
});
|
|
3815
|
-
}
|
|
3816
|
-
async annotateTarget(figmaThumbnailUrl, misalignedData, viewport, designOffset) {
|
|
3817
|
-
const axios4 = (await import("axios")).default;
|
|
3818
|
-
const response = await axios4.get(figmaThumbnailUrl, { responseType: "arraybuffer", timeout: 3e4 });
|
|
3819
|
-
const figmaThumbnailBase64 = Buffer.from(response.data).toString("base64");
|
|
3820
|
-
return await browserManagement(viewport, async (browser) => {
|
|
3821
|
-
return await annotateTargetWithPlaywright(browser, figmaThumbnailBase64, misalignedData, viewport, designOffset);
|
|
3822
|
-
});
|
|
3823
|
-
}
|
|
3824
|
-
/**
|
|
3825
|
-
* Annotate target using pre-cached base64 thumbnail data.
|
|
3826
|
-
* Use this method when you have already downloaded the Figma thumbnail to avoid redundant downloads.
|
|
3827
|
-
*/
|
|
3828
|
-
async annotateTargetFromBase64(figmaThumbnailBase64, misalignedData, viewport, designOffset) {
|
|
3829
|
-
return await browserManagement(viewport, async (browser) => {
|
|
3830
|
-
return await annotateTargetWithPlaywright(browser, figmaThumbnailBase64, misalignedData, viewport, designOffset);
|
|
3831
|
-
});
|
|
3832
|
-
}
|
|
3833
|
-
async combine(renderMarked, targetMarked, outputPath, options) {
|
|
3834
|
-
await combineSideBySide(renderMarked, targetMarked, outputPath, options);
|
|
3835
|
-
return outputPath;
|
|
3836
|
-
}
|
|
3837
|
-
async diffHeatmap(renderSnap, targetSnap) {
|
|
3838
|
-
return await generatePixelDiffHeatmap(renderSnap, targetSnap);
|
|
3839
|
-
}
|
|
3840
|
-
/**
|
|
3841
|
-
* Generate annotated comparison screenshot for a single iteration.
|
|
3842
|
-
* This is a convenience method that orchestrates the full screenshot workflow.
|
|
3843
|
-
*
|
|
3844
|
-
* @returns IterationScreenshotResult with individual annotated screenshots and combined path
|
|
3845
|
-
*/
|
|
3846
|
-
async generateIterationScreenshot(misalignedComponents, serverUrl, figmaThumbnailUrl, viewport, designOffset, outputPath, cachedFigmaThumbnailBase64) {
|
|
3847
|
-
if (misalignedComponents.length === 0) {
|
|
3848
|
-
return { renderMarked: "", targetMarked: "", combinedPath: "" };
|
|
3849
|
-
}
|
|
3850
|
-
try {
|
|
3851
|
-
const misalignedData = this.formatForVisualization(misalignedComponents);
|
|
3852
|
-
const render = await this.annotateRender(serverUrl, misalignedData, viewport);
|
|
3853
|
-
const targetMarked = cachedFigmaThumbnailBase64 ? await this.annotateTargetFromBase64(cachedFigmaThumbnailBase64, misalignedData, viewport, designOffset) : await this.annotateTarget(figmaThumbnailUrl, misalignedData, viewport, designOffset);
|
|
3854
|
-
await this.combine(render.renderMarked, targetMarked, outputPath);
|
|
3855
|
-
logger.printInfoLog(`Saved comparison screenshot: ${path7.basename(outputPath)}`);
|
|
3856
|
-
return {
|
|
3857
|
-
renderMarked: render.renderMarked,
|
|
3858
|
-
targetMarked,
|
|
3859
|
-
combinedPath: outputPath
|
|
3860
|
-
};
|
|
3861
|
-
} catch (error) {
|
|
3862
|
-
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
3863
|
-
logger.printWarnLog(`Failed to generate iteration screenshot: ${errorMsg}`);
|
|
3864
|
-
return { renderMarked: "", targetMarked: "", combinedPath: "" };
|
|
3865
|
-
}
|
|
3866
|
-
}
|
|
3867
|
-
/**
|
|
3868
|
-
* Transform MisalignedComponent[] to MisalignedComponentData[] format.
|
|
3869
|
-
* This is an internal helper for converting validation results to visualization format.
|
|
3870
|
-
*/
|
|
3871
|
-
formatForVisualization(components) {
|
|
3872
|
-
return components.map((comp, idx) => {
|
|
3873
|
-
if (comp.elementIds.length === 0) {
|
|
3874
|
-
logger.printWarnLog(`Component ${comp.name} has no elementIds. Using componentId as fallback.`);
|
|
3875
|
-
}
|
|
3876
|
-
return {
|
|
3877
|
-
index: idx + 1,
|
|
3878
|
-
elementId: comp.elementIds[0] || comp.componentId,
|
|
3879
|
-
elementName: comp.name,
|
|
3880
|
-
componentId: comp.componentId,
|
|
3881
|
-
componentName: comp.name,
|
|
3882
|
-
componentPath: comp.path,
|
|
3883
|
-
currentX: comp.currentX,
|
|
3884
|
-
currentY: comp.currentY,
|
|
3885
|
-
currentWidth: comp.currentWidth,
|
|
3886
|
-
currentHeight: comp.currentHeight,
|
|
3887
|
-
targetX: comp.targetX,
|
|
3888
|
-
targetY: comp.targetY,
|
|
3889
|
-
targetWidth: comp.targetWidth,
|
|
3890
|
-
targetHeight: comp.targetHeight,
|
|
3891
|
-
distance: comp.distance,
|
|
3892
|
-
xDelta: comp.validationReport.absoluteError[0],
|
|
3893
|
-
yDelta: comp.validationReport.absoluteError[1]
|
|
3894
|
-
};
|
|
3895
|
-
});
|
|
3896
|
-
}
|
|
3897
|
-
};
|
|
3898
|
-
_init5 = __decoratorStart(null);
|
|
3899
|
-
VisualizationTool = __decorateElement(_init5, 0, "VisualizationTool", _VisualizationTool_decorators, VisualizationTool);
|
|
3900
|
-
__runInitializers(_init5, 1, VisualizationTool);
|
|
3901
|
-
|
|
3902
|
-
// src/tools/report-tool/index.ts
|
|
3903
|
-
var _ReportTool_decorators, _init6;
|
|
3904
|
-
_ReportTool_decorators = [tools6({
|
|
3905
|
-
generateReport: {
|
|
3906
|
-
description: "Generate visual validation report structure from pre-computed validation results. Creates annotated screenshots, heatmap, and userReport structure.",
|
|
3907
|
-
params: [
|
|
3908
|
-
{
|
|
3909
|
-
name: "request",
|
|
3910
|
-
type: "object",
|
|
3911
|
-
description: "ReportGenerationRequest with validationResult, figmaThumbnailUrl, serverUrl, outputDir, etc."
|
|
3912
|
-
}
|
|
3913
|
-
],
|
|
3914
|
-
returns: { type: "object", description: "ReportGenerationResult with userReport and misalignedCount" }
|
|
3915
|
-
},
|
|
3916
|
-
createMinimalReport: {
|
|
3917
|
-
description: "Create a minimal user report for error scenarios without screenshots",
|
|
3918
|
-
params: [{ name: "options", type: "object", description: "ErrorReportOptions with serverUrl, figmaThumbnailUrl, mae, sae" }],
|
|
3919
|
-
returns: { type: "object", description: "Minimal UserReport structure" }
|
|
3920
|
-
},
|
|
3921
|
-
generateHtml: {
|
|
3922
|
-
description: "Generate standalone HTML report file from userReport with embedded data and inlined assets",
|
|
3923
|
-
params: [
|
|
3924
|
-
{ name: "userReport", type: "object", description: "UserReport data from validation with screenshots and metrics" },
|
|
3925
|
-
{ name: "outputDir", type: "string", description: "Output directory path for HTML file" }
|
|
3926
|
-
],
|
|
3927
|
-
returns: { type: "object", description: "GenerateHtmlResult with success/htmlPath/error" }
|
|
3928
|
-
}
|
|
3929
|
-
})];
|
|
3930
|
-
var ReportTool = class {
|
|
3931
|
-
/**
|
|
3932
|
-
* Generate visual validation report from validation results.
|
|
3933
|
-
*
|
|
3934
|
-
* This method ONLY does visualization and formatting:
|
|
3935
|
-
* - Loads saved annotated screenshots from last validation iteration (avoids port mismatch)
|
|
3936
|
-
* - Generates pixel difference heatmap
|
|
3937
|
-
* - Builds userReport structure
|
|
3938
|
-
*
|
|
3939
|
-
*/
|
|
3940
|
-
async generateReport(request) {
|
|
3941
|
-
logger.printInfoLog("\nGenerating validation report...");
|
|
3942
|
-
const visualizationTool = new VisualizationTool();
|
|
3943
|
-
const { validationResult } = request;
|
|
3944
|
-
const comparisonDir = path8.join(request.outputDir, "comparison_screenshots");
|
|
3945
|
-
if (!fs6.existsSync(comparisonDir)) {
|
|
3946
|
-
fs6.mkdirSync(comparisonDir, { recursive: true });
|
|
3947
|
-
}
|
|
3948
|
-
const misalignedData = visualizationTool["formatForVisualization"](validationResult.misalignedComponents);
|
|
3949
|
-
logger.printInfoLog(`Final misaligned components: ${misalignedData.length}`);
|
|
3950
|
-
const renderMarkedBuffer = await fsPromises.readFile(request.savedRenderMarkedPath);
|
|
3951
|
-
const targetMarkedBuffer = await fsPromises.readFile(request.savedTargetMarkedPath);
|
|
3952
|
-
const renderMarked = `data:image/webp;base64,${renderMarkedBuffer.toString("base64")}`;
|
|
3953
|
-
const targetMarked = `data:image/webp;base64,${targetMarkedBuffer.toString("base64")}`;
|
|
3954
|
-
const screenshots = {
|
|
3955
|
-
renderSnap: validationResult.screenshots?.renderSnap || renderMarked,
|
|
3956
|
-
renderMarked,
|
|
3957
|
-
targetMarked
|
|
3958
|
-
};
|
|
3959
|
-
const finalScreenshotPath = path8.join(comparisonDir, "final.webp");
|
|
3960
|
-
await visualizationTool.combine(screenshots.renderMarked, screenshots.targetMarked, finalScreenshotPath);
|
|
3961
|
-
logger.printSuccessLog(`Saved final comparison screenshot: ${path8.basename(finalScreenshotPath)}`);
|
|
3962
|
-
let heatmap = "";
|
|
3963
|
-
try {
|
|
3964
|
-
heatmap = await visualizationTool.diffHeatmap(screenshots.renderSnap, request.figmaThumbnailUrl);
|
|
3965
|
-
if (heatmap) {
|
|
3966
|
-
const heatmapPath = path8.join(comparisonDir, "heatmap.webp");
|
|
3967
|
-
const base64Data = heatmap.split(",")[1];
|
|
3968
|
-
if (!base64Data) {
|
|
3969
|
-
throw new Error("Invalid heatmap data URI format");
|
|
3970
|
-
}
|
|
3971
|
-
const buffer = Buffer.from(base64Data, "base64");
|
|
3972
|
-
await fs6.promises.writeFile(heatmapPath, buffer);
|
|
3973
|
-
logger.printSuccessLog(`Saved pixel difference heatmap: ${path8.basename(heatmapPath)}`);
|
|
3974
|
-
}
|
|
3975
|
-
} catch (heatmapError) {
|
|
3976
|
-
const errorMsg = heatmapError instanceof Error ? heatmapError.message : "Unknown error";
|
|
3977
|
-
logger.printWarnLog(`Failed to generate pixel difference heatmap: ${errorMsg}. Continuing without heatmap.`);
|
|
3978
|
-
}
|
|
3979
|
-
const userReport = this.buildUserReportStructure(
|
|
3980
|
-
validationResult.mae,
|
|
3981
|
-
validationResult.sae,
|
|
3982
|
-
misalignedData.length,
|
|
3983
|
-
screenshots,
|
|
3984
|
-
heatmap,
|
|
3985
|
-
request.serverUrl,
|
|
3986
|
-
request.figmaThumbnailUrl,
|
|
3987
|
-
misalignedData
|
|
3988
|
-
);
|
|
3989
|
-
logger.printSuccessLog("Validation report generated successfully");
|
|
3990
|
-
return {
|
|
3991
|
-
userReport,
|
|
3992
|
-
misalignedCount: misalignedData.length
|
|
3993
|
-
};
|
|
3994
|
-
}
|
|
3995
|
-
/**
|
|
3996
|
-
* Creates a minimal user report for error scenarios.
|
|
3997
|
-
*/
|
|
3998
|
-
createMinimalReport(options) {
|
|
3999
|
-
return {
|
|
4000
|
-
design: {
|
|
4001
|
-
snap: options.figmaThumbnailUrl || "",
|
|
4002
|
-
markedSnap: ""
|
|
4003
|
-
},
|
|
4004
|
-
page: {
|
|
4005
|
-
url: options.serverUrl,
|
|
4006
|
-
snap: "",
|
|
4007
|
-
markedSnap: ""
|
|
4008
|
-
},
|
|
4009
|
-
report: {
|
|
4010
|
-
heatmap: "",
|
|
4011
|
-
detail: {
|
|
4012
|
-
metrics: {
|
|
4013
|
-
mae: options.mae ?? -1,
|
|
4014
|
-
sae: options.sae ?? -1,
|
|
4015
|
-
misalignedCount: 0
|
|
4016
|
-
},
|
|
4017
|
-
components: []
|
|
4018
|
-
}
|
|
4019
|
-
}
|
|
4020
|
-
};
|
|
4021
|
-
}
|
|
4022
|
-
/**
|
|
4023
|
-
* Generate standalone HTML report file from userReport.
|
|
4024
|
-
* Inlines all assets (JS/CSS) for single-file distribution.
|
|
4025
|
-
*/
|
|
4026
|
-
async generateHtml(userReport, outputDir) {
|
|
4027
|
-
const reportDistDir = this.getReportDistDir();
|
|
4028
|
-
const reportIndexHtml = path8.join(reportDistDir, "index.html");
|
|
4029
|
-
logger.printInfoLog(`[ReportTool] Using template: ${reportIndexHtml}`);
|
|
4030
|
-
logger.printInfoLog(`[ReportTool] Output directory: ${outputDir}`);
|
|
4031
|
-
try {
|
|
4032
|
-
try {
|
|
4033
|
-
await fsPromises.access(reportIndexHtml);
|
|
4034
|
-
} catch {
|
|
4035
|
-
const errorMsg = `Template not found at ${reportIndexHtml}`;
|
|
4036
|
-
logger.printErrorLog(`[ReportTool] ${errorMsg}. Skipping report generation.`);
|
|
4037
|
-
return { success: false, error: errorMsg };
|
|
4038
|
-
}
|
|
4039
|
-
await fsPromises.mkdir(outputDir, { recursive: true });
|
|
4040
|
-
let reportData = userReport;
|
|
4041
|
-
if (reportData) {
|
|
4042
|
-
reportData = this.transformReportData(reportData);
|
|
4043
|
-
}
|
|
4044
|
-
logger.printTestLog(`Report data: ${JSON.stringify(reportData)}`);
|
|
4045
|
-
let htmlContent = await fsPromises.readFile(reportIndexHtml, "utf-8");
|
|
4046
|
-
const jsonString = JSON.stringify(reportData).replace(/<\/script>/gi, "<\\/script>").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
|
|
4047
|
-
const scriptTag = `<script>window.__REPORT_DATA__ = ${jsonString};</script>`;
|
|
4048
|
-
htmlContent = htmlContent.replace(/<script>\s*window\.__REPORT_DATA__\s*=\s*null;?\s*<\/script>/, scriptTag);
|
|
4049
|
-
const scriptRegexGlobal = /<script\s+type="module"\s+crossorigin\s+src="\/assets\/([^"]+)"><\/script>/g;
|
|
4050
|
-
const jsMatches = [...htmlContent.matchAll(scriptRegexGlobal)];
|
|
4051
|
-
const jsContentsMap = /* @__PURE__ */ new Map();
|
|
4052
|
-
for (const match of jsMatches) {
|
|
4053
|
-
const fileName = match[1];
|
|
4054
|
-
if (fileName) {
|
|
4055
|
-
const jsFilePath = path8.join(reportDistDir, "assets", fileName);
|
|
4056
|
-
try {
|
|
4057
|
-
let jsContent = await fsPromises.readFile(jsFilePath, "utf-8");
|
|
4058
|
-
jsContent = jsContent.replace(/<\/script>/g, "\\u003c/script>");
|
|
4059
|
-
jsContentsMap.set(fileName, jsContent);
|
|
4060
|
-
} catch {
|
|
4061
|
-
logger.printWarnLog(`[ReportTool] JS asset not found: ${jsFilePath}`);
|
|
4062
|
-
}
|
|
4063
|
-
}
|
|
4064
|
-
}
|
|
4065
|
-
htmlContent = htmlContent.replace(scriptRegexGlobal, (match, fileName) => {
|
|
4066
|
-
const jsContent = jsContentsMap.get(fileName);
|
|
4067
|
-
if (jsContent) {
|
|
4068
|
-
return `<script type="module">${jsContent}</script>`;
|
|
4069
|
-
}
|
|
4070
|
-
return match;
|
|
4071
|
-
});
|
|
4072
|
-
const cssRegex = /<link\s+rel="stylesheet"\s+crossorigin\s+href="\/assets\/(.*?)">/;
|
|
4073
|
-
const cssMatch = htmlContent.match(cssRegex);
|
|
4074
|
-
if (cssMatch) {
|
|
4075
|
-
const cssFileName = cssMatch[1];
|
|
4076
|
-
if (!cssFileName) {
|
|
4077
|
-
logger.printWarnLog("[ReportTool] CSS filename not found in match");
|
|
4078
|
-
} else {
|
|
4079
|
-
const cssFilePath = path8.join(reportDistDir, "assets", cssFileName);
|
|
4080
|
-
try {
|
|
4081
|
-
const cssContent = await fsPromises.readFile(cssFilePath, "utf-8");
|
|
4082
|
-
htmlContent = htmlContent.replace(cssMatch[0], `<style>${cssContent}</style>`);
|
|
4083
|
-
} catch {
|
|
4084
|
-
}
|
|
4085
|
-
}
|
|
4086
|
-
}
|
|
4087
|
-
htmlContent = htmlContent.replace('<link rel="stylesheet" href="/index.css">', "");
|
|
4088
|
-
const absoluteOutputPath = path8.join(outputDir, "index.html");
|
|
4089
|
-
await fsPromises.writeFile(absoluteOutputPath, htmlContent);
|
|
4090
|
-
return { success: true, htmlPath: absoluteOutputPath };
|
|
4091
|
-
} catch (error) {
|
|
4092
|
-
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
4093
|
-
logger.printErrorLog(`[ReportTool] Failed to generate report: ${errorMsg}`);
|
|
4094
|
-
return { success: false, error: errorMsg };
|
|
4095
|
-
}
|
|
4096
|
-
}
|
|
4097
|
-
// ==================== PRIVATE HELPER METHODS ====================
|
|
4098
|
-
/**
|
|
4099
|
-
* Build userReport structure from validation results
|
|
4100
|
-
*/
|
|
4101
|
-
buildUserReportStructure(mae, sae, misalignedCount, screenshots, heatmap, serverUrl, figmaThumbnailUrl, misalignedData) {
|
|
4102
|
-
return {
|
|
4103
|
-
design: {
|
|
4104
|
-
snap: figmaThumbnailUrl,
|
|
4105
|
-
markedSnap: screenshots.targetMarked
|
|
4106
|
-
},
|
|
4107
|
-
page: {
|
|
4108
|
-
url: serverUrl,
|
|
4109
|
-
snap: screenshots.renderSnap,
|
|
4110
|
-
markedSnap: screenshots.renderMarked
|
|
4111
|
-
},
|
|
4112
|
-
report: {
|
|
4113
|
-
heatmap,
|
|
4114
|
-
detail: {
|
|
4115
|
-
metrics: {
|
|
4116
|
-
mae,
|
|
4117
|
-
sae,
|
|
4118
|
-
misalignedCount
|
|
4119
|
-
},
|
|
4120
|
-
components: misalignedData.map((c) => ({
|
|
4121
|
-
componentId: c.componentId,
|
|
4122
|
-
componentPath: c.componentPath,
|
|
4123
|
-
elementId: c.elementId,
|
|
4124
|
-
validationInfo: {
|
|
4125
|
-
x: c.xDelta,
|
|
4126
|
-
y: c.yDelta
|
|
4127
|
-
}
|
|
4128
|
-
}))
|
|
4129
|
-
}
|
|
4130
|
-
}
|
|
4131
|
-
};
|
|
4132
|
-
}
|
|
4133
|
-
/**
|
|
4134
|
-
* Transform report data by grouping components by componentId
|
|
4135
|
-
*/
|
|
4136
|
-
transformReportData(reportData) {
|
|
4137
|
-
const components = reportData?.report?.detail?.components || [];
|
|
4138
|
-
if (!components?.length) return reportData;
|
|
4139
|
-
const groupedComponentsMap = /* @__PURE__ */ new Map();
|
|
4140
|
-
components.forEach((component, index) => {
|
|
4141
|
-
const { componentId, componentPath, elementId, validationInfo } = component;
|
|
4142
|
-
const elementIndex = index + 1;
|
|
4143
|
-
if (!groupedComponentsMap.has(componentId)) {
|
|
4144
|
-
groupedComponentsMap.set(componentId, {
|
|
4145
|
-
componentId,
|
|
4146
|
-
componentPath,
|
|
4147
|
-
elements: []
|
|
4148
|
-
});
|
|
4149
|
-
}
|
|
4150
|
-
groupedComponentsMap.get(componentId)?.elements?.push({
|
|
4151
|
-
elementId: elementId || "",
|
|
4152
|
-
elementIndex,
|
|
4153
|
-
validationInfo: validationInfo || { x: 0, y: 0 }
|
|
4154
|
-
});
|
|
4155
|
-
});
|
|
4156
|
-
const groupedComponents = Array.from(groupedComponentsMap.values());
|
|
4157
|
-
return {
|
|
4158
|
-
...reportData,
|
|
4159
|
-
report: {
|
|
4160
|
-
...reportData.report,
|
|
4161
|
-
detail: {
|
|
4162
|
-
...reportData.report.detail,
|
|
4163
|
-
components: groupedComponents
|
|
4164
|
-
}
|
|
4165
|
-
}
|
|
4166
|
-
};
|
|
4167
|
-
}
|
|
4168
|
-
/**
|
|
4169
|
-
* Determine the root directory of the package by walking up to find package.json
|
|
4170
|
-
*/
|
|
4171
|
-
getPackageRoot() {
|
|
4172
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
4173
|
-
const __dirname = path8.dirname(__filename);
|
|
4174
|
-
let currentDir = __dirname;
|
|
4175
|
-
while (currentDir !== path8.parse(currentDir).root) {
|
|
4176
|
-
if (fs6.existsSync(path8.join(currentDir, "package.json"))) {
|
|
4177
|
-
return currentDir;
|
|
4178
|
-
}
|
|
4179
|
-
currentDir = path8.dirname(currentDir);
|
|
4180
|
-
}
|
|
4181
|
-
return process.cwd();
|
|
4182
|
-
}
|
|
4183
|
-
/**
|
|
4184
|
-
* Get the report dist directory containing built HTML/assets
|
|
4185
|
-
*/
|
|
4186
|
-
getReportDistDir() {
|
|
4187
|
-
const root = this.getPackageRoot();
|
|
4188
|
-
const paths = [
|
|
4189
|
-
path8.join(root, "dist/tools/report-tool/template"),
|
|
4190
|
-
// Production layout (if copied)
|
|
4191
|
-
path8.join(root, "src/tools/report-tool/template/dist")
|
|
4192
|
-
// Development layout
|
|
4193
|
-
];
|
|
4194
|
-
for (const p of paths) {
|
|
4195
|
-
if (fs6.existsSync(path8.join(p, "index.html"))) {
|
|
4196
|
-
return p;
|
|
4197
|
-
}
|
|
4198
|
-
}
|
|
4199
|
-
return path8.join(root, "src/tools/report-tool/template/dist");
|
|
4200
|
-
}
|
|
4201
|
-
};
|
|
4202
|
-
_init6 = __decoratorStart(null);
|
|
4203
|
-
ReportTool = __decorateElement(_init6, 0, "ReportTool", _ReportTool_decorators, ReportTool);
|
|
4204
|
-
__runInitializers(_init6, 1, ReportTool);
|
|
4205
|
-
|
|
4206
|
-
// src/nodes/validation/report/index.ts
|
|
4207
|
-
async function report(options) {
|
|
4208
|
-
const tool = new ReportTool();
|
|
4209
|
-
try {
|
|
4210
|
-
logger.printInfoLog("Generating validation report structure...");
|
|
4211
|
-
const reportResult = await tool.generateReport({
|
|
4212
|
-
validationResult: options.validationResult,
|
|
4213
|
-
figmaThumbnailUrl: options.figmaThumbnailUrl,
|
|
4214
|
-
cachedFigmaThumbnailBase64: options.cachedFigmaThumbnailBase64,
|
|
4215
|
-
designOffset: options.designOffset,
|
|
4216
|
-
outputDir: options.outputDir,
|
|
4217
|
-
serverUrl: options.serverUrl,
|
|
4218
|
-
savedRenderMarkedPath: options.savedRenderMarkedPath,
|
|
4219
|
-
savedTargetMarkedPath: options.savedTargetMarkedPath
|
|
4220
|
-
});
|
|
4221
|
-
logger.printInfoLog("Generating HTML report file...");
|
|
4222
|
-
const htmlResult = await tool.generateHtml(reportResult.userReport, options.outputDir);
|
|
4223
|
-
if (!htmlResult.success) {
|
|
4224
|
-
logger.printWarnLog(`Failed to generate HTML: ${htmlResult.error}`);
|
|
4225
|
-
return {
|
|
4226
|
-
success: false,
|
|
4227
|
-
error: htmlResult.error,
|
|
4228
|
-
userReport: reportResult.userReport
|
|
4229
|
-
};
|
|
4230
|
-
}
|
|
4231
|
-
logger.printSuccessLog(`Report generated successfully: ${htmlResult.htmlPath}`);
|
|
4232
|
-
return {
|
|
4233
|
-
success: true,
|
|
4234
|
-
htmlPath: htmlResult.htmlPath,
|
|
4235
|
-
userReport: reportResult.userReport
|
|
4236
|
-
};
|
|
4237
|
-
} catch (error) {
|
|
4238
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
4239
|
-
logger.printErrorLog(`Error generating report: ${errorMessage}`);
|
|
4240
|
-
const minimalReport = tool.createMinimalReport({
|
|
4241
|
-
serverUrl: options.serverUrl,
|
|
4242
|
-
figmaThumbnailUrl: options.figmaThumbnailUrl,
|
|
4243
|
-
mae: options.validationResult?.mae,
|
|
4244
|
-
sae: options.validationResult?.sae
|
|
4245
|
-
});
|
|
4246
|
-
return {
|
|
4247
|
-
success: false,
|
|
4248
|
-
error: errorMessage,
|
|
4249
|
-
userReport: minimalReport
|
|
4250
|
-
};
|
|
4251
|
-
}
|
|
4252
|
-
}
|
|
4253
|
-
|
|
4254
|
-
// src/tools/launch-tool/index.ts
|
|
4255
|
-
import * as path9 from "path";
|
|
4256
|
-
import { tools as tools7 } from "evoltagent";
|
|
4257
|
-
|
|
4258
|
-
// src/tools/launch-tool/utils/dev-server-manager.ts
|
|
4259
|
-
import * as http from "http";
|
|
4260
|
-
import * as net from "net";
|
|
4261
|
-
import { spawn as spawn2 } from "child_process";
|
|
4262
|
-
init_logger();
|
|
4263
|
-
function normalizeExecutable(executable) {
|
|
4264
|
-
if (process.platform !== "win32") {
|
|
4265
|
-
return executable;
|
|
4266
|
-
}
|
|
4267
|
-
const lower = executable.toLowerCase();
|
|
4268
|
-
if (lower === "npm" || lower === "pnpm" || lower === "yarn" || lower === "npx") {
|
|
4269
|
-
return `${lower}.cmd`;
|
|
4270
|
-
}
|
|
4271
|
-
return executable;
|
|
4272
|
-
}
|
|
4273
|
-
function parseCommandWithPortSupport(commandStr) {
|
|
4274
|
-
const parts = commandStr.trim().split(/\s+/);
|
|
4275
|
-
const executable = parts[0];
|
|
4276
|
-
if (!executable) {
|
|
4277
|
-
throw new Error("Failed to parse command: empty command string");
|
|
4278
|
-
}
|
|
4279
|
-
const args = parts.slice(1);
|
|
4280
|
-
const needsDoubleDash = ["npm", "pnpm", "yarn"].includes(executable);
|
|
4281
|
-
return { executable, args, needsDoubleDash };
|
|
4282
|
-
}
|
|
4283
|
-
async function isPortAvailable(port) {
|
|
4284
|
-
return await new Promise((resolve2) => {
|
|
4285
|
-
const server = net.createServer();
|
|
4286
|
-
server.unref();
|
|
4287
|
-
server.on("error", () => resolve2(false));
|
|
4288
|
-
server.listen(port, () => {
|
|
4289
|
-
server.close(() => resolve2(true));
|
|
4290
|
-
});
|
|
4291
|
-
});
|
|
4292
|
-
}
|
|
4293
|
-
function calculateHashPort(workspacePath, basePort = 5200) {
|
|
4294
|
-
let hash = 0;
|
|
4295
|
-
for (let i = 0; i < workspacePath.length; i++) {
|
|
4296
|
-
hash = (hash << 5) - hash + workspacePath.charCodeAt(i);
|
|
4297
|
-
hash = hash & hash;
|
|
4298
|
-
}
|
|
4299
|
-
const offset = Math.abs(hash) % 1e3;
|
|
4300
|
-
return basePort + offset;
|
|
4301
|
-
}
|
|
4302
|
-
async function getFreeHashPort(workspacePath, _startPort = DEFAULT_PORT) {
|
|
4303
|
-
const preferredPort = calculateHashPort(workspacePath);
|
|
4304
|
-
if (await isPortAvailable(preferredPort)) {
|
|
4305
|
-
logger.printInfoLog(`Using workspace-preferred port: ${preferredPort}`);
|
|
4306
|
-
return preferredPort;
|
|
4307
|
-
}
|
|
4308
|
-
logger.printInfoLog(`Preferred port ${preferredPort} occupied, trying nearby ports...`);
|
|
4309
|
-
for (let offset = 1; offset <= 10; offset++) {
|
|
4310
|
-
const portUp = preferredPort + offset;
|
|
4311
|
-
const portDown = preferredPort - offset;
|
|
4312
|
-
if (await isPortAvailable(portUp)) {
|
|
4313
|
-
logger.printInfoLog(`Using nearby port: ${portUp} (preferred was ${preferredPort})`);
|
|
4314
|
-
return portUp;
|
|
4315
|
-
}
|
|
4316
|
-
if (portDown > 1024 && await isPortAvailable(portDown)) {
|
|
4317
|
-
logger.printInfoLog(`Using nearby port: ${portDown} (preferred was ${preferredPort})`);
|
|
4318
|
-
return portDown;
|
|
4319
|
-
}
|
|
4320
|
-
}
|
|
4321
|
-
throw new Error(`No available ports found in workspace-preferred range (${preferredPort - 10} to ${preferredPort + 10})`);
|
|
4322
|
-
}
|
|
4323
|
-
async function waitForServerReady(url, timeoutMs) {
|
|
4324
|
-
const start = Date.now();
|
|
4325
|
-
const intervalMs = 400;
|
|
4326
|
-
while (Date.now() - start < timeoutMs) {
|
|
4327
|
-
const ok = await new Promise((resolve2) => {
|
|
4328
|
-
const req = http.get(url, (res) => {
|
|
4329
|
-
res.resume();
|
|
4330
|
-
resolve2(res.statusCode !== void 0 && res.statusCode >= 200 && res.statusCode < 500);
|
|
4331
|
-
});
|
|
4332
|
-
req.on("error", () => resolve2(false));
|
|
4333
|
-
req.setTimeout(500, () => req.destroy());
|
|
4334
|
-
});
|
|
4335
|
-
if (ok) return true;
|
|
4336
|
-
await new Promise((r) => setTimeout(r, intervalMs));
|
|
4337
|
-
}
|
|
4338
|
-
return false;
|
|
4339
|
-
}
|
|
4340
|
-
var DevServerManager = class {
|
|
4341
|
-
constructor(params) {
|
|
4342
|
-
this.params = params;
|
|
4343
|
-
}
|
|
4344
|
-
handle = null;
|
|
4345
|
-
get current() {
|
|
4346
|
-
return this.handle;
|
|
4347
|
-
}
|
|
4348
|
-
async start(options = { timeoutMs: 6e4 }) {
|
|
4349
|
-
if (this.handle) {
|
|
4350
|
-
return this.handle;
|
|
4351
|
-
}
|
|
4352
|
-
const port = await getFreeHashPort(this.params.appPath);
|
|
4353
|
-
const url = buildDevServerUrl(port);
|
|
4354
|
-
const { executable, args, needsDoubleDash } = parseCommandWithPortSupport(this.params.runCommand);
|
|
4355
|
-
const normalizedExecutable = normalizeExecutable(executable);
|
|
4356
|
-
const commandArgs = needsDoubleDash ? [...args, "--", "--port", String(port)] : [...args, "--port", String(port)];
|
|
4357
|
-
logger.printInfoLog(`Starting dev server: ${normalizedExecutable} ${commandArgs.join(" ")}`);
|
|
4358
|
-
const child = spawn2(normalizedExecutable, commandArgs, {
|
|
4359
|
-
cwd: this.params.appPath,
|
|
4360
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
4361
|
-
env: {
|
|
4362
|
-
...process.env,
|
|
4363
|
-
PORT: String(port),
|
|
4364
|
-
FORCE_COLOR: "1"
|
|
4365
|
-
}
|
|
4366
|
-
});
|
|
4367
|
-
const maxBytes = 1e6;
|
|
4368
|
-
let out = "";
|
|
4369
|
-
const push = (chunk) => {
|
|
4370
|
-
out += chunk.toString("utf-8");
|
|
4371
|
-
if (out.length > maxBytes) {
|
|
4372
|
-
out = out.slice(out.length - maxBytes);
|
|
4373
|
-
}
|
|
4374
|
-
};
|
|
4375
|
-
child.stdout?.on("data", push);
|
|
4376
|
-
child.stderr?.on("data", push);
|
|
4377
|
-
process.once("exit", () => {
|
|
4378
|
-
try {
|
|
4379
|
-
child.kill("SIGTERM");
|
|
4380
|
-
} catch {
|
|
4381
|
-
}
|
|
4382
|
-
});
|
|
4383
|
-
const ready = await waitForServerReady(url, options.timeoutMs);
|
|
4384
|
-
if (!ready) {
|
|
4385
|
-
const tail = out.trim();
|
|
4386
|
-
try {
|
|
4387
|
-
child.kill("SIGTERM");
|
|
4388
|
-
} catch {
|
|
4389
|
-
}
|
|
4390
|
-
throw new Error(`Dev server did not become ready at ${url} within ${options.timeoutMs}ms.
|
|
4391
|
-
${tail}`);
|
|
4392
|
-
}
|
|
4393
|
-
this.handle = {
|
|
4394
|
-
child,
|
|
4395
|
-
port,
|
|
4396
|
-
url,
|
|
4397
|
-
outputTail: () => out.trim()
|
|
4398
|
-
};
|
|
4399
|
-
logger.printSuccessLog(`Dev server ready at ${url}`);
|
|
4400
|
-
return this.handle;
|
|
4401
|
-
}
|
|
4402
|
-
async stop() {
|
|
4403
|
-
if (!this.handle) return;
|
|
4404
|
-
const child = this.handle.child;
|
|
4405
|
-
this.handle = null;
|
|
4406
|
-
await new Promise((resolve2) => {
|
|
4407
|
-
try {
|
|
4408
|
-
child.once("close", () => resolve2());
|
|
4409
|
-
child.kill("SIGTERM");
|
|
4410
|
-
} catch {
|
|
4411
|
-
resolve2();
|
|
4412
|
-
}
|
|
4413
|
-
setTimeout(() => resolve2(), 1500);
|
|
4414
|
-
});
|
|
4415
|
-
}
|
|
4416
|
-
async restart(options = { timeoutMs: 6e4 }) {
|
|
4417
|
-
await this.stop();
|
|
4418
|
-
return await this.start(options);
|
|
4419
|
-
}
|
|
4420
|
-
};
|
|
4421
|
-
|
|
4422
|
-
// src/tools/launch-tool/index.ts
|
|
4423
|
-
function makeServerKey(appPath) {
|
|
4424
|
-
const safeApp = appPath.split(path9.sep).join("_");
|
|
4425
|
-
return `launch:${safeApp}`;
|
|
4426
|
-
}
|
|
4427
|
-
var _LaunchTool_decorators, _init7;
|
|
4428
|
-
_LaunchTool_decorators = [tools7({
|
|
4429
|
-
startDevServer: {
|
|
4430
|
-
description: "Start a dev server in-process and wait for it to become reachable.",
|
|
4431
|
-
params: [
|
|
4432
|
-
{ name: "appPath", type: "string", description: "Absolute path to the app directory" },
|
|
4433
|
-
{ name: "runCommand", type: "string", description: 'Run command (e.g. "pnpm dev")' },
|
|
4434
|
-
{ name: "timeoutMs", type: "number", description: "Timeout in milliseconds", optional: true }
|
|
4435
|
-
],
|
|
4436
|
-
returns: { type: "string", description: "JSON string containing StartDevServerResult with url/port/pid/serverKey." }
|
|
4437
|
-
},
|
|
4438
|
-
stopDevServer: {
|
|
4439
|
-
description: "Stop a previously started dev server by serverKey.",
|
|
4440
|
-
params: [{ name: "serverKey", type: "string", description: "Server key returned by startDevServer()" }],
|
|
4441
|
-
returns: { type: "string", description: "JSON string containing StopDevServerResult" }
|
|
4442
|
-
}
|
|
4443
|
-
})];
|
|
4444
|
-
var _LaunchTool = class _LaunchTool {
|
|
4445
|
-
static serverManagers = /* @__PURE__ */ new Map();
|
|
4446
|
-
/** Mutex per appPath so only one startDevServer runs at a time for a given app (avoids duplicate port logs). */
|
|
4447
|
-
static inFlightStarts = /* @__PURE__ */ new Map();
|
|
4448
|
-
async startDevServer(appPath, runCommand, timeoutMs = 6e4) {
|
|
4449
|
-
const serverKey = makeServerKey(appPath);
|
|
4450
|
-
const prev = _LaunchTool.inFlightStarts.get(appPath);
|
|
4451
|
-
let resolveThis;
|
|
4452
|
-
const thisRun = new Promise((r) => {
|
|
4453
|
-
resolveThis = r;
|
|
4454
|
-
});
|
|
4455
|
-
_LaunchTool.inFlightStarts.set(appPath, thisRun);
|
|
4456
|
-
await prev;
|
|
4457
|
-
try {
|
|
4458
|
-
const entry = _LaunchTool.serverManagers.get(serverKey);
|
|
4459
|
-
if (entry) {
|
|
4460
|
-
const handle2 = await entry.manager.restart({ timeoutMs });
|
|
4461
|
-
const result2 = {
|
|
4462
|
-
success: true,
|
|
4463
|
-
serverKey,
|
|
4464
|
-
url: handle2.url,
|
|
4465
|
-
port: handle2.port,
|
|
4466
|
-
pid: handle2.child.pid ?? void 0,
|
|
4467
|
-
outputTail: handle2.outputTail()
|
|
4468
|
-
};
|
|
4469
|
-
return JSON.stringify(result2, null, 2);
|
|
4470
|
-
}
|
|
4471
|
-
const manager = new DevServerManager({ appPath, runCommand: runCommand.trim() });
|
|
4472
|
-
const handle = await manager.start({ timeoutMs });
|
|
4473
|
-
_LaunchTool.serverManagers.set(serverKey, { manager, appPath });
|
|
4474
|
-
const result = {
|
|
4475
|
-
success: true,
|
|
4476
|
-
serverKey,
|
|
4477
|
-
url: handle.url,
|
|
4478
|
-
port: handle.port,
|
|
4479
|
-
pid: handle.child.pid ?? void 0,
|
|
4480
|
-
outputTail: handle.outputTail()
|
|
4481
|
-
};
|
|
4482
|
-
return JSON.stringify(result, null, 2);
|
|
4483
|
-
} catch (error) {
|
|
4484
|
-
const result = {
|
|
4485
|
-
success: false,
|
|
4486
|
-
error: error instanceof Error ? error.message : String(error)
|
|
4487
|
-
};
|
|
4488
|
-
return JSON.stringify(result, null, 2);
|
|
4489
|
-
} finally {
|
|
4490
|
-
resolveThis();
|
|
4491
|
-
_LaunchTool.inFlightStarts.delete(appPath);
|
|
4492
|
-
}
|
|
4493
|
-
}
|
|
4494
|
-
async stopDevServer(serverKey) {
|
|
4495
|
-
const entry = _LaunchTool.serverManagers.get(serverKey);
|
|
4496
|
-
if (!entry) {
|
|
4497
|
-
const result = { success: false, serverKey, error: `Unknown serverKey: ${serverKey}` };
|
|
4498
|
-
return JSON.stringify(result, null, 2);
|
|
4499
|
-
}
|
|
4500
|
-
try {
|
|
4501
|
-
await entry.manager.stop();
|
|
4502
|
-
_LaunchTool.serverManagers.delete(serverKey);
|
|
4503
|
-
const result = { success: true, serverKey };
|
|
4504
|
-
return JSON.stringify(result, null, 2);
|
|
4505
|
-
} catch (error) {
|
|
4506
|
-
const result = {
|
|
4507
|
-
success: false,
|
|
4508
|
-
serverKey,
|
|
4509
|
-
error: error instanceof Error ? error.message : String(error)
|
|
4510
|
-
};
|
|
4511
|
-
return JSON.stringify(result, null, 2);
|
|
4512
|
-
}
|
|
4513
|
-
}
|
|
4514
|
-
};
|
|
4515
|
-
_init7 = __decoratorStart(null);
|
|
4516
|
-
_LaunchTool = __decorateElement(_init7, 0, "LaunchTool", _LaunchTool_decorators, _LaunchTool);
|
|
4517
|
-
__runInitializers(_init7, 1, _LaunchTool);
|
|
4518
|
-
var LaunchTool = _LaunchTool;
|
|
4519
|
-
|
|
4520
|
-
// src/nodes/validation/utils/extraction/extract-protocol-context.ts
|
|
4521
|
-
function extractValidationContext(protocol) {
|
|
4522
|
-
const offset = extractOffset(protocol);
|
|
4523
|
-
const elements = /* @__PURE__ */ new Map();
|
|
4524
|
-
const components = /* @__PURE__ */ new Map();
|
|
4525
|
-
const elementToComponent = /* @__PURE__ */ new Map();
|
|
4526
|
-
const figmaPositions = {};
|
|
4527
|
-
traverseProtocol(protocol, (node) => {
|
|
4528
|
-
const componentInfo = {
|
|
4529
|
-
id: node.id,
|
|
4530
|
-
name: node.data.name,
|
|
4531
|
-
path: node.data.componentPath ?? node.data.path ?? node.data.kebabName ?? node.id
|
|
4532
|
-
};
|
|
4533
|
-
components.set(node.id, componentInfo);
|
|
4534
|
-
const hasChildren = Array.isArray(node.children) && node.children.length > 0;
|
|
4535
|
-
const itemType = hasChildren ? "frame" : "component";
|
|
4536
|
-
if (node.data.elements && Array.isArray(node.data.elements)) {
|
|
4537
|
-
extractElementsRecursive(
|
|
4538
|
-
node.data.elements,
|
|
4539
|
-
componentInfo,
|
|
4540
|
-
itemType,
|
|
4541
|
-
elements,
|
|
4542
|
-
elementToComponent,
|
|
4543
|
-
figmaPositions,
|
|
4544
|
-
offset
|
|
4545
|
-
);
|
|
4546
|
-
}
|
|
4547
|
-
});
|
|
4548
|
-
return { offset, elements, components, elementToComponent, figmaPositions };
|
|
4549
|
-
}
|
|
4550
|
-
function extractOffset(protocol) {
|
|
4551
|
-
if (protocol.data.layout?.boundingBox) {
|
|
4552
|
-
return {
|
|
4553
|
-
x: protocol.data.layout.boundingBox.left,
|
|
4554
|
-
y: protocol.data.layout.boundingBox.top
|
|
4555
|
-
};
|
|
4556
|
-
}
|
|
4557
|
-
const firstElement = protocol.data.elements?.[0];
|
|
4558
|
-
if (firstElement?.absoluteBoundingBox) {
|
|
4559
|
-
return {
|
|
4560
|
-
x: firstElement.absoluteBoundingBox.x,
|
|
4561
|
-
y: firstElement.absoluteBoundingBox.y
|
|
4562
|
-
};
|
|
4563
|
-
}
|
|
4564
|
-
return { x: 0, y: 0 };
|
|
4565
|
-
}
|
|
4566
|
-
function traverseProtocol(node, callback) {
|
|
4567
|
-
callback(node);
|
|
4568
|
-
if (node.children && Array.isArray(node.children)) {
|
|
4569
|
-
for (const child of node.children) {
|
|
4570
|
-
traverseProtocol(child, callback);
|
|
4571
|
-
}
|
|
4572
|
-
}
|
|
4573
|
-
}
|
|
4574
|
-
function extractElementsRecursive(elements, componentInfo, itemType, elementsMap, elementToComponent, figmaPositions, offset) {
|
|
4575
|
-
for (const element of elements) {
|
|
4576
|
-
if (!element || !element.id) continue;
|
|
4577
|
-
const bbox = element.absoluteBoundingBox;
|
|
4578
|
-
const position = bbox ? {
|
|
4579
|
-
x: bbox.x - offset.x,
|
|
4580
|
-
y: bbox.y - offset.y,
|
|
4581
|
-
w: bbox.width,
|
|
4582
|
-
h: bbox.height
|
|
4583
|
-
} : { x: 0, y: 0, w: 0, h: 0 };
|
|
4584
|
-
figmaPositions[element.id] = position;
|
|
4585
|
-
elementsMap.set(element.id, {
|
|
4586
|
-
id: element.id,
|
|
4587
|
-
name: element.name,
|
|
4588
|
-
type: element.type,
|
|
4589
|
-
position,
|
|
4590
|
-
layoutMode: element.layoutMode,
|
|
4591
|
-
primaryAxisAlignItems: element.primaryAxisAlignItems,
|
|
4592
|
-
counterAxisAlignItems: element.counterAxisAlignItems,
|
|
4593
|
-
itemSpacing: element.itemSpacing,
|
|
4594
|
-
paddingTop: element.paddingTop,
|
|
4595
|
-
paddingRight: element.paddingRight,
|
|
4596
|
-
paddingBottom: element.paddingBottom,
|
|
4597
|
-
paddingLeft: element.paddingLeft,
|
|
4598
|
-
constraints: element.constraints,
|
|
4599
|
-
parentComponentId: componentInfo.id,
|
|
4600
|
-
parentComponentName: componentInfo.name,
|
|
4601
|
-
parentComponentPath: componentInfo.path,
|
|
4602
|
-
parentItemType: itemType
|
|
4603
|
-
});
|
|
4604
|
-
elementToComponent.set(element.id, componentInfo);
|
|
4605
|
-
if (element.children && Array.isArray(element.children)) {
|
|
4606
|
-
extractElementsRecursive(element.children, componentInfo, itemType, elementsMap, elementToComponent, figmaPositions, offset);
|
|
4607
|
-
}
|
|
4608
|
-
if (element.frames && Array.isArray(element.frames)) {
|
|
4609
|
-
extractElementsRecursive(element.frames, componentInfo, itemType, elementsMap, elementToComponent, figmaPositions, offset);
|
|
4610
|
-
}
|
|
4611
|
-
}
|
|
4612
|
-
}
|
|
4613
|
-
function extractComponentPaths(context, workspace) {
|
|
4614
|
-
const paths = {};
|
|
4615
|
-
for (const [id, info] of context.components) {
|
|
4616
|
-
if (info.path) {
|
|
4617
|
-
paths[id] = workspaceManager.resolveAppSrc(workspace, workspaceManager.resolveComponentPath(info.path));
|
|
4618
|
-
}
|
|
4619
|
-
}
|
|
4620
|
-
return paths;
|
|
4621
|
-
}
|
|
4622
|
-
function toElementMetadataRegistry(context) {
|
|
4623
|
-
const elements = /* @__PURE__ */ new Map();
|
|
4624
|
-
for (const [id, element] of context.elements) {
|
|
4625
|
-
elements.set(id, {
|
|
4626
|
-
id: element.id,
|
|
4627
|
-
name: element.name,
|
|
4628
|
-
parentComponentId: element.parentComponentId,
|
|
4629
|
-
parentComponentName: element.parentComponentName,
|
|
4630
|
-
parentComponentPath: element.parentComponentPath,
|
|
4631
|
-
parentItemType: element.parentItemType
|
|
4632
|
-
});
|
|
4633
|
-
}
|
|
4634
|
-
return { elements, components: context.components };
|
|
4635
|
-
}
|
|
4636
|
-
|
|
4637
|
-
// src/nodes/validation/core/validate-position.ts
|
|
4638
|
-
init_logger();
|
|
4639
|
-
|
|
4640
|
-
// src/tools/position-tool/index.ts
|
|
4641
|
-
init_logger();
|
|
4642
|
-
import { tools as tools8 } from "evoltagent";
|
|
4643
|
-
|
|
4644
|
-
// src/tools/position-tool/utils/capture-position.ts
|
|
4645
|
-
init_logger();
|
|
4646
|
-
|
|
4647
|
-
// src/tools/position-tool/utils/position-metrics.ts
|
|
4648
|
-
function calculatePositionMetrics(positions) {
|
|
4649
|
-
const positionValues = Object.values(positions);
|
|
4650
|
-
const comparableItems = positionValues.filter((p) => p.metrics);
|
|
4651
|
-
const comparableCount = comparableItems.length;
|
|
4652
|
-
if (comparableCount === 0) {
|
|
4653
|
-
return {
|
|
4654
|
-
mae: 0,
|
|
4655
|
-
mse: 0,
|
|
4656
|
-
rmse: 0,
|
|
4657
|
-
accurateItems: 0,
|
|
4658
|
-
misalignedItems: 0,
|
|
4659
|
-
comparableItems: 0,
|
|
4660
|
-
accuracyRate: 0,
|
|
4661
|
-
averageDistance: 0,
|
|
4662
|
-
maxDistance: 0
|
|
4663
|
-
};
|
|
4664
|
-
}
|
|
4665
|
-
const distances = comparableItems.map((p) => p.metrics.absoluteDistance);
|
|
4666
|
-
const mae = distances.reduce((sum, d) => sum + d, 0) / comparableCount;
|
|
4667
|
-
const mse = distances.reduce((sum, d) => sum + d * d, 0) / comparableCount;
|
|
4668
|
-
const rmse = Math.sqrt(mse);
|
|
4669
|
-
const maxDistance = Math.max(...distances);
|
|
4670
|
-
const accurateItems = comparableItems.filter((p) => p.metrics.status === "accurate").length;
|
|
4671
|
-
const misalignedItems = comparableItems.filter((p) => p.metrics.status === "misaligned").length;
|
|
4672
|
-
const accuracyRate = comparableCount > 0 ? accurateItems / comparableCount * 100 : 0;
|
|
4673
|
-
return {
|
|
4674
|
-
mae: Math.round(mae * 100) / 100,
|
|
4675
|
-
mse: Math.round(mse * 100) / 100,
|
|
4676
|
-
rmse: Math.round(rmse * 100) / 100,
|
|
4677
|
-
accurateItems,
|
|
4678
|
-
misalignedItems,
|
|
4679
|
-
comparableItems: comparableCount,
|
|
4680
|
-
accuracyRate: Math.round(accuracyRate * 100) / 100,
|
|
4681
|
-
averageDistance: Math.round(mae * 100) / 100,
|
|
4682
|
-
maxDistance: Math.round(maxDistance * 100) / 100
|
|
4683
|
-
};
|
|
4684
|
-
}
|
|
4685
|
-
|
|
4686
|
-
// src/tools/position-tool/utils/capture-position.ts
|
|
4687
|
-
async function captureBrowserPositions(input) {
|
|
4688
|
-
const url = input.url;
|
|
4689
|
-
const timeout = input.timeout ?? DEFAULT_TIMEOUT;
|
|
4690
|
-
const positionThreshold = input.positionThreshold ?? POSITION_THRESHOLD;
|
|
4691
|
-
const errors = [];
|
|
4692
|
-
let browser = null;
|
|
4693
|
-
let page = null;
|
|
4694
|
-
try {
|
|
4695
|
-
const figmaPositions = input.validationContext.figmaPositions;
|
|
4696
|
-
const designOffset = input.validationContext.offset;
|
|
4697
|
-
const allElements = Array.from(input.elementRegistry.elements.values()).map((e) => ({
|
|
4698
|
-
id: e.id,
|
|
4699
|
-
name: e.name,
|
|
4700
|
-
parentItemId: e.parentComponentId,
|
|
4701
|
-
parentItemName: e.parentComponentName,
|
|
4702
|
-
parentItemType: e.parentItemType
|
|
4703
|
-
}));
|
|
4704
|
-
let viewport = input.viewport ?? DEFAULT_VIEWPORT;
|
|
4705
|
-
if (input.figmaThumbnailUrl && !input.viewport) {
|
|
4706
|
-
const { fetchThumbnailDimensions: fetchThumbnailDimensions2 } = await Promise.resolve().then(() => (init_fetch_thumbnail_dimensions(), fetch_thumbnail_dimensions_exports));
|
|
4707
|
-
const thumbnailDimensions = await fetchThumbnailDimensions2(input.figmaThumbnailUrl);
|
|
4708
|
-
if (thumbnailDimensions) {
|
|
4709
|
-
viewport = { width: thumbnailDimensions.width, height: thumbnailDimensions.height };
|
|
4710
|
-
logger.printInfoLog(`Using viewport dimensions from Figma thumbnail: ${viewport.width}\xD7${viewport.height}px`);
|
|
4711
|
-
} else {
|
|
4712
|
-
logger.printWarnLog(`Using default viewport dimensions: ${viewport.width}\xD7${viewport.height}px`);
|
|
4713
|
-
}
|
|
4714
|
-
}
|
|
4715
|
-
try {
|
|
4716
|
-
browser = await launchChromiumWithAutoInstall({ headless: true });
|
|
4717
|
-
} catch (launchError) {
|
|
4718
|
-
throw new Error(`Failed to launch Playwright Chromium browser for position validation: ${launchError.message}`);
|
|
4719
|
-
}
|
|
4720
|
-
const context = await browser.newContext({ viewport });
|
|
4721
|
-
page = await context.newPage();
|
|
4722
|
-
try {
|
|
4723
|
-
await page.goto(url, { waitUntil: "domcontentloaded", timeout });
|
|
4724
|
-
} catch (error) {
|
|
4725
|
-
throw new Error(`Failed to load ${url}. Make sure the development server is running. Error: ${error.message}`);
|
|
4726
|
-
}
|
|
4727
|
-
if (input.waitForSelector) {
|
|
4728
|
-
try {
|
|
4729
|
-
await page.waitForSelector(input.waitForSelector, { timeout: SELECTOR_WAIT_TIMEOUT });
|
|
4730
|
-
} catch {
|
|
4731
|
-
errors.push(`Warning: waitForSelector "${input.waitForSelector}" timed out`);
|
|
4732
|
-
}
|
|
4733
|
-
}
|
|
4734
|
-
let screenshot;
|
|
4735
|
-
if (input.returnScreenshot) {
|
|
4736
|
-
try {
|
|
4737
|
-
const buffer = await page.screenshot({ fullPage: true, type: "png" });
|
|
4738
|
-
screenshot = `data:image/png;base64,${buffer.toString("base64")}`;
|
|
4739
|
-
} catch (error) {
|
|
4740
|
-
errors.push(`Warning: Failed to capture screenshot: ${error.message}`);
|
|
4741
|
-
}
|
|
4742
|
-
}
|
|
4743
|
-
const positions = {};
|
|
4744
|
-
let capturedCount = 0;
|
|
4745
|
-
for (const element of allElements) {
|
|
4746
|
-
try {
|
|
4747
|
-
const escapedId = element.id.replace(/:/g, "\\:");
|
|
4748
|
-
const selector = `[id="${escapedId}"]`;
|
|
4749
|
-
const result = await page.evaluate((sel) => {
|
|
4750
|
-
const el = document.querySelector(sel);
|
|
4751
|
-
if (!el) return null;
|
|
4752
|
-
const rect = el.getBoundingClientRect();
|
|
4753
|
-
const style = window.getComputedStyle(el);
|
|
4754
|
-
if (rect.width === 0 && rect.height === 0) {
|
|
4755
|
-
return null;
|
|
4756
|
-
}
|
|
4757
|
-
return {
|
|
4758
|
-
boundingBox: {
|
|
4759
|
-
x: rect.x,
|
|
4760
|
-
y: rect.y,
|
|
4761
|
-
width: rect.width,
|
|
4762
|
-
height: rect.height,
|
|
4763
|
-
top: rect.top,
|
|
4764
|
-
left: rect.left,
|
|
4765
|
-
right: rect.right,
|
|
4766
|
-
bottom: rect.bottom
|
|
4767
|
-
},
|
|
4768
|
-
computedStyle: {
|
|
4769
|
-
position: style.position,
|
|
4770
|
-
display: style.display,
|
|
4771
|
-
top: style.top,
|
|
4772
|
-
left: style.left,
|
|
4773
|
-
right: style.right,
|
|
4774
|
-
bottom: style.bottom,
|
|
4775
|
-
transform: style.transform,
|
|
4776
|
-
tagName: el.tagName.toLowerCase()
|
|
4777
|
-
}
|
|
4778
|
-
};
|
|
4779
|
-
}, selector);
|
|
4780
|
-
if (!result) {
|
|
4781
|
-
errors.push(`Element ${element.id} not found in DOM or has no dimensions`);
|
|
4782
|
-
continue;
|
|
4783
|
-
}
|
|
4784
|
-
const figmaPos = figmaPositions[element.id];
|
|
4785
|
-
let metrics;
|
|
4786
|
-
if (figmaPos) {
|
|
4787
|
-
const xDelta = result.boundingBox.x - figmaPos.x;
|
|
4788
|
-
const yDelta = result.boundingBox.y - figmaPos.y;
|
|
4789
|
-
const xOffset = Math.abs(xDelta);
|
|
4790
|
-
const yOffset = Math.abs(yDelta);
|
|
4791
|
-
const absoluteDistance = Math.sqrt(xOffset ** 2 + yOffset ** 2);
|
|
4792
|
-
metrics = {
|
|
4793
|
-
xOffset: Math.round(xOffset * 100) / 100,
|
|
4794
|
-
yOffset: Math.round(yOffset * 100) / 100,
|
|
4795
|
-
xDelta: Math.round(xDelta * 100) / 100,
|
|
4796
|
-
yDelta: Math.round(yDelta * 100) / 100,
|
|
4797
|
-
absoluteDistance: Math.round(absoluteDistance * 100) / 100,
|
|
4798
|
-
status: absoluteDistance <= positionThreshold ? "accurate" : "misaligned"
|
|
4799
|
-
};
|
|
4800
|
-
}
|
|
4801
|
-
positions[element.id] = {
|
|
4802
|
-
elementId: element.id,
|
|
4803
|
-
elementName: element.name,
|
|
4804
|
-
parentItemId: element.parentItemId,
|
|
4805
|
-
parentItemName: element.parentItemName,
|
|
4806
|
-
parentItemType: element.parentItemType,
|
|
4807
|
-
boundingBox: {
|
|
4808
|
-
x: Math.round(result.boundingBox.x * 100) / 100,
|
|
4809
|
-
y: Math.round(result.boundingBox.y * 100) / 100,
|
|
4810
|
-
width: Math.round(result.boundingBox.width * 100) / 100,
|
|
4811
|
-
height: Math.round(result.boundingBox.height * 100) / 100,
|
|
4812
|
-
top: Math.round(result.boundingBox.top * 100) / 100,
|
|
4813
|
-
left: Math.round(result.boundingBox.left * 100) / 100,
|
|
4814
|
-
right: Math.round(result.boundingBox.right * 100) / 100,
|
|
4815
|
-
bottom: Math.round(result.boundingBox.bottom * 100) / 100
|
|
4816
|
-
},
|
|
4817
|
-
computedStyle: result.computedStyle,
|
|
4818
|
-
figmaPosition: figmaPos ? { x: figmaPos.x, y: figmaPos.y, width: figmaPos.w, height: figmaPos.h } : void 0,
|
|
4819
|
-
metrics
|
|
4820
|
-
};
|
|
4821
|
-
capturedCount++;
|
|
4822
|
-
} catch (error) {
|
|
4823
|
-
const errorMsg = `Failed to capture position for ${element.id}` + (element.name ? ` (${element.name})` : "") + ` in ${element.parentItemName || "unknown component"}: ${error.message}`;
|
|
4824
|
-
errors.push(errorMsg);
|
|
4825
|
-
}
|
|
4826
|
-
}
|
|
4827
|
-
const maeMetrics = calculatePositionMetrics(positions);
|
|
4828
|
-
return {
|
|
4829
|
-
metadata: {
|
|
4830
|
-
capturedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4831
|
-
totalItems: allElements.length,
|
|
4832
|
-
capturedItems: capturedCount,
|
|
4833
|
-
...maeMetrics,
|
|
4834
|
-
url,
|
|
4835
|
-
viewport,
|
|
4836
|
-
designOffset
|
|
4837
|
-
},
|
|
4838
|
-
positions,
|
|
4839
|
-
errors,
|
|
4840
|
-
screenshot
|
|
4841
|
-
};
|
|
4842
|
-
} finally {
|
|
4843
|
-
if (page) {
|
|
4844
|
-
await page.close().catch(() => {
|
|
4845
|
-
});
|
|
4846
|
-
}
|
|
4847
|
-
if (browser) {
|
|
4848
|
-
await browser.close().catch(() => {
|
|
4849
|
-
});
|
|
4850
|
-
}
|
|
4851
|
-
}
|
|
4852
|
-
}
|
|
4853
|
-
|
|
4854
|
-
// src/tools/position-tool/utils/aggregate-elements.ts
|
|
4855
|
-
function toRect(rectLike) {
|
|
4856
|
-
return { x: rectLike.x, y: rectLike.y, width: rectLike.width, height: rectLike.height };
|
|
4857
|
-
}
|
|
4858
|
-
function calculateAggregateBoundingBox(rectangles) {
|
|
4859
|
-
if (rectangles.length === 0) {
|
|
4860
|
-
throw new Error("Cannot calculate bounding box from empty array");
|
|
4861
|
-
}
|
|
4862
|
-
const minX = Math.min(...rectangles.map((r) => r.x));
|
|
4863
|
-
const minY = Math.min(...rectangles.map((r) => r.y));
|
|
4864
|
-
const maxRight = Math.max(...rectangles.map((r) => r.x + r.width));
|
|
4865
|
-
const maxBottom = Math.max(...rectangles.map((r) => r.y + r.height));
|
|
4866
|
-
return {
|
|
4867
|
-
minX,
|
|
4868
|
-
minY,
|
|
4869
|
-
maxRight,
|
|
4870
|
-
maxBottom,
|
|
4871
|
-
width: maxRight - minX,
|
|
4872
|
-
height: maxBottom - minY
|
|
4873
|
-
};
|
|
4874
|
-
}
|
|
4875
|
-
function calculatePositionError(current, target) {
|
|
4876
|
-
const errorX = Math.abs(target.minX - current.minX);
|
|
4877
|
-
const errorY = Math.abs(target.minY - current.minY);
|
|
4878
|
-
const distance = Math.sqrt(errorX ** 2 + errorY ** 2);
|
|
4879
|
-
return { errorX, errorY, distance };
|
|
4880
|
-
}
|
|
4881
|
-
function calculateComponentMetrics(compData, positionThreshold) {
|
|
4882
|
-
if (compData.positions.length === 0 || compData.targets.length === 0) {
|
|
4883
|
-
return null;
|
|
4884
|
-
}
|
|
4885
|
-
const currentBounds = calculateAggregateBoundingBox(compData.positions);
|
|
4886
|
-
const targetBounds = calculateAggregateBoundingBox(compData.targets);
|
|
4887
|
-
const error = calculatePositionError(currentBounds, targetBounds);
|
|
4888
|
-
if (error.distance <= positionThreshold) {
|
|
4889
|
-
return null;
|
|
4890
|
-
}
|
|
4891
|
-
return { currentBounds, targetBounds, error };
|
|
4892
|
-
}
|
|
4893
|
-
|
|
4894
|
-
// src/tools/position-tool/index.ts
|
|
4895
|
-
var _PositionTool_decorators, _init8;
|
|
4896
|
-
_PositionTool_decorators = [tools8({
|
|
4897
|
-
capturePosition: {
|
|
4898
|
-
description: "Capture viewport-relative positions in the browser and compare to Figma absoluteBoundingBox, returning per-element deltas and metrics.",
|
|
4899
|
-
params: [{ name: "input", type: "object", description: "BrowserPositionInput (figmaJSON, structure, url, etc.)" }],
|
|
4900
|
-
returns: { type: "object", description: "PositionValidationOutput containing metadata, positions, and errors." }
|
|
4901
|
-
},
|
|
4902
|
-
aggregateElements: {
|
|
4903
|
-
description: "Aggregate element-level rectangles into component-level bounds and return misaligned components based on aggregate distance threshold.",
|
|
4904
|
-
params: [
|
|
4905
|
-
{ name: "positions", type: "object", description: "Positions keyed by elementId (from capturePosition)." },
|
|
4906
|
-
{
|
|
4907
|
-
name: "elementToComponentMap",
|
|
4908
|
-
type: "object",
|
|
4909
|
-
description: "Mapping from elementId -> {id,name,path} for parent component (built from structure tree)."
|
|
4910
|
-
},
|
|
4911
|
-
{ name: "positionThreshold", type: "number", description: "Pixel threshold for aggregate distance." }
|
|
4912
|
-
],
|
|
4913
|
-
returns: { type: "object", description: "AggregateElementsResult: misalignedComponents + skippedElements." }
|
|
4914
|
-
},
|
|
4915
|
-
computeMetrics: {
|
|
4916
|
-
description: "Compute MAE/MSE/RMSE/accuracy metrics from element positions, and SAE from component misalignments.",
|
|
4917
|
-
params: [
|
|
4918
|
-
{ name: "positions", type: "object", description: "Positions keyed by elementId (from capturePosition)." },
|
|
4919
|
-
{
|
|
4920
|
-
name: "misalignedComponents",
|
|
4921
|
-
type: "object",
|
|
4922
|
-
description: "Optional component misalignments (from aggregateElements) used to compute SAE.",
|
|
4923
|
-
optional: true
|
|
4924
|
-
}
|
|
4925
|
-
],
|
|
4926
|
-
returns: { type: "object", description: "PositionToolMetrics (ValidationMetrics + sae)." }
|
|
4927
|
-
}
|
|
4928
|
-
})];
|
|
4929
|
-
var PositionTool = class {
|
|
4930
|
-
async capturePosition(input) {
|
|
4931
|
-
return await captureBrowserPositions(input);
|
|
4932
|
-
}
|
|
4933
|
-
/**
|
|
4934
|
-
* Deterministic in-process aggregation.
|
|
4935
|
-
* NOTE: Map-based inputs are not JSON-friendly for future agent calls; we can add a JSON form later.
|
|
4936
|
-
*/
|
|
4937
|
-
aggregateElements(positions, elementToComponentMap, positionThreshold) {
|
|
4938
|
-
const componentMap = /* @__PURE__ */ new Map();
|
|
4939
|
-
const skippedElements = [];
|
|
4940
|
-
for (const [elementId, position] of Object.entries(positions)) {
|
|
4941
|
-
if (!position.figmaPosition) {
|
|
4942
|
-
skippedElements.push({
|
|
4943
|
-
elementId,
|
|
4944
|
-
reason: "no_figma_position",
|
|
4945
|
-
details: "Element exists in render but has no corresponding Figma position data"
|
|
4946
|
-
});
|
|
4947
|
-
continue;
|
|
4948
|
-
}
|
|
4949
|
-
const component = elementToComponentMap.get(elementId);
|
|
4950
|
-
if (!component) {
|
|
4951
|
-
skippedElements.push({
|
|
4952
|
-
elementId,
|
|
4953
|
-
reason: "missing_component_mapping",
|
|
4954
|
-
details: "Element has no component mapping in structure tree"
|
|
4955
|
-
});
|
|
4956
|
-
continue;
|
|
4957
|
-
}
|
|
4958
|
-
if (!componentMap.has(component.id)) {
|
|
4959
|
-
componentMap.set(component.id, {
|
|
4960
|
-
componentId: component.id,
|
|
4961
|
-
componentName: component.name,
|
|
4962
|
-
componentPath: component.path,
|
|
4963
|
-
elementIds: [],
|
|
4964
|
-
positions: [],
|
|
4965
|
-
targets: [],
|
|
4966
|
-
errors: []
|
|
4967
|
-
});
|
|
4968
|
-
}
|
|
4969
|
-
const compData = componentMap.get(component.id);
|
|
4970
|
-
compData.elementIds.push(elementId);
|
|
4971
|
-
compData.positions.push(toRect(position.boundingBox));
|
|
4972
|
-
compData.targets.push(toRect(position.figmaPosition));
|
|
4973
|
-
compData.errors.push({
|
|
4974
|
-
x: Math.abs(position.metrics?.xDelta ?? 0),
|
|
4975
|
-
y: Math.abs(position.metrics?.yDelta ?? 0)
|
|
4976
|
-
});
|
|
4977
|
-
}
|
|
4978
|
-
this.logSkippedElementsSummary(skippedElements);
|
|
4979
|
-
const misalignedComponents = [];
|
|
4980
|
-
for (const compData of componentMap.values()) {
|
|
4981
|
-
if (compData.positions.length === 0 || compData.targets.length === 0) {
|
|
4982
|
-
for (const elemId of compData.elementIds) {
|
|
4983
|
-
skippedElements.push({
|
|
4984
|
-
elementId: elemId,
|
|
4985
|
-
reason: "incomplete_data",
|
|
4986
|
-
details: `Component ${compData.componentName} has ${compData.positions.length} positions, ${compData.targets.length} targets`
|
|
4987
|
-
});
|
|
4988
|
-
}
|
|
4989
|
-
continue;
|
|
4990
|
-
}
|
|
4991
|
-
const metrics = calculateComponentMetrics(compData, positionThreshold);
|
|
4992
|
-
if (!metrics) {
|
|
4993
|
-
continue;
|
|
4994
|
-
}
|
|
4995
|
-
const { currentBounds, targetBounds, error } = metrics;
|
|
4996
|
-
misalignedComponents.push({
|
|
4997
|
-
name: compData.componentName,
|
|
4998
|
-
componentId: compData.componentId,
|
|
4999
|
-
elementIds: compData.elementIds,
|
|
5000
|
-
path: compData.componentPath,
|
|
5001
|
-
validationReport: {
|
|
5002
|
-
currentPosition: [currentBounds.minX, currentBounds.minY],
|
|
5003
|
-
targetPosition: [targetBounds.minX, targetBounds.minY],
|
|
5004
|
-
absoluteError: [error.errorX, error.errorY]
|
|
5005
|
-
},
|
|
5006
|
-
currentX: currentBounds.minX,
|
|
5007
|
-
currentY: currentBounds.minY,
|
|
5008
|
-
targetX: targetBounds.minX,
|
|
5009
|
-
targetY: targetBounds.minY,
|
|
5010
|
-
currentWidth: currentBounds.width,
|
|
5011
|
-
currentHeight: currentBounds.height,
|
|
5012
|
-
targetWidth: targetBounds.width,
|
|
5013
|
-
targetHeight: targetBounds.height,
|
|
5014
|
-
distance: error.distance
|
|
5015
|
-
});
|
|
5016
|
-
}
|
|
5017
|
-
return { misalignedComponents, skippedElements };
|
|
5018
|
-
}
|
|
5019
|
-
computeMetrics(positions, misalignedComponents) {
|
|
5020
|
-
const metrics = calculatePositionMetrics(positions);
|
|
5021
|
-
const sae = misalignedComponents?.reduce((sum, comp) => {
|
|
5022
|
-
const [errorX, errorY] = comp.validationReport.absoluteError;
|
|
5023
|
-
return sum + errorX + errorY;
|
|
5024
|
-
}, 0) ?? 0;
|
|
5025
|
-
return { ...metrics, sae: Math.round(sae * 100) / 100 };
|
|
5026
|
-
}
|
|
5027
|
-
logSkippedElementsSummary(skippedElements) {
|
|
5028
|
-
if (skippedElements.length === 0) {
|
|
5029
|
-
return;
|
|
5030
|
-
}
|
|
5031
|
-
const byReason = {
|
|
5032
|
-
missing_component_mapping: 0,
|
|
5033
|
-
incomplete_data: 0,
|
|
5034
|
-
no_figma_position: 0
|
|
5035
|
-
};
|
|
5036
|
-
for (const elem of skippedElements) {
|
|
5037
|
-
byReason[elem.reason] += 1;
|
|
5038
|
-
}
|
|
5039
|
-
const summary = Object.entries(byReason).filter(([, count]) => count > 0).map(([reason, count]) => `${count} ${reason.replace(/_/g, " ")}`).join(", ");
|
|
5040
|
-
const firstFew = skippedElements.slice(0, 3).map((e) => e.elementId).join(", ");
|
|
5041
|
-
logger.printWarnLog(`Skipped ${skippedElements.length} element(s): ${summary}. First few: ${firstFew}`);
|
|
5042
|
-
}
|
|
5043
|
-
};
|
|
5044
|
-
_init8 = __decoratorStart(null);
|
|
5045
|
-
PositionTool = __decorateElement(_init8, 0, "PositionTool", _PositionTool_decorators, PositionTool);
|
|
5046
|
-
__runInitializers(_init8, 1, PositionTool);
|
|
5047
|
-
|
|
5048
|
-
// src/nodes/validation/core/validate-position.ts
|
|
5049
|
-
async function validatePositions(config) {
|
|
5050
|
-
const { serverUrl, figmaThumbnailUrl, protocol, positionThreshold, validationContext, elementRegistry, resolvedComponentPaths } = config;
|
|
5051
|
-
const positionTool = new PositionTool();
|
|
5052
|
-
const captureResult = await positionTool.capturePosition({
|
|
5053
|
-
protocol,
|
|
5054
|
-
validationContext,
|
|
5055
|
-
url: serverUrl,
|
|
5056
|
-
figmaThumbnailUrl,
|
|
5057
|
-
positionThreshold,
|
|
5058
|
-
returnScreenshot: true,
|
|
5059
|
-
elementRegistry
|
|
5060
|
-
});
|
|
5061
|
-
const resolvedElementToComponent = /* @__PURE__ */ new Map();
|
|
5062
|
-
for (const [elementId, componentInfo] of validationContext.elementToComponent) {
|
|
5063
|
-
const resolvedPath = resolvedComponentPaths[componentInfo.id] || componentInfo.path;
|
|
5064
|
-
resolvedElementToComponent.set(elementId, {
|
|
5065
|
-
id: componentInfo.id,
|
|
5066
|
-
name: componentInfo.name,
|
|
5067
|
-
path: resolvedPath
|
|
5068
|
-
});
|
|
5069
|
-
}
|
|
5070
|
-
const aggregated = positionTool.aggregateElements(captureResult.positions, resolvedElementToComponent, positionThreshold);
|
|
5071
|
-
const misalignedComponents = aggregated.misalignedComponents;
|
|
5072
|
-
const skippedElements = aggregated.skippedElements;
|
|
5073
|
-
const sae = misalignedComponents.reduce((sum, comp) => {
|
|
5074
|
-
const [errorX, errorY] = comp.validationReport.absoluteError;
|
|
5075
|
-
return sum + errorX + errorY;
|
|
5076
|
-
}, 0);
|
|
5077
|
-
logger.printInfoLog(`Found ${misalignedComponents.length} misaligned components`);
|
|
5078
|
-
return {
|
|
5079
|
-
mae: captureResult.metadata.mae,
|
|
5080
|
-
sae,
|
|
5081
|
-
misalignedComponents,
|
|
5082
|
-
skippedElements,
|
|
5083
|
-
viewport: captureResult.metadata.viewport,
|
|
5084
|
-
screenshots: captureResult.screenshot ? {
|
|
5085
|
-
renderSnap: captureResult.screenshot
|
|
5086
|
-
} : void 0
|
|
5087
|
-
};
|
|
5088
|
-
}
|
|
5089
|
-
|
|
5090
|
-
// src/nodes/validation/core/validation-loop.ts
|
|
5091
|
-
function filterComponentsToFix(misaligned, positionThreshold) {
|
|
5092
|
-
return misaligned.filter((comp) => {
|
|
5093
|
-
const [errorX, errorY] = comp.validationReport.absoluteError;
|
|
5094
|
-
return errorX > positionThreshold || errorY > positionThreshold;
|
|
5095
|
-
});
|
|
5096
|
-
}
|
|
5097
|
-
function recordComponentPosition(comp, iteration, componentHistory) {
|
|
5098
|
-
const history = componentHistory[comp.componentId] ?? [];
|
|
5099
|
-
history.push({
|
|
5100
|
-
iteration,
|
|
5101
|
-
position: comp.validationReport.currentPosition,
|
|
5102
|
-
error: comp.validationReport.absoluteError,
|
|
5103
|
-
fixApplied: null
|
|
5104
|
-
});
|
|
5105
|
-
componentHistory[comp.componentId] = history;
|
|
5106
|
-
}
|
|
5107
|
-
async function refineComponent(comp, context) {
|
|
5108
|
-
const { workspace, structureTree, componentPaths, componentHistory, validationContext, previousScreenshotPath } = context;
|
|
5109
|
-
try {
|
|
5110
|
-
const elementIds = Array.from(validationContext.elements.values()).filter((e) => e.parentComponentId === comp.componentId).map((e) => e.id);
|
|
5111
|
-
const figmaMetadata = extractLayoutFromContext(validationContext, elementIds);
|
|
5112
|
-
logger.printLog(` Analyzing ${comp.name}...`);
|
|
5113
|
-
const judger = createJudgerAgent({
|
|
5114
|
-
workspaceDir: workspace.app,
|
|
5115
|
-
structureTree,
|
|
5116
|
-
componentPaths,
|
|
5117
|
-
history: componentHistory
|
|
5118
|
-
});
|
|
5119
|
-
const judgerInstruction = formatJudgerInstruction(comp, figmaMetadata, componentPaths);
|
|
5120
|
-
const judgerImages = previousScreenshotPath ? [previousScreenshotPath] : void 0;
|
|
5121
|
-
const diagnosis = await judger.run(judgerInstruction, judgerImages);
|
|
5122
|
-
logger.printLog(` Error type: ${diagnosis.errorType}`);
|
|
5123
|
-
logger.printLog(` Fix instructions: ${diagnosis.refineInstructions?.length || 0}`);
|
|
5124
|
-
if (!diagnosis.refineInstructions || diagnosis.refineInstructions.length === 0) {
|
|
5125
|
-
const history2 = componentHistory[comp.componentId];
|
|
5126
|
-
return {
|
|
5127
|
-
componentId: comp.componentId,
|
|
5128
|
-
componentPath: comp.path,
|
|
5129
|
-
elementIds: comp.elementIds,
|
|
5130
|
-
validationReport: comp.validationReport,
|
|
5131
|
-
diagnosis,
|
|
5132
|
-
refinerResult: {
|
|
5133
|
-
success: false,
|
|
5134
|
-
summary: ["Skipped - no instructions from judger"],
|
|
5135
|
-
editsApplied: 0
|
|
5136
|
-
},
|
|
5137
|
-
positionHistory: history2 ? [...history2] : void 0
|
|
5138
|
-
};
|
|
5139
|
-
}
|
|
5140
|
-
logger.printLog(` Applying fixes to ${comp.name}...`);
|
|
5141
|
-
const refiner = createRefinerAgent(workspace.app);
|
|
5142
|
-
const refinerInstruction = formatRefinerInstruction(comp, diagnosis, componentPaths);
|
|
5143
|
-
const refinerResult = await refiner.run(refinerInstruction);
|
|
5144
|
-
if (refinerResult.success) {
|
|
5145
|
-
logger.printSuccessLog(`${comp.name}: ${refinerResult.editsApplied} edits applied`);
|
|
5146
|
-
} else {
|
|
5147
|
-
logger.printWarnLog(`${comp.name}: ${refinerResult.editsApplied} edits applied`);
|
|
5148
|
-
}
|
|
5149
|
-
const history = componentHistory[comp.componentId];
|
|
5150
|
-
const last = history?.at(-1);
|
|
5151
|
-
if (last) {
|
|
5152
|
-
last.fixApplied = refinerResult.summary;
|
|
5153
|
-
}
|
|
5154
|
-
return {
|
|
5155
|
-
componentId: comp.componentId,
|
|
5156
|
-
componentPath: comp.path,
|
|
5157
|
-
elementIds: comp.elementIds,
|
|
5158
|
-
validationReport: comp.validationReport,
|
|
5159
|
-
diagnosis,
|
|
5160
|
-
refinerResult,
|
|
5161
|
-
positionHistory: history ? [...history] : void 0
|
|
5162
|
-
};
|
|
5163
|
-
} catch (error) {
|
|
5164
|
-
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
5165
|
-
logger.printInfoLog(`${comp.name}: ${errorMsg}`);
|
|
5166
|
-
const history = componentHistory[comp.componentId];
|
|
5167
|
-
return {
|
|
5168
|
-
componentId: comp.componentId,
|
|
5169
|
-
componentPath: comp.path,
|
|
5170
|
-
elementIds: comp.elementIds,
|
|
5171
|
-
validationReport: comp.validationReport,
|
|
5172
|
-
diagnosis: void 0,
|
|
5173
|
-
refinerResult: void 0,
|
|
5174
|
-
positionHistory: history ? [...history] : void 0
|
|
5175
|
-
};
|
|
5176
|
-
}
|
|
5177
|
-
}
|
|
5178
|
-
function saveProcessedJson(outputDir, processedOutput) {
|
|
5179
|
-
const processedJsonPath = path10.join(outputDir, "processed.json");
|
|
5180
|
-
fs7.writeFileSync(processedJsonPath, JSON.stringify(processedOutput, null, 2));
|
|
5181
|
-
logger.printInfoLog("Saved processed.json");
|
|
5182
|
-
}
|
|
5183
|
-
async function performCommit(appPath, iteration, stage) {
|
|
5184
|
-
const commitResult = await commit({ appPath, iteration });
|
|
5185
|
-
if (!commitResult.success) {
|
|
5186
|
-
logger.printWarnLog(`Git commit (${stage}) failed: ${commitResult.message}`);
|
|
5187
|
-
} else if (iteration === void 0) {
|
|
5188
|
-
logger.printSuccessLog(`Initial project committed successfully, starting validation loop...`);
|
|
5189
|
-
}
|
|
5190
|
-
}
|
|
5191
|
-
function saveIterationAndProcessedJson(iterations, iteration, currentMae, currentSae, misalignedCount, components, screenshotPath, skippedElements, outputDir, targetMae) {
|
|
5192
|
-
iterations.push({
|
|
5193
|
-
iteration,
|
|
5194
|
-
metrics: { mae: currentMae, sae: currentSae, misalignedCount },
|
|
5195
|
-
components,
|
|
5196
|
-
screenshotPath,
|
|
5197
|
-
skippedElements: skippedElements && skippedElements.length > 0 ? skippedElements : void 0
|
|
5198
|
-
});
|
|
5199
|
-
const processedOutput = {
|
|
5200
|
-
iterations,
|
|
5201
|
-
finalResult: {
|
|
5202
|
-
success: currentMae <= targetMae,
|
|
5203
|
-
finalMae: currentMae,
|
|
5204
|
-
finalSae: currentSae,
|
|
5205
|
-
totalIterations: iterations.length,
|
|
5206
|
-
misalignedCount
|
|
5207
|
-
}
|
|
5208
|
-
};
|
|
5209
|
-
saveProcessedJson(outputDir, processedOutput);
|
|
5210
|
-
}
|
|
5211
|
-
async function validationLoop(params) {
|
|
5212
|
-
const { protocol, figmaThumbnailUrl, outputDir, workspace } = params;
|
|
5213
|
-
if (!protocol || !figmaThumbnailUrl || !outputDir || !workspace) {
|
|
5214
|
-
throw new Error("Something wrong in validation loop, missing required parameters...");
|
|
5215
|
-
}
|
|
5216
|
-
const config = {
|
|
5217
|
-
...DEFAULT_VALIDATION_LOOP_CONFIG,
|
|
5218
|
-
...params.config
|
|
5219
|
-
};
|
|
5220
|
-
const mode = config.mode ?? "full";
|
|
5221
|
-
const maxIterations = mode === "reportOnly" ? 1 : config.maxIterations;
|
|
5222
|
-
const visualizationTool = new VisualizationTool();
|
|
5223
|
-
let currentServerUrl;
|
|
5224
|
-
let serverKey;
|
|
5225
|
-
try {
|
|
5226
|
-
const validationContext = extractValidationContext(protocol);
|
|
5227
|
-
const designOffset = [validationContext.offset.x, validationContext.offset.y];
|
|
5228
|
-
if (Math.abs(designOffset[0]) >= 1 || Math.abs(designOffset[1]) >= 1) {
|
|
5229
|
-
logger.printInfoLog(`Design offset: (${designOffset[0].toFixed(0)}, ${designOffset[1].toFixed(0)} px)`);
|
|
5230
|
-
}
|
|
5231
|
-
const resolvedComponentPaths = extractComponentPaths(validationContext, workspace);
|
|
5232
|
-
const elementRegistry = toElementMetadataRegistry(validationContext);
|
|
5233
|
-
logger.printInfoLog("Downloading Figma thumbnail (will be cached for all iterations)...");
|
|
5234
|
-
const cachedFigmaThumbnailBase64 = await downloadImage(figmaThumbnailUrl, void 0, void 0, true);
|
|
5235
|
-
logger.printSuccessLog("Figma thumbnail cached successfully");
|
|
5236
|
-
const iterations = [];
|
|
5237
|
-
const componentHistory = {};
|
|
5238
|
-
let previousScreenshotPath;
|
|
5239
|
-
let currentMae = -1;
|
|
5240
|
-
let currentSae = 0;
|
|
5241
|
-
let lastMisalignedCount = 0;
|
|
5242
|
-
let lastValidationResult;
|
|
5243
|
-
let lastRenderMarkedPath;
|
|
5244
|
-
let lastTargetMarkedPath;
|
|
5245
|
-
for (let iteration = 1; iteration <= maxIterations; iteration++) {
|
|
5246
|
-
if (mode === "reportOnly") {
|
|
5247
|
-
logger.printLog(`
|
|
5248
|
-
${"=".repeat(60)}`);
|
|
5249
|
-
logger.printLog(`Running validation (report-only mode)`);
|
|
5250
|
-
logger.printLog(`${"=".repeat(60)}`);
|
|
5251
|
-
} else {
|
|
5252
|
-
logger.printLog(`
|
|
5253
|
-
${"=".repeat(60)}`);
|
|
5254
|
-
logger.printLog(`Iteration ${iteration}/${maxIterations}`);
|
|
5255
|
-
logger.printLog(`${"=".repeat(60)}`);
|
|
5256
|
-
}
|
|
5257
|
-
if (iteration === 1) {
|
|
5258
|
-
logger.printInfoLog("Initial launch: installing dependencies, building, fixing errors...");
|
|
5259
|
-
const launchResult2 = await launch(workspace.app, {
|
|
5260
|
-
skipDependencyInstall: false
|
|
5261
|
-
});
|
|
5262
|
-
if (!launchResult2.success) {
|
|
5263
|
-
throw new Error(`Initial launch failed: ${launchResult2.error}`);
|
|
5264
|
-
}
|
|
5265
|
-
currentServerUrl = launchResult2.url;
|
|
5266
|
-
serverKey = launchResult2.serverKey;
|
|
5267
|
-
logger.printSuccessLog(`Dev server ready at ${currentServerUrl}`);
|
|
5268
|
-
if (mode === "reportOnly") {
|
|
5269
|
-
await performCommit(workspace.app, void 0, "initial state");
|
|
5270
|
-
}
|
|
5271
|
-
}
|
|
5272
|
-
const validationResult = await validatePositions({
|
|
5273
|
-
serverUrl: currentServerUrl,
|
|
5274
|
-
figmaThumbnailUrl,
|
|
5275
|
-
protocol,
|
|
5276
|
-
iteration,
|
|
5277
|
-
positionThreshold: config.positionThreshold,
|
|
5278
|
-
designOffset,
|
|
5279
|
-
outputDir,
|
|
5280
|
-
validationContext,
|
|
5281
|
-
elementRegistry,
|
|
5282
|
-
cachedFigmaThumbnailBase64,
|
|
5283
|
-
resolvedComponentPaths
|
|
5284
|
-
});
|
|
5285
|
-
lastValidationResult = validationResult;
|
|
5286
|
-
currentMae = validationResult.mae;
|
|
5287
|
-
currentSae = validationResult.sae;
|
|
5288
|
-
const misaligned = validationResult.misalignedComponents;
|
|
5289
|
-
lastMisalignedCount = misaligned.length;
|
|
5290
|
-
logger.printLog(`MAE: ${currentMae.toFixed(2)}px (target: <=${config.targetMae}px)`);
|
|
5291
|
-
logger.printLog(`SAE: ${currentSae.toFixed(2)}px`);
|
|
5292
|
-
logger.printLog(`Misaligned: ${misaligned.length}`);
|
|
5293
|
-
const screenshotResult = await visualizationTool.generateIterationScreenshot(
|
|
5294
|
-
misaligned,
|
|
5295
|
-
currentServerUrl,
|
|
5296
|
-
figmaThumbnailUrl,
|
|
5297
|
-
validationResult.viewport,
|
|
5298
|
-
{ x: designOffset[0], y: designOffset[1] },
|
|
5299
|
-
path10.join(outputDir, "comparison_screenshots", `iteration_${iteration}.webp`),
|
|
5300
|
-
cachedFigmaThumbnailBase64
|
|
5301
|
-
);
|
|
5302
|
-
if (screenshotResult.renderMarked && screenshotResult.targetMarked) {
|
|
5303
|
-
const comparisonDir = path10.join(outputDir, "comparison_screenshots");
|
|
5304
|
-
lastRenderMarkedPath = path10.join(comparisonDir, `iteration_${iteration}_render_marked.webp`);
|
|
5305
|
-
lastTargetMarkedPath = path10.join(comparisonDir, `iteration_${iteration}_target_marked.webp`);
|
|
5306
|
-
const sharp5 = (await import("sharp")).default;
|
|
5307
|
-
await sharp5(Buffer.from(screenshotResult.renderMarked.split(",")[1], "base64")).toFile(lastRenderMarkedPath);
|
|
5308
|
-
await sharp5(Buffer.from(screenshotResult.targetMarked.split(",")[1], "base64")).toFile(lastTargetMarkedPath);
|
|
5309
|
-
}
|
|
5310
|
-
const comparisonScreenshotPath = screenshotResult.combinedPath;
|
|
5311
|
-
previousScreenshotPath = comparisonScreenshotPath;
|
|
5312
|
-
const misalignedToFix = filterComponentsToFix(misaligned, config.positionThreshold);
|
|
5313
|
-
if (mode === "reportOnly") {
|
|
5314
|
-
} else {
|
|
5315
|
-
logger.printInfoLog(
|
|
5316
|
-
`Skipping ${misaligned.length - misalignedToFix.length} components with error <= ${config.positionThreshold}px`
|
|
5317
|
-
);
|
|
5318
|
-
}
|
|
5319
|
-
for (const comp of misalignedToFix) {
|
|
5320
|
-
recordComponentPosition(comp, iteration, componentHistory);
|
|
5321
|
-
}
|
|
5322
|
-
const componentLogs = [];
|
|
5323
|
-
if (mode === "reportOnly") {
|
|
5324
|
-
logger.printInfoLog("Report-only mode: skipping component refinement, saving validation report...");
|
|
5325
|
-
saveIterationAndProcessedJson(
|
|
5326
|
-
iterations,
|
|
5327
|
-
iteration,
|
|
5328
|
-
currentMae,
|
|
5329
|
-
currentSae,
|
|
5330
|
-
misaligned.length,
|
|
5331
|
-
componentLogs,
|
|
5332
|
-
comparisonScreenshotPath,
|
|
5333
|
-
validationResult.skippedElements,
|
|
5334
|
-
outputDir,
|
|
5335
|
-
config.targetMae
|
|
5336
|
-
);
|
|
5337
|
-
break;
|
|
5338
|
-
}
|
|
5339
|
-
if (currentMae <= config.targetMae) {
|
|
5340
|
-
logger.printSuccessLog("Validation passed!");
|
|
5341
|
-
saveIterationAndProcessedJson(
|
|
5342
|
-
iterations,
|
|
5343
|
-
iteration,
|
|
5344
|
-
currentMae,
|
|
5345
|
-
currentSae,
|
|
5346
|
-
misaligned.length,
|
|
5347
|
-
componentLogs,
|
|
5348
|
-
comparisonScreenshotPath,
|
|
5349
|
-
validationResult.skippedElements,
|
|
5350
|
-
outputDir,
|
|
5351
|
-
config.targetMae
|
|
5352
|
-
);
|
|
5353
|
-
if (serverKey) {
|
|
5354
|
-
await performCommit(workspace.app, iteration, `iteration ${iteration}`);
|
|
5355
|
-
}
|
|
5356
|
-
break;
|
|
5357
|
-
}
|
|
5358
|
-
logger.printInfoLog(`Refining ${misalignedToFix.length} components...`);
|
|
5359
|
-
const refinementContext = {
|
|
5360
|
-
workspace,
|
|
5361
|
-
structureTree: protocol,
|
|
5362
|
-
componentPaths: resolvedComponentPaths,
|
|
5363
|
-
componentHistory,
|
|
5364
|
-
validationContext,
|
|
5365
|
-
previousScreenshotPath
|
|
5366
|
-
};
|
|
5367
|
-
for (const comp of misalignedToFix) {
|
|
5368
|
-
const log = await refineComponent(comp, refinementContext);
|
|
5369
|
-
componentLogs.push(log);
|
|
5370
|
-
}
|
|
5371
|
-
logger.printInfoLog("Re-launching dev server after refiner changes...");
|
|
5372
|
-
const launchTool = new LaunchTool();
|
|
5373
|
-
await launchTool.stopDevServer(serverKey).catch(() => {
|
|
5374
|
-
logger.printWarnLog("Failed to stop previous server");
|
|
5375
|
-
});
|
|
5376
|
-
const launchResult = await launch(workspace.app, {
|
|
5377
|
-
skipDependencyInstall: true
|
|
5378
|
-
});
|
|
5379
|
-
if (!launchResult.success) {
|
|
5380
|
-
throw new Error(`Launch failed after refiner at iteration ${iteration}: ${launchResult.error}`);
|
|
5381
|
-
}
|
|
5382
|
-
currentServerUrl = launchResult.url;
|
|
5383
|
-
serverKey = launchResult.serverKey;
|
|
5384
|
-
logger.printSuccessLog(`Dev server restarted at ${currentServerUrl}`);
|
|
5385
|
-
if (mode === "full") {
|
|
5386
|
-
await performCommit(workspace.app, iteration, `iteration ${iteration}`);
|
|
5387
|
-
}
|
|
5388
|
-
saveIterationAndProcessedJson(
|
|
5389
|
-
iterations,
|
|
5390
|
-
iteration,
|
|
5391
|
-
currentMae,
|
|
5392
|
-
currentSae,
|
|
5393
|
-
misaligned.length,
|
|
5394
|
-
componentLogs,
|
|
5395
|
-
comparisonScreenshotPath,
|
|
5396
|
-
validationResult.skippedElements,
|
|
5397
|
-
outputDir,
|
|
5398
|
-
config.targetMae
|
|
5399
|
-
);
|
|
5400
|
-
logger.printInfoLog(`Iteration ${iteration} complete
|
|
5401
|
-
`);
|
|
5402
|
-
}
|
|
5403
|
-
const validationPassed = currentMae <= config.targetMae;
|
|
5404
|
-
if (!validationPassed) {
|
|
5405
|
-
if (mode === "reportOnly") {
|
|
5406
|
-
logger.printWarnLog(
|
|
5407
|
-
`Validation did not satisfy MAE threshold (${config.targetMae}px) in report-only mode. Final MAE: ${currentMae.toFixed(2)}px`
|
|
5408
|
-
);
|
|
5409
|
-
} else {
|
|
5410
|
-
logger.printWarnLog(
|
|
5411
|
-
`Max iterations (${maxIterations}) reached without satisfying MAE threshold (${config.targetMae}px). Final MAE: ${currentMae.toFixed(2)}px`
|
|
5412
|
-
);
|
|
5413
|
-
}
|
|
5414
|
-
}
|
|
5415
|
-
const finalOutput = {
|
|
5416
|
-
iterations,
|
|
5417
|
-
finalResult: {
|
|
5418
|
-
success: validationPassed,
|
|
5419
|
-
finalMae: currentMae,
|
|
5420
|
-
finalSae: currentSae,
|
|
5421
|
-
totalIterations: iterations.length,
|
|
5422
|
-
misalignedCount: lastMisalignedCount
|
|
5423
|
-
}
|
|
5424
|
-
};
|
|
5425
|
-
if (!currentServerUrl || !serverKey) {
|
|
5426
|
-
throw new Error("Server was not launched properly during validation");
|
|
5427
|
-
}
|
|
5428
|
-
try {
|
|
5429
|
-
if (!lastValidationResult) {
|
|
5430
|
-
throw new Error("No validation results available for report generation");
|
|
5431
|
-
}
|
|
5432
|
-
if (!lastRenderMarkedPath || !lastTargetMarkedPath) {
|
|
5433
|
-
throw new Error("No saved screenshots available for report generation");
|
|
5434
|
-
}
|
|
5435
|
-
const reportResult = await report({
|
|
5436
|
-
validationResult: lastValidationResult,
|
|
5437
|
-
figmaThumbnailUrl,
|
|
5438
|
-
cachedFigmaThumbnailBase64,
|
|
5439
|
-
designOffset: { x: designOffset[0], y: designOffset[1] },
|
|
5440
|
-
outputDir,
|
|
5441
|
-
serverUrl: currentServerUrl,
|
|
5442
|
-
savedRenderMarkedPath: lastRenderMarkedPath,
|
|
5443
|
-
savedTargetMarkedPath: lastTargetMarkedPath
|
|
5444
|
-
});
|
|
5445
|
-
finalOutput.finalResult.misalignedCount = lastValidationResult.misalignedComponents.length;
|
|
5446
|
-
saveProcessedJson(outputDir, finalOutput);
|
|
5447
|
-
return {
|
|
5448
|
-
reportGenerated: reportResult.success,
|
|
5449
|
-
validationPassed,
|
|
5450
|
-
finalMae: currentMae,
|
|
5451
|
-
finalSae: currentSae,
|
|
5452
|
-
totalIterations: iterations.length,
|
|
5453
|
-
processedOutput: finalOutput,
|
|
5454
|
-
userReport: reportResult.userReport
|
|
5455
|
-
};
|
|
5456
|
-
} catch (screenshotError) {
|
|
5457
|
-
const errorMsg = screenshotError instanceof Error ? screenshotError.message : String(screenshotError);
|
|
5458
|
-
logger.printWarnLog(`Failed to generate final report: ${errorMsg}. Returning minimal report.`);
|
|
5459
|
-
saveProcessedJson(outputDir, finalOutput);
|
|
5460
|
-
return {
|
|
5461
|
-
reportGenerated: false,
|
|
5462
|
-
validationPassed,
|
|
5463
|
-
finalMae: currentMae,
|
|
5464
|
-
finalSae: currentSae,
|
|
5465
|
-
totalIterations: iterations.length,
|
|
5466
|
-
processedOutput: finalOutput,
|
|
5467
|
-
userReport: {
|
|
5468
|
-
design: { snap: figmaThumbnailUrl, markedSnap: "" },
|
|
5469
|
-
page: { url: currentServerUrl, snap: "", markedSnap: "" },
|
|
5470
|
-
report: {
|
|
5471
|
-
heatmap: "",
|
|
5472
|
-
detail: {
|
|
5473
|
-
metrics: { mae: currentMae, sae: currentSae, misalignedCount: lastMisalignedCount },
|
|
5474
|
-
components: []
|
|
5475
|
-
}
|
|
5476
|
-
}
|
|
5477
|
-
}
|
|
5478
|
-
};
|
|
5479
|
-
}
|
|
5480
|
-
} finally {
|
|
5481
|
-
if (serverKey) {
|
|
5482
|
-
logger.printInfoLog("Cleaning up dev server...");
|
|
5483
|
-
const launchTool = new LaunchTool();
|
|
5484
|
-
await launchTool.stopDevServer(serverKey).catch((err) => {
|
|
5485
|
-
logger.printWarnLog(`Failed to stop dev server: ${err instanceof Error ? err.message : String(err)}`);
|
|
5486
|
-
});
|
|
5487
|
-
}
|
|
5488
|
-
}
|
|
5489
|
-
}
|
|
5490
|
-
|
|
5491
|
-
// src/nodes/validation/index.ts
|
|
5492
|
-
var runValidation = async (state) => {
|
|
5493
|
-
if (!state.protocol) {
|
|
5494
|
-
throw new Error("No protocol found for validation (state.protocol is missing).");
|
|
5495
|
-
}
|
|
5496
|
-
if (!state.figmaInfo?.thumbnail) {
|
|
5497
|
-
throw new Error("Missing Figma thumbnail URL (state.figmaInfo.thumbnail is missing).");
|
|
5498
|
-
}
|
|
5499
|
-
const mode = state.config?.validationMode ?? "full";
|
|
5500
|
-
if (mode === "codeOnly") {
|
|
5501
|
-
logger.printInfoLog("Code-only mode: skipping validation, committing generated code...");
|
|
5502
|
-
const commitResult = await commit({ appPath: state.workspace.app });
|
|
5503
|
-
if (!commitResult.success) {
|
|
5504
|
-
logger.printWarnLog(`Git commit failed: ${commitResult.message}`);
|
|
5505
|
-
} else {
|
|
5506
|
-
logger.printSuccessLog("Git commit completed successfully!");
|
|
5507
|
-
}
|
|
5508
|
-
return;
|
|
5509
|
-
}
|
|
5510
|
-
const outputDir = path11.join(state.workspace.process, "validation");
|
|
5511
|
-
if (!fs8.existsSync(outputDir)) {
|
|
5512
|
-
fs8.mkdirSync(outputDir, { recursive: true });
|
|
5513
|
-
}
|
|
5514
|
-
logger.printInfoLog(`Starting validation loop (mode: ${mode})...`);
|
|
5515
|
-
const result = await validationLoop({
|
|
5516
|
-
protocol: state.protocol,
|
|
5517
|
-
figmaThumbnailUrl: state.figmaInfo.thumbnail,
|
|
5518
|
-
outputDir,
|
|
5519
|
-
workspace: state.workspace,
|
|
5520
|
-
config: { mode }
|
|
5521
|
-
});
|
|
5522
|
-
if (result.error) {
|
|
5523
|
-
throw new Error(result.error);
|
|
5524
|
-
}
|
|
5525
|
-
const reportHtmlPath = path11.join(outputDir, "index.html");
|
|
5526
|
-
if (result.validationPassed) {
|
|
5527
|
-
logger.printSuccessLog(`Validation PASSED (MAE: ${result.finalMae.toFixed(METRIC_DECIMAL_PLACES)}px)`);
|
|
5528
|
-
} else {
|
|
5529
|
-
logger.printWarnLog(`Validation FAILED (MAE: ${result.finalMae.toFixed(METRIC_DECIMAL_PLACES)}px)`);
|
|
5530
|
-
}
|
|
5531
|
-
logger.printInfoLog(`Validation report: ${reportHtmlPath}`);
|
|
5532
|
-
};
|
|
5533
|
-
|
|
5534
|
-
// src/utils/url-parser.ts
|
|
5535
|
-
var parseFigmaUrl = (url) => {
|
|
5536
|
-
let fileId = null;
|
|
5537
|
-
let name = "untitled";
|
|
5538
|
-
let nodeId = null;
|
|
5539
|
-
try {
|
|
5540
|
-
const urlObj = new URL(decodeURIComponent(url));
|
|
5541
|
-
const pathParts = urlObj.pathname.split("/").filter(Boolean);
|
|
5542
|
-
if (pathParts.length >= 3) {
|
|
5543
|
-
fileId = pathParts[pathParts.length - 2] || null;
|
|
5544
|
-
const fileName = pathParts[pathParts.length - 1];
|
|
5545
|
-
name = fileName ? encodeURI(fileName).toLowerCase() : "untitled";
|
|
5546
|
-
name = name.length > 20 ? name.substring(0, 20) : name;
|
|
5547
|
-
}
|
|
5548
|
-
nodeId = urlObj.searchParams.get("node-id") || null;
|
|
5549
|
-
nodeId = nodeId ? nodeId.replace(/-/g, ":") : null;
|
|
5550
|
-
} catch {
|
|
5551
|
-
}
|
|
5552
|
-
if (!fileId || !nodeId) {
|
|
5553
|
-
throw new Error("Invalid Figma URL");
|
|
5554
|
-
}
|
|
5555
|
-
return { fileId, name, nodeId, projectName: `${name}_${nodeId}` };
|
|
5556
|
-
};
|
|
5557
|
-
|
|
5558
|
-
// src/nodes/code/index.ts
|
|
5559
|
-
init_logger();
|
|
5560
|
-
|
|
5561
|
-
// src/nodes/code/utils.ts
|
|
5562
|
-
init_logger();
|
|
5563
|
-
import fs10 from "fs";
|
|
5564
|
-
|
|
5565
|
-
// src/nodes/code/prompt.ts
|
|
5566
|
-
var STYLING_GUIDELINES = `
|
|
5567
|
-
- **Style Consistency**: Implement styles using the technical stack and libraries listed in <styling>.
|
|
5568
|
-
- **Strict Restriction**: Absolutely ONLY use the technical stack and libraries listed in <styling>. Do NOT use any other styling methods, libraries, or frameworks (e.g., if clsx is not listed, do not use clsx).
|
|
5569
|
-
- **Default Styling**: If <styling> is empty or does not contain specific libraries, DEFAULT to standard vanilla CSS.
|
|
5570
|
-
|
|
5571
|
-
- **Tailwind CSS + CSS Modules (CRITICAL)**:
|
|
5572
|
-
- If the stack includes BOTH Tailwind and CSS Modules (Less/SCSS), use them correctly:
|
|
5573
|
-
1. **Tailwind utilities**: Use DIRECTLY in JSX className (e.g., \`className="flex items-center gap-4"\`)
|
|
5574
|
-
2. **CSS Modules**: Use ONLY for complex styles that can't be expressed with Tailwind utilities (e.g., gradients, animations, pseudo-elements)
|
|
5575
|
-
3. **NEVER use \`@apply\` in CSS Module files** - it's a Tailwind-specific directive that doesn't work in Less/SCSS
|
|
5576
|
-
4. Example correct usage:
|
|
5577
|
-
TSX: \`<div className={\`flex \${styles.customGradient}\`}>\`
|
|
5578
|
-
Less: \`.customGradient { background: linear-gradient(...); }\`
|
|
5579
|
-
|
|
5580
|
-
- **CSS Modules Only**: If the tech stack specifies CSS Modules without Tailwind:
|
|
5581
|
-
1. Create a corresponding style file (e.g., \`index.module.less\`, \`index.module.scss\`, or \`index.module.css\`)
|
|
5582
|
-
2. Import it as \`import styles from './index.module.[ext]';\` in the TSX
|
|
5583
|
-
3. Define all styles in the style file using standard CSS/Less/SCSS syntax
|
|
5584
|
-
4. Use \`styles.className\` in JSX`;
|
|
5585
|
-
var ASSETS_HANDLING = `
|
|
5586
|
-
- **CRITICAL**: For any image URL starting with \`@/assets\`, you MUST import it at the top of the file.
|
|
5587
|
-
- **Asset Name Matching**:
|
|
5588
|
-
- Check the \`<available_assets>\` list for actual filenames in the project.
|
|
5589
|
-
- Asset filenames follow the pattern: \`kebab-case-name-id1-id2.ext\` (e.g., "Star 2.svg" \u2192 "star-2-1-2861.svg")
|
|
5590
|
-
- Match the base name (ignoring spaces, case, and ID suffix): "@/assets/arXiv.svg" \u2192 look for "arxiv-*.svg" in the list
|
|
5591
|
-
- Use the EXACT filename from the available assets list in your import.
|
|
5592
|
-
- Example: If available_assets contains "arxiv-1-2956.svg", use:
|
|
5593
|
-
\`import ArXivIcon from '@/assets/arxiv-1-2956.svg';\`
|
|
5594
|
-
- **Usage**: \`<img src={ArXivIcon} />\`, do not use backgroundImage property.
|
|
5595
|
-
- **NEVER** use the string path directly in JSX or styles.`;
|
|
5596
|
-
var DOM_IDS_REQUIREMENT = `
|
|
5597
|
-
- Assign \`id\` attributes to the main container and any internal elements, matching \`frame_details\`.`;
|
|
5598
|
-
var REACT_IMPORT_RULE = `
|
|
5599
|
-
- Do **NOT** include \`import React from 'react';\` at the top of the file.`;
|
|
5600
|
-
var FILE_NAMING_CONVENTION = `
|
|
5601
|
-
- ALWAYS name the main component file \`index.tsx\`.
|
|
5602
|
-
- ALWAYS name the style file (if applicable) \`index.module.[css|less|scss]\`.
|
|
5603
|
-
- NEVER use PascalCase or other names for filenames (e.g., DO NOT use \`MainFrame.tsx\` or \`Button.tsx\`).`;
|
|
5604
|
-
var OUTPUT_FORMAT = `
|
|
5605
|
-
<output_format>
|
|
5606
|
-
If only one file (TSX) is needed:
|
|
5607
|
-
\`\`\`tsx
|
|
5608
|
-
// code...
|
|
5609
|
-
\`\`\`
|
|
5610
|
-
|
|
5611
|
-
If multiple files are needed (e.g., TSX + Styles):
|
|
5612
|
-
## index.tsx
|
|
5613
|
-
\`\`\`tsx
|
|
5614
|
-
// code...
|
|
5615
|
-
\`\`\`
|
|
1895
|
+
If multiple files are needed (e.g., TSX + Styles):
|
|
1896
|
+
## index.tsx
|
|
1897
|
+
\`\`\`tsx
|
|
1898
|
+
// code...
|
|
1899
|
+
\`\`\`
|
|
5616
1900
|
|
|
5617
1901
|
## index.module.[css|less|scss]
|
|
5618
1902
|
\`\`\`[css|less|scss]
|
|
@@ -5851,72 +2135,6 @@ ${OUTPUT_FORMAT}
|
|
|
5851
2135
|
</system_instructions>
|
|
5852
2136
|
`.trim();
|
|
5853
2137
|
};
|
|
5854
|
-
var injectRootComponentPrompt = ({
|
|
5855
|
-
appContent,
|
|
5856
|
-
componentName,
|
|
5857
|
-
componentPath
|
|
5858
|
-
}) => {
|
|
5859
|
-
return `
|
|
5860
|
-
<system_instructions>
|
|
5861
|
-
<role>
|
|
5862
|
-
You are a React code refactoring expert.
|
|
5863
|
-
Your task is to inject a root component into an existing App.tsx file with minimal changes.
|
|
5864
|
-
</role>
|
|
5865
|
-
|
|
5866
|
-
<input_context>
|
|
5867
|
-
<current_app_content>
|
|
5868
|
-
${appContent}
|
|
5869
|
-
</current_app_content>
|
|
5870
|
-
<component_to_inject>
|
|
5871
|
-
- Component Name: ${componentName}
|
|
5872
|
-
- Import Path: ${componentPath}
|
|
5873
|
-
</component_to_inject>
|
|
5874
|
-
</input_context>
|
|
5875
|
-
|
|
5876
|
-
<requirements>
|
|
5877
|
-
<req_1>
|
|
5878
|
-
**Import Statement**:
|
|
5879
|
-
- Add the import statement at the top of the file: \`import ${componentName} from '${componentPath}';\`
|
|
5880
|
-
- Place it after existing imports, before the component definition.
|
|
5881
|
-
- Do NOT remove or modify existing imports.
|
|
5882
|
-
</req_1>
|
|
5883
|
-
|
|
5884
|
-
<req_2>
|
|
5885
|
-
**Component Rendering**:
|
|
5886
|
-
- Inside the App component's return statement, replace the existing content with \`<${componentName} />\`.
|
|
5887
|
-
- If the return contains a wrapper div or other container, keep it and place the component inside.
|
|
5888
|
-
- Preserve any existing className, styles, or other attributes on wrapper elements.
|
|
5889
|
-
</req_2>
|
|
5890
|
-
|
|
5891
|
-
<req_3>
|
|
5892
|
-
**Preserve Existing Code**:
|
|
5893
|
-
- Keep all existing imports (CSS, Less, etc.)
|
|
5894
|
-
- Preserve any hooks, state, or logic inside the App component
|
|
5895
|
-
- Maintain the component structure and export statement
|
|
5896
|
-
- Do NOT add comments or explanatory text
|
|
5897
|
-
</req_3>
|
|
5898
|
-
|
|
5899
|
-
<req_4>
|
|
5900
|
-
**Minimal Changes**:
|
|
5901
|
-
- Only add the necessary import and render the component
|
|
5902
|
-
- Do NOT refactor or optimize existing code
|
|
5903
|
-
- Do NOT change formatting or styling unless necessary
|
|
5904
|
-
- Do NOT add TypeScript types unless they already exist
|
|
5905
|
-
</req_4>
|
|
5906
|
-
|
|
5907
|
-
<req_5>
|
|
5908
|
-
**React Import**:
|
|
5909
|
-
${REACT_IMPORT_RULE}
|
|
5910
|
-
</req_5>
|
|
5911
|
-
</requirements>
|
|
5912
|
-
|
|
5913
|
-
<output_format>
|
|
5914
|
-
Return ONLY the complete updated App.tsx code without markdown code blocks or explanation.
|
|
5915
|
-
The output should be valid TypeScript/React code that can be directly written to the file.
|
|
5916
|
-
</output_format>
|
|
5917
|
-
</system_instructions>
|
|
5918
|
-
`.trim();
|
|
5919
|
-
};
|
|
5920
2138
|
|
|
5921
2139
|
// src/nodes/code/constants.ts
|
|
5922
2140
|
var DEFAULT_STYLING = {
|
|
@@ -5932,124 +2150,14 @@ var DEFAULT_STYLING = {
|
|
|
5932
2150
|
}
|
|
5933
2151
|
]
|
|
5934
2152
|
};
|
|
5935
|
-
var DEFAULT_APP_CONTENT = `function App() {
|
|
5936
|
-
return (
|
|
5937
|
-
<div>
|
|
5938
|
-
{/* Component will be injected here */}
|
|
5939
|
-
</div>
|
|
5940
|
-
);
|
|
5941
|
-
}
|
|
5942
|
-
|
|
5943
|
-
export default App;`;
|
|
5944
2153
|
|
|
5945
2154
|
// src/nodes/code/utils.ts
|
|
5946
|
-
import
|
|
2155
|
+
import path4 from "path";
|
|
5947
2156
|
|
|
5948
2157
|
// src/utils/code-cache.ts
|
|
5949
|
-
|
|
5950
|
-
import fs9 from "fs";
|
|
5951
|
-
function getCachePath(workspace) {
|
|
5952
|
-
return workspace.checkpoint;
|
|
5953
|
-
}
|
|
5954
|
-
function loadCodeCache(workspace) {
|
|
5955
|
-
const cachePath = getCachePath(workspace);
|
|
5956
|
-
try {
|
|
5957
|
-
if (!fs9.existsSync(cachePath)) {
|
|
5958
|
-
logger.printInfoLog("No code cache found, starting fresh");
|
|
5959
|
-
return createEmptyCache();
|
|
5960
|
-
}
|
|
5961
|
-
const content = fs9.readFileSync(cachePath, "utf-8");
|
|
5962
|
-
const cache = JSON.parse(content);
|
|
5963
|
-
if (!Array.isArray(cache.generatedComponents) || typeof cache.appInjected !== "boolean") {
|
|
5964
|
-
logger.printWarnLog("Invalid cache format, starting fresh");
|
|
5965
|
-
return createEmptyCache();
|
|
5966
|
-
}
|
|
5967
|
-
logger.printInfoLog(`Loaded code cache: ${cache.generatedComponents.length} components cached`);
|
|
5968
|
-
return cache;
|
|
5969
|
-
} catch (error) {
|
|
5970
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
5971
|
-
logger.printWarnLog(`Failed to load code cache: ${errorMessage}. Starting fresh.`);
|
|
5972
|
-
return createEmptyCache();
|
|
5973
|
-
}
|
|
5974
|
-
}
|
|
5975
|
-
function saveCodeCache(workspace, cache) {
|
|
5976
|
-
const cachePath = getCachePath(workspace);
|
|
5977
|
-
try {
|
|
5978
|
-
if (!fs9.existsSync(workspace.process)) {
|
|
5979
|
-
fs9.mkdirSync(workspace.process, { recursive: true });
|
|
5980
|
-
}
|
|
5981
|
-
const content = JSON.stringify(cache, null, 2);
|
|
5982
|
-
fs9.writeFileSync(cachePath, content, "utf-8");
|
|
5983
|
-
} catch (error) {
|
|
5984
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
5985
|
-
logger.printWarnLog(`Failed to save code cache: ${errorMessage}`);
|
|
5986
|
-
}
|
|
5987
|
-
}
|
|
5988
|
-
function isComponentGenerated(cache, nodeId) {
|
|
5989
|
-
return cache.generatedComponents.includes(nodeId);
|
|
5990
|
-
}
|
|
5991
|
-
function markComponentGenerated(cache, nodeId) {
|
|
5992
|
-
if (!isComponentGenerated(cache, nodeId)) {
|
|
5993
|
-
cache.generatedComponents.push(nodeId);
|
|
5994
|
-
}
|
|
5995
|
-
}
|
|
5996
|
-
function saveComponentGenerated(cache, nodeId, workspace) {
|
|
5997
|
-
markComponentGenerated(cache, nodeId);
|
|
5998
|
-
saveCodeCache(workspace, cache);
|
|
5999
|
-
}
|
|
6000
|
-
function isAppInjected(cache) {
|
|
6001
|
-
return cache.appInjected;
|
|
6002
|
-
}
|
|
6003
|
-
function markAppInjected(cache) {
|
|
6004
|
-
cache.appInjected = true;
|
|
6005
|
-
}
|
|
6006
|
-
function saveAppInjected(cache, workspace) {
|
|
6007
|
-
markAppInjected(cache);
|
|
6008
|
-
saveCodeCache(workspace, cache);
|
|
6009
|
-
}
|
|
6010
|
-
function createEmptyCache() {
|
|
6011
|
-
return {
|
|
6012
|
-
generatedComponents: [],
|
|
6013
|
-
appInjected: false
|
|
6014
|
-
};
|
|
6015
|
-
}
|
|
2158
|
+
import fs4 from "fs";
|
|
6016
2159
|
|
|
6017
2160
|
// src/nodes/code/utils.ts
|
|
6018
|
-
async function processNode(state, cache) {
|
|
6019
|
-
const assetFilesList = getAssetFilesList(state);
|
|
6020
|
-
const flatNodes = flattenPostOrder(state.protocol);
|
|
6021
|
-
const total = flatNodes.length;
|
|
6022
|
-
if (total === 0) {
|
|
6023
|
-
logger.printWarnLog("No components found in structure to generate.");
|
|
6024
|
-
return 0;
|
|
6025
|
-
}
|
|
6026
|
-
logger.printInfoLog(`Processing ${total} nodes...`);
|
|
6027
|
-
let processedCount = 0;
|
|
6028
|
-
let skippedCount = 0;
|
|
6029
|
-
const processSingleNode = async (currentNode) => {
|
|
6030
|
-
const componentName = currentNode.data.name || currentNode.data.componentName || "UnknownComponent";
|
|
6031
|
-
const nodeId = currentNode.id;
|
|
6032
|
-
if (isComponentGenerated(cache, nodeId)) {
|
|
6033
|
-
skippedCount++;
|
|
6034
|
-
logger.printInfoLog(`[${processedCount + skippedCount}/${total}] \u23ED\uFE0F Skipping (cached): ${componentName}`);
|
|
6035
|
-
return;
|
|
6036
|
-
}
|
|
6037
|
-
const progressInfo = `[${++processedCount + skippedCount}/${total}]`;
|
|
6038
|
-
const isLeaf = !currentNode.children?.length;
|
|
6039
|
-
if (isLeaf) {
|
|
6040
|
-
await generateComponent(currentNode, state, assetFilesList, progressInfo);
|
|
6041
|
-
} else {
|
|
6042
|
-
await generateFrame(currentNode, state, assetFilesList, progressInfo);
|
|
6043
|
-
}
|
|
6044
|
-
saveComponentGenerated(cache, nodeId, state.workspace);
|
|
6045
|
-
};
|
|
6046
|
-
await promisePool(flatNodes, processSingleNode);
|
|
6047
|
-
if (skippedCount > 0) {
|
|
6048
|
-
logger.printInfoLog(`\u23ED\uFE0F Skipped ${skippedCount} cached components`);
|
|
6049
|
-
}
|
|
6050
|
-
logger.printSuccessLog(`Generated ${processedCount} components`);
|
|
6051
|
-
return processedCount;
|
|
6052
|
-
}
|
|
6053
2161
|
function flattenPostOrder(node) {
|
|
6054
2162
|
const result = [];
|
|
6055
2163
|
function traverse(n) {
|
|
@@ -6069,221 +2177,44 @@ function detectRenderingModes(node) {
|
|
|
6069
2177
|
});
|
|
6070
2178
|
return { hasStates, hasIndependentChildren };
|
|
6071
2179
|
}
|
|
6072
|
-
async function generateFrame(node, state, assetFilesList, progressInfo) {
|
|
6073
|
-
const frameName = node.data.name;
|
|
6074
|
-
logger.printInfoLog(`${progressInfo} \u{1F5BC}\uFE0F Generating Frame: ${frameName}`);
|
|
6075
|
-
const childrenImports = (node.children || []).map((child) => ({
|
|
6076
|
-
name: child.data.name || "",
|
|
6077
|
-
path: child.data.path
|
|
6078
|
-
}));
|
|
6079
|
-
const renderingModes = detectRenderingModes(node);
|
|
6080
|
-
const prompt = generateFramePrompt({
|
|
6081
|
-
frameDetails: JSON.stringify(node.data),
|
|
6082
|
-
childrenImports: JSON.stringify(childrenImports),
|
|
6083
|
-
styling: JSON.stringify(DEFAULT_STYLING),
|
|
6084
|
-
assetFiles: assetFilesList,
|
|
6085
|
-
renderingModes
|
|
6086
|
-
});
|
|
6087
|
-
const code = await callModel({
|
|
6088
|
-
question: prompt,
|
|
6089
|
-
imageUrls: state.figmaInfo.thumbnail
|
|
6090
|
-
});
|
|
6091
|
-
const componentPath = node.data.path || "";
|
|
6092
|
-
const filePath = workspaceManager.resolveAppSrc(state.workspace, workspaceManager.resolveComponentPath(componentPath));
|
|
6093
|
-
saveGeneratedCode(code, filePath);
|
|
6094
|
-
logger.printSuccessLog(`Successfully generated frame: ${frameName}`);
|
|
6095
|
-
}
|
|
6096
|
-
async function generateComponent(node, state, assetFilesList, progressInfo) {
|
|
6097
|
-
const componentName = node.data.componentName || node.data.name || "UnknownComponent";
|
|
6098
|
-
const componentPath = node.data.componentPath || node.data.path || "";
|
|
6099
|
-
logger.printInfoLog(`${progressInfo} \u{1F4E6} Generating Component: ${componentName}`);
|
|
6100
|
-
const prompt = generateComponentPrompt({
|
|
6101
|
-
componentName,
|
|
6102
|
-
componentDetails: JSON.stringify(node.data),
|
|
6103
|
-
styling: JSON.stringify(DEFAULT_STYLING),
|
|
6104
|
-
assetFiles: assetFilesList
|
|
6105
|
-
});
|
|
6106
|
-
const code = await callModel({
|
|
6107
|
-
question: prompt,
|
|
6108
|
-
imageUrls: state.figmaInfo.thumbnail
|
|
6109
|
-
});
|
|
6110
|
-
const filePath = workspaceManager.resolveAppSrc(state.workspace, workspaceManager.resolveComponentPath(componentPath));
|
|
6111
|
-
saveGeneratedCode(code, filePath);
|
|
6112
|
-
logger.printSuccessLog(`Successfully generated component: ${componentName}`);
|
|
6113
|
-
}
|
|
6114
2180
|
function saveGeneratedCode(code, filePath) {
|
|
6115
2181
|
const files = extractFiles(code);
|
|
6116
2182
|
if (files.length > 0) {
|
|
6117
2183
|
createFiles({ files, filePath });
|
|
6118
2184
|
} else {
|
|
6119
2185
|
const extractedCode = extractCode(code);
|
|
6120
|
-
const folderPath =
|
|
6121
|
-
const fileName =
|
|
2186
|
+
const folderPath = path4.dirname(filePath);
|
|
2187
|
+
const fileName = path4.basename(filePath);
|
|
6122
2188
|
writeFile(folderPath, fileName, extractedCode);
|
|
6123
2189
|
}
|
|
6124
2190
|
}
|
|
6125
|
-
function getAssetFilesList(state) {
|
|
6126
|
-
try {
|
|
6127
|
-
const assetsDir = workspaceManager.resolveAppSrc(state.workspace, "assets");
|
|
6128
|
-
if (!fs10.existsSync(assetsDir)) {
|
|
6129
|
-
return "";
|
|
6130
|
-
}
|
|
6131
|
-
const files = fs10.readdirSync(assetsDir);
|
|
6132
|
-
return files.join(", ");
|
|
6133
|
-
} catch {
|
|
6134
|
-
return "";
|
|
6135
|
-
}
|
|
6136
|
-
}
|
|
6137
|
-
async function injectRootComponentToApp(state, cache) {
|
|
6138
|
-
try {
|
|
6139
|
-
if (isAppInjected(cache)) {
|
|
6140
|
-
logger.printInfoLog("\u23ED\uFE0F Skipping App.tsx injection (already injected)");
|
|
6141
|
-
return;
|
|
6142
|
-
}
|
|
6143
|
-
logger.printInfoLog("\u{1F489} Injecting root component into App.tsx...");
|
|
6144
|
-
const appTsxPath = workspaceManager.resolveAppSrc(state.workspace, "App.tsx");
|
|
6145
|
-
let appContent;
|
|
6146
|
-
try {
|
|
6147
|
-
appContent = fs10.readFileSync(appTsxPath, "utf8");
|
|
6148
|
-
} catch {
|
|
6149
|
-
logger.printWarnLog("App.tsx not found, using default template");
|
|
6150
|
-
appContent = DEFAULT_APP_CONTENT;
|
|
6151
|
-
}
|
|
6152
|
-
const rootNode = state.protocol;
|
|
6153
|
-
const componentName = rootNode.data.name || "RootComponent";
|
|
6154
|
-
const componentPath = rootNode.data.path || "";
|
|
6155
|
-
const prompt = injectRootComponentPrompt({
|
|
6156
|
-
appContent,
|
|
6157
|
-
componentName,
|
|
6158
|
-
componentPath
|
|
6159
|
-
});
|
|
6160
|
-
const updatedCode = await callModel({
|
|
6161
|
-
question: prompt
|
|
6162
|
-
});
|
|
6163
|
-
const finalCode = updatedCode.includes("```") ? extractCode(updatedCode) : updatedCode.trim();
|
|
6164
|
-
const appFolderPath = path12.dirname(appTsxPath);
|
|
6165
|
-
writeFile(appFolderPath, "App.tsx", finalCode);
|
|
6166
|
-
saveAppInjected(cache, state.workspace);
|
|
6167
|
-
logger.printSuccessLog(`Successfully injected ${componentName} into App.tsx`);
|
|
6168
|
-
} catch (error) {
|
|
6169
|
-
logger.printErrorLog(`Failed to inject root component: ${error.message}`);
|
|
6170
|
-
}
|
|
6171
|
-
}
|
|
6172
|
-
|
|
6173
|
-
// src/nodes/code/index.ts
|
|
6174
|
-
async function generateCode(state) {
|
|
6175
|
-
try {
|
|
6176
|
-
logger.printInfoLog("\u{1F680} Starting Code Generation...");
|
|
6177
|
-
if (!state.protocol) {
|
|
6178
|
-
throw new Error("No protocol data found");
|
|
6179
|
-
}
|
|
6180
|
-
const cache = loadCodeCache(state.workspace);
|
|
6181
|
-
const totalComponents = await processNode(state, cache);
|
|
6182
|
-
await injectRootComponentToApp(state, cache);
|
|
6183
|
-
logger.printSuccessLog(`\u2728 Code generation completed! Generated ${totalComponents} components`);
|
|
6184
|
-
} catch (error) {
|
|
6185
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
6186
|
-
throw new Error(`Code generation failed: ${errorMessage}`);
|
|
6187
|
-
}
|
|
6188
|
-
}
|
|
6189
|
-
|
|
6190
|
-
// src/utils/checkpoint.ts
|
|
6191
|
-
init_logger();
|
|
6192
|
-
import fs11 from "fs";
|
|
6193
|
-
import { SqliteSaver } from "@langchain/langgraph-checkpoint-sqlite";
|
|
6194
|
-
|
|
6195
|
-
// src/cli/prompts.ts
|
|
6196
|
-
init_logger();
|
|
6197
|
-
import prompts from "prompts";
|
|
6198
|
-
async function typedPrompt(config) {
|
|
6199
|
-
return prompts(config, {
|
|
6200
|
-
onCancel: () => {
|
|
6201
|
-
logger.printWarnLog("Operation cancelled by user.");
|
|
6202
|
-
process.exit(0);
|
|
6203
|
-
}
|
|
6204
|
-
});
|
|
6205
|
-
}
|
|
6206
|
-
async function promptUserChoice() {
|
|
6207
|
-
const response = await typedPrompt({
|
|
6208
|
-
type: "select",
|
|
6209
|
-
name: "choice",
|
|
6210
|
-
message: "What would you like to do?",
|
|
6211
|
-
choices: [
|
|
6212
|
-
{ title: "Resume from cache", value: "resume" },
|
|
6213
|
-
{ title: "Start fresh (will clear existing cache)", value: "fresh" }
|
|
6214
|
-
],
|
|
6215
|
-
initial: 0
|
|
6216
|
-
});
|
|
6217
|
-
return response.choice || "resume";
|
|
6218
|
-
}
|
|
6219
|
-
|
|
6220
|
-
// src/utils/checkpoint.ts
|
|
6221
|
-
async function checkpointExists(checkpointer, threadId) {
|
|
6222
|
-
try {
|
|
6223
|
-
const checkpoints = checkpointer.list({ configurable: { thread_id: threadId } }, { limit: 1 });
|
|
6224
|
-
const firstCheckpoint = await checkpoints.next();
|
|
6225
|
-
return !firstCheckpoint.done && firstCheckpoint.value !== void 0;
|
|
6226
|
-
} catch (error) {
|
|
6227
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
6228
|
-
logger.printTestLog(`Error checking checkpoint: ${errorMessage}`);
|
|
6229
|
-
return false;
|
|
6230
|
-
}
|
|
6231
|
-
}
|
|
6232
|
-
async function promptCheckpointChoice(checkpointer, threadId) {
|
|
6233
|
-
const hasCheckpoint = await checkpointExists(checkpointer, threadId);
|
|
6234
|
-
if (!hasCheckpoint) {
|
|
6235
|
-
return void 0;
|
|
6236
|
-
}
|
|
6237
|
-
const choice = await promptUserChoice();
|
|
6238
|
-
return choice === "resume";
|
|
6239
|
-
}
|
|
6240
|
-
function initializeSqliteSaver(dbPath) {
|
|
6241
|
-
const dbDir = dbPath.substring(0, dbPath.lastIndexOf("/"));
|
|
6242
|
-
if (!fs11.existsSync(dbDir)) {
|
|
6243
|
-
fs11.mkdirSync(dbDir, { recursive: true });
|
|
6244
|
-
}
|
|
6245
|
-
const checkpointer = SqliteSaver.fromConnString(dbPath);
|
|
6246
|
-
return checkpointer;
|
|
6247
|
-
}
|
|
6248
|
-
|
|
6249
|
-
// src/graph.ts
|
|
6250
|
-
init_logger();
|
|
6251
|
-
async function design2code(url, mode) {
|
|
6252
|
-
const urlInfo = parseFigmaUrl(url);
|
|
6253
|
-
const threadId = urlInfo.projectName;
|
|
6254
|
-
const workspace = workspaceManager.initWorkspace(threadId);
|
|
6255
|
-
let checkpointer = initializeSqliteSaver(workspace.db);
|
|
6256
|
-
const resume = await promptCheckpointChoice(checkpointer, threadId);
|
|
6257
|
-
logger.printInfoLog(`Starting design-to-code process for: ${urlInfo.projectName}`);
|
|
6258
|
-
if (resume !== true) {
|
|
6259
|
-
workspaceManager.deleteWorkspace(workspace);
|
|
6260
|
-
logger.printInfoLog("Starting fresh...");
|
|
6261
|
-
checkpointer = initializeSqliteSaver(workspace.db);
|
|
6262
|
-
} else {
|
|
6263
|
-
logger.printInfoLog("Resuming from cache...");
|
|
6264
|
-
}
|
|
6265
|
-
await callModel({
|
|
6266
|
-
question: "\u8BF7\u4ECB\u7ECD\u4F60\u81EA\u5DF1\uFF0C\u4F60\u662F\u4EC0\u4E48\u6A21\u578B",
|
|
6267
|
-
streaming: false
|
|
6268
|
-
});
|
|
6269
|
-
const graph = new StateGraph(GraphStateAnnotation).addNode("initial" /* INITIAL */, initialProject).addNode("process" /* PROCESS */, generateProtocol).addNode("code" /* CODE */, generateCode).addNode("validation" /* VALIDATION */, runValidation).addEdge(START, "initial" /* INITIAL */).addEdge("initial" /* INITIAL */, "process" /* PROCESS */).addEdge("process" /* PROCESS */, "code" /* CODE */).addEdge("code" /* CODE */, "validation" /* VALIDATION */).addEdge("validation" /* VALIDATION */, END).compile({ checkpointer });
|
|
6270
|
-
const config = { configurable: { thread_id: threadId } };
|
|
6271
|
-
const validationMode = mode ?? "full";
|
|
6272
|
-
const state = resume === true ? null : {
|
|
6273
|
-
messages: [],
|
|
6274
|
-
urlInfo,
|
|
6275
|
-
workspace,
|
|
6276
|
-
config: {
|
|
6277
|
-
validationMode
|
|
6278
|
-
}
|
|
6279
|
-
};
|
|
6280
|
-
await graph.invoke(state, config);
|
|
6281
|
-
logger.printSuccessLog("Design-to-code process completed!");
|
|
6282
|
-
}
|
|
6283
2191
|
export {
|
|
2192
|
+
DEFAULT_STYLING,
|
|
2193
|
+
FigmaImageFormat,
|
|
6284
2194
|
GraphNode,
|
|
6285
|
-
|
|
6286
|
-
|
|
2195
|
+
INITIAL_AGENT_SYSTEM_PROMPT,
|
|
2196
|
+
ValidationMode,
|
|
2197
|
+
applyPropsAndStateToProtocol,
|
|
2198
|
+
detectRenderingModes,
|
|
2199
|
+
downloadImage,
|
|
2200
|
+
executeFigmaAndImagesActions,
|
|
2201
|
+
extractComponentGroups,
|
|
2202
|
+
extractDataListPrompt,
|
|
2203
|
+
extractHierarchicalNodesByIds,
|
|
2204
|
+
extractJSON,
|
|
2205
|
+
extractNodePositionsHierarchical,
|
|
2206
|
+
figmaTool,
|
|
2207
|
+
flattenPostOrder,
|
|
2208
|
+
generateComponentPrompt,
|
|
2209
|
+
generateFramePrompt,
|
|
2210
|
+
generateStructurePrompt,
|
|
2211
|
+
initialAgentInstruction,
|
|
2212
|
+
parseFigmaUrl,
|
|
2213
|
+
postProcessStructure,
|
|
2214
|
+
saveGeneratedCode,
|
|
2215
|
+
simplifyFigmaNodeForContent,
|
|
2216
|
+
toKebabCase,
|
|
2217
|
+
toPascalCase,
|
|
6287
2218
|
workspaceManager
|
|
6288
2219
|
};
|
|
6289
2220
|
//# sourceMappingURL=index.js.map
|