vision-navigator 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 +113 -0
- package/dist/cdp-driver.js +484 -0
- package/dist/cli.js +207 -0
- package/dist/injected-scripts.js +328 -0
- package/dist/navigator.js +897 -0
- package/dist/server.js +409 -0
- package/dist/storage.js +132 -0
- package/package.json +48 -0
- package/public/app.js +155 -0
- package/public/index.html +567 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
4
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
5
|
+
};
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
require("dotenv/config");
|
|
8
|
+
const server_1 = require("./server");
|
|
9
|
+
const navigator_1 = require("./navigator");
|
|
10
|
+
const fs_1 = __importDefault(require("fs"));
|
|
11
|
+
const path_1 = __importDefault(require("path"));
|
|
12
|
+
const js_yaml_1 = __importDefault(require("js-yaml"));
|
|
13
|
+
const rawArgs = process.argv.slice(2);
|
|
14
|
+
const args = [];
|
|
15
|
+
for (let i = 0; i < rawArgs.length; i++) {
|
|
16
|
+
const a = rawArgs[i];
|
|
17
|
+
if (a === '--groq-key') {
|
|
18
|
+
const v = rawArgs[i + 1];
|
|
19
|
+
if (v) {
|
|
20
|
+
process.env.GROQ_API_KEY = v;
|
|
21
|
+
i++;
|
|
22
|
+
}
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
if (a === '--groq-model') {
|
|
26
|
+
const v = rawArgs[i + 1];
|
|
27
|
+
if (v) {
|
|
28
|
+
process.env.GROQ_MODEL = v;
|
|
29
|
+
i++;
|
|
30
|
+
}
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
if (a === '--fast') {
|
|
34
|
+
process.env.FAST_MODE = '1';
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
if (a === '--verify') {
|
|
38
|
+
process.env.VERIFY_WITH_AI = '1';
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
if (a === '--no-analysis') {
|
|
42
|
+
process.env.SKIP_ANALYSIS = '1';
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
if (a === '--analysis-async') {
|
|
46
|
+
process.env.ANALYSIS_ASYNC = '1';
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
args.push(a);
|
|
50
|
+
}
|
|
51
|
+
const command = args[0];
|
|
52
|
+
if (command === 'run') {
|
|
53
|
+
const filePath = args[1];
|
|
54
|
+
if (!filePath) {
|
|
55
|
+
console.error("Error: Please provide a path to a YAML workflow file.");
|
|
56
|
+
console.log("Usage: vision-navigator run <workflow.yaml>");
|
|
57
|
+
process.exit(1);
|
|
58
|
+
}
|
|
59
|
+
const fullPath = path_1.default.resolve(process.cwd(), filePath);
|
|
60
|
+
if (!fs_1.default.existsSync(fullPath)) {
|
|
61
|
+
console.error(`Error: File not found: ${fullPath}`);
|
|
62
|
+
process.exit(1);
|
|
63
|
+
}
|
|
64
|
+
console.log(`Loading workflow from ${filePath}...`);
|
|
65
|
+
const fileContent = fs_1.default.readFileSync(fullPath, 'utf-8');
|
|
66
|
+
let workflow;
|
|
67
|
+
try {
|
|
68
|
+
workflow = js_yaml_1.default.load(fileContent);
|
|
69
|
+
}
|
|
70
|
+
catch (e) {
|
|
71
|
+
console.error(`Error parsing YAML: ${e}`);
|
|
72
|
+
process.exit(1);
|
|
73
|
+
}
|
|
74
|
+
const nav = new navigator_1.Navigator();
|
|
75
|
+
nav.runWorkflow(workflow).then(run => {
|
|
76
|
+
console.log("\n========================================");
|
|
77
|
+
console.log(" WORKFLOW EXECUTION RESULTS ");
|
|
78
|
+
console.log("========================================\n");
|
|
79
|
+
let allPassed = true;
|
|
80
|
+
run.results.forEach((r, i) => {
|
|
81
|
+
const statusIcon = (r.status === 'executed' || r.status === 'completed') ? '✅' : '❌';
|
|
82
|
+
console.log(`${statusIcon} Step ${i + 1}: ${r.step}`);
|
|
83
|
+
console.log(` Status: ${r.status}`);
|
|
84
|
+
if (r.action) {
|
|
85
|
+
console.log(` Action: ${r.action.replace(/\n/g, ' ')}`);
|
|
86
|
+
}
|
|
87
|
+
if (r.ai_suggestions) {
|
|
88
|
+
console.log(` 💡 AI Feedback: ${r.ai_suggestions.split('\n').map((l) => l.trim()).filter((l) => l).join(' ')}`);
|
|
89
|
+
}
|
|
90
|
+
if (r.has_console_errors || (r.network_issues && r.network_issues.length > 0)) {
|
|
91
|
+
console.log(` ⚠️ Issues detected (check web UI or logs for details)`);
|
|
92
|
+
}
|
|
93
|
+
console.log("");
|
|
94
|
+
if (r.status !== 'executed' && r.status !== 'completed') {
|
|
95
|
+
allPassed = false;
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
console.log("========================================");
|
|
99
|
+
if (allPassed) {
|
|
100
|
+
console.log("🎉 All steps executed successfully!");
|
|
101
|
+
process.exit(0);
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
console.log("⚠️ Some steps failed or encountered issues.");
|
|
105
|
+
process.exit(1);
|
|
106
|
+
}
|
|
107
|
+
}).catch(e => {
|
|
108
|
+
console.error(`Workflow execution failed: ${e}`);
|
|
109
|
+
process.exit(1);
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
else if (command === 'test') {
|
|
113
|
+
const dirPath = args[1] || '.';
|
|
114
|
+
const fullDirPath = path_1.default.resolve(process.cwd(), dirPath);
|
|
115
|
+
if (!fs_1.default.existsSync(fullDirPath) || !fs_1.default.statSync(fullDirPath).isDirectory()) {
|
|
116
|
+
console.error(`Error: Directory not found: ${fullDirPath}`);
|
|
117
|
+
process.exit(1);
|
|
118
|
+
}
|
|
119
|
+
const files = fs_1.default.readdirSync(fullDirPath)
|
|
120
|
+
.filter(f => f.endsWith('.yaml') || f.endsWith('.yml'))
|
|
121
|
+
.map(f => path_1.default.join(fullDirPath, f));
|
|
122
|
+
if (files.length === 0) {
|
|
123
|
+
console.log(`No YAML files found in ${fullDirPath}`);
|
|
124
|
+
process.exit(0);
|
|
125
|
+
}
|
|
126
|
+
console.log(`Found ${files.length} test files. Starting execution...\n`);
|
|
127
|
+
(async () => {
|
|
128
|
+
const nav = new navigator_1.Navigator();
|
|
129
|
+
let allTestsPassed = true;
|
|
130
|
+
const testResults = [];
|
|
131
|
+
for (const file of files) {
|
|
132
|
+
console.log(`\n--- Running Test: ${path_1.default.basename(file)} ---`);
|
|
133
|
+
const fileContent = fs_1.default.readFileSync(file, 'utf-8');
|
|
134
|
+
let workflow;
|
|
135
|
+
try {
|
|
136
|
+
workflow = js_yaml_1.default.load(fileContent);
|
|
137
|
+
}
|
|
138
|
+
catch (e) {
|
|
139
|
+
console.error(`Error parsing YAML: ${e}`);
|
|
140
|
+
testResults.push({ file: path_1.default.basename(file), passed: false, steps: 0 });
|
|
141
|
+
allTestsPassed = false;
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
try {
|
|
145
|
+
const run = await nav.runWorkflow(workflow);
|
|
146
|
+
let passed = true;
|
|
147
|
+
run.results.forEach((r) => {
|
|
148
|
+
if (r.status !== 'executed' && r.status !== 'completed') {
|
|
149
|
+
passed = false;
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
if (!passed)
|
|
153
|
+
allTestsPassed = false;
|
|
154
|
+
console.log(`\nResult for ${path_1.default.basename(file)}: ${passed ? '✅ PASSED' : '❌ FAILED'}`);
|
|
155
|
+
testResults.push({ file: path_1.default.basename(file), passed, steps: run.results.length });
|
|
156
|
+
}
|
|
157
|
+
catch (e) {
|
|
158
|
+
console.error(`Failed executing ${path_1.default.basename(file)}: ${e}`);
|
|
159
|
+
testResults.push({ file: path_1.default.basename(file), passed: false, steps: 0 });
|
|
160
|
+
allTestsPassed = false;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
console.log("\n========================================");
|
|
164
|
+
console.log(" TEST SUITE RESULTS ");
|
|
165
|
+
console.log("========================================");
|
|
166
|
+
testResults.forEach(tr => {
|
|
167
|
+
console.log(`${tr.passed ? '✅' : '❌'} ${tr.file} (${tr.steps} steps)`);
|
|
168
|
+
});
|
|
169
|
+
console.log("========================================");
|
|
170
|
+
if (allTestsPassed) {
|
|
171
|
+
console.log("🎉 ALL TESTS PASSED!");
|
|
172
|
+
process.exit(0);
|
|
173
|
+
}
|
|
174
|
+
else {
|
|
175
|
+
console.log("⚠️ SOME TESTS FAILED.");
|
|
176
|
+
process.exit(1);
|
|
177
|
+
}
|
|
178
|
+
})();
|
|
179
|
+
}
|
|
180
|
+
else if (command === 'serve' || !command) {
|
|
181
|
+
const port = process.env.PORT ? parseInt(process.env.PORT, 10) : 8000;
|
|
182
|
+
(0, server_1.startServer)(port);
|
|
183
|
+
}
|
|
184
|
+
else {
|
|
185
|
+
console.log("Usage: vision-navigator [serve | run <file.yaml>]");
|
|
186
|
+
console.log("");
|
|
187
|
+
console.log("Commands:");
|
|
188
|
+
console.log(" serve Start the web UI server (default)");
|
|
189
|
+
console.log(" run <file.yaml> Execute a workflow file in the terminal");
|
|
190
|
+
console.log(" test [dir] Run all YAML workflows in a directory (default: current dir)");
|
|
191
|
+
console.log("");
|
|
192
|
+
console.log("Environment:");
|
|
193
|
+
console.log(" GROQ_API_KEY Use Groq for actions/verification/analysis");
|
|
194
|
+
console.log(" GROQ_MODEL Override Groq model (default: llama-3.1-8b-instant)");
|
|
195
|
+
console.log(" FAST_MODE=1 Faster runs (shorter prompts, less waiting)");
|
|
196
|
+
console.log(" VERIFY_WITH_AI=1 Verify critical steps (login/submit/continue etc)");
|
|
197
|
+
console.log(" SKIP_ANALYSIS=1 Skip long step analysis (keeps verifier if enabled)");
|
|
198
|
+
console.log(" ANALYSIS_ASYNC=1 Run analysis in background (faster step loop)");
|
|
199
|
+
console.log("");
|
|
200
|
+
console.log("Flags:");
|
|
201
|
+
console.log(" --groq-key <key> Set GROQ_API_KEY for this run");
|
|
202
|
+
console.log(" --groq-model <id> Set GROQ_MODEL for this run");
|
|
203
|
+
console.log(" --fast Set FAST_MODE=1");
|
|
204
|
+
console.log(" --verify Set VERIFY_WITH_AI=1");
|
|
205
|
+
console.log(" --no-analysis Set SKIP_ANALYSIS=1");
|
|
206
|
+
console.log(" --analysis-async Set ANALYSIS_ASYNC=1");
|
|
207
|
+
}
|
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.PAGE_AGENT_SCRIPT = void 0;
|
|
4
|
+
exports.PAGE_AGENT_SCRIPT = `
|
|
5
|
+
(function() {
|
|
6
|
+
const DOM_HASH_MAP = {};
|
|
7
|
+
const ID = { current: 0 };
|
|
8
|
+
const HIGHLIGHT_CONTAINER_ID = 'playwright-highlight-container';
|
|
9
|
+
let xpathCache = new WeakMap();
|
|
10
|
+
|
|
11
|
+
// Cache for DOM queries
|
|
12
|
+
const DOM_CACHE = {
|
|
13
|
+
boundingRects: new WeakMap(),
|
|
14
|
+
computedStyles: new WeakMap(),
|
|
15
|
+
clearCache: () => {
|
|
16
|
+
DOM_CACHE.boundingRects = new WeakMap();
|
|
17
|
+
DOM_CACHE.computedStyles = new WeakMap();
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
function getCachedBoundingRect(element) {
|
|
22
|
+
if (!element) return null;
|
|
23
|
+
if (DOM_CACHE.boundingRects.has(element)) {
|
|
24
|
+
return DOM_CACHE.boundingRects.get(element);
|
|
25
|
+
}
|
|
26
|
+
const rect = element.getBoundingClientRect();
|
|
27
|
+
if (rect) {
|
|
28
|
+
DOM_CACHE.boundingRects.set(element, rect);
|
|
29
|
+
}
|
|
30
|
+
return rect;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function getCachedComputedStyle(element) {
|
|
34
|
+
if (!element) return null;
|
|
35
|
+
if (DOM_CACHE.computedStyles.has(element)) {
|
|
36
|
+
return DOM_CACHE.computedStyles.get(element);
|
|
37
|
+
}
|
|
38
|
+
const style = window.getComputedStyle(element);
|
|
39
|
+
if (style) {
|
|
40
|
+
DOM_CACHE.computedStyles.set(element, style);
|
|
41
|
+
}
|
|
42
|
+
return style;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function getElementPosition(currentElement) {
|
|
46
|
+
if (!currentElement.parentElement) return 0;
|
|
47
|
+
const tagName = currentElement.nodeName.toLowerCase();
|
|
48
|
+
const siblings = Array.from(currentElement.parentElement.children).filter(
|
|
49
|
+
(sib) => sib.nodeName.toLowerCase() === tagName
|
|
50
|
+
);
|
|
51
|
+
if (siblings.length === 1) return 0;
|
|
52
|
+
return siblings.indexOf(currentElement) + 1;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function getXPathTree(element, stopAtBoundary = true) {
|
|
56
|
+
if (xpathCache.has(element)) return xpathCache.get(element);
|
|
57
|
+
const segments = [];
|
|
58
|
+
let currentElement = element;
|
|
59
|
+
while (currentElement && currentElement.nodeType === Node.ELEMENT_NODE) {
|
|
60
|
+
if (stopAtBoundary && (currentElement.parentNode instanceof ShadowRoot || currentElement.parentNode instanceof HTMLIFrameElement)) {
|
|
61
|
+
break;
|
|
62
|
+
}
|
|
63
|
+
const position = getElementPosition(currentElement);
|
|
64
|
+
const tagName = currentElement.nodeName.toLowerCase();
|
|
65
|
+
const xpathIndex = position > 0 ? \`[\${position}]\` : '';
|
|
66
|
+
segments.unshift(\`\${tagName}\${xpathIndex}\`);
|
|
67
|
+
currentElement = currentElement.parentNode;
|
|
68
|
+
}
|
|
69
|
+
const result = segments.join('/');
|
|
70
|
+
xpathCache.set(element, result);
|
|
71
|
+
return result;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function isScrollableElement(element) {
|
|
75
|
+
if (!element || element.nodeType !== Node.ELEMENT_NODE) return null;
|
|
76
|
+
const style = getCachedComputedStyle(element);
|
|
77
|
+
if (!style) return null;
|
|
78
|
+
const display = style.display;
|
|
79
|
+
if (display === 'inline' || display === 'inline-block') return null;
|
|
80
|
+
|
|
81
|
+
const overflowX = style.overflowX;
|
|
82
|
+
const overflowY = style.overflowY;
|
|
83
|
+
const scrollableX = overflowX === 'auto' || overflowX === 'scroll';
|
|
84
|
+
const scrollableY = overflowY === 'auto' || overflowY === 'scroll';
|
|
85
|
+
|
|
86
|
+
if (!scrollableX && !scrollableY) return null;
|
|
87
|
+
|
|
88
|
+
const scrollWidth = element.scrollWidth - element.clientWidth;
|
|
89
|
+
const scrollHeight = element.scrollHeight - element.clientHeight;
|
|
90
|
+
const threshold = 4;
|
|
91
|
+
|
|
92
|
+
if (scrollWidth < threshold && scrollHeight < threshold) return null;
|
|
93
|
+
if (!scrollableY && scrollWidth < threshold) return null;
|
|
94
|
+
if (!scrollableX && scrollHeight < threshold) return null;
|
|
95
|
+
|
|
96
|
+
return {
|
|
97
|
+
top: element.scrollTop,
|
|
98
|
+
right: element.scrollWidth - element.clientWidth - element.scrollLeft,
|
|
99
|
+
bottom: element.scrollHeight - element.clientHeight - element.scrollTop,
|
|
100
|
+
left: element.scrollLeft
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function isElementVisible(element) {
|
|
105
|
+
const style = getCachedComputedStyle(element);
|
|
106
|
+
return (
|
|
107
|
+
element.offsetWidth > 0 &&
|
|
108
|
+
element.offsetHeight > 0 &&
|
|
109
|
+
style?.visibility !== 'hidden' &&
|
|
110
|
+
style?.display !== 'none'
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function isInteractiveElement(element) {
|
|
115
|
+
if (!element || element.nodeType !== Node.ELEMENT_NODE) return false;
|
|
116
|
+
|
|
117
|
+
const tagName = element.tagName.toLowerCase();
|
|
118
|
+
const style = getCachedComputedStyle(element);
|
|
119
|
+
|
|
120
|
+
// Ignore elements that cannot receive pointer events
|
|
121
|
+
if (style?.pointerEvents === 'none') return false;
|
|
122
|
+
|
|
123
|
+
// Explicit interactive tags
|
|
124
|
+
const interactiveTags = new Set(['a', 'button', 'input', 'select', 'textarea', 'details', 'summary', 'label', 'option']);
|
|
125
|
+
if (interactiveTags.has(tagName)) {
|
|
126
|
+
// Check disabled
|
|
127
|
+
if (element.disabled || element.readOnly || element.inert) return false;
|
|
128
|
+
return true;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Roles
|
|
132
|
+
const role = element.getAttribute('role');
|
|
133
|
+
const interactiveRoles = new Set(['button', 'link', 'menuitem', 'tab', 'checkbox', 'radio', 'switch', 'combobox', 'searchbox', 'textbox', 'listbox', 'option', 'slider', 'spinbutton', 'scrollbar']);
|
|
134
|
+
if (role && interactiveRoles.has(role)) return true;
|
|
135
|
+
|
|
136
|
+
// Content editable
|
|
137
|
+
if (element.getAttribute('contenteditable') === 'true' || element.getAttribute('contenteditable') === '') return true;
|
|
138
|
+
if (element.isContentEditable) {
|
|
139
|
+
// Only mark as interactive if parent is not also contentEditable (to avoid highlighting every single child span)
|
|
140
|
+
if (element.parentElement && !element.parentElement.isContentEditable) {
|
|
141
|
+
return true;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Scrollable
|
|
146
|
+
if (isScrollableElement(element)) return true;
|
|
147
|
+
|
|
148
|
+
// Event listeners (requires getEventListeners or heuristic)
|
|
149
|
+
try {
|
|
150
|
+
if (typeof getEventListeners === 'function') {
|
|
151
|
+
const listeners = getEventListeners(element);
|
|
152
|
+
if (listeners && (listeners.click || listeners.mousedown || listeners.mouseup)) return true;
|
|
153
|
+
}
|
|
154
|
+
} catch(e) {}
|
|
155
|
+
|
|
156
|
+
if (element.onclick || element.onmousedown || element.onmouseup) return true;
|
|
157
|
+
|
|
158
|
+
// Interactive cursors
|
|
159
|
+
const interactiveCursors = new Set(['pointer', 'cell', 'crosshair', 'move', 'grab', 'grabbing', 'zoom-in', 'zoom-out']);
|
|
160
|
+
if (style?.cursor && interactiveCursors.has(style.cursor)) {
|
|
161
|
+
// Check if parent has the same cursor (to avoid highlighting children just due to inheritance)
|
|
162
|
+
let parent = element.parentElement;
|
|
163
|
+
if (parent) {
|
|
164
|
+
const parentStyle = getCachedComputedStyle(parent);
|
|
165
|
+
if (parentStyle && parentStyle.cursor === style.cursor) {
|
|
166
|
+
return false;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return true;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return false;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function highlightElement(element, index, parentIframe) {
|
|
176
|
+
let container = document.getElementById(HIGHLIGHT_CONTAINER_ID);
|
|
177
|
+
if (!container) {
|
|
178
|
+
container = document.createElement('div');
|
|
179
|
+
container.id = HIGHLIGHT_CONTAINER_ID;
|
|
180
|
+
container.style.position = 'fixed';
|
|
181
|
+
container.style.top = '0';
|
|
182
|
+
container.style.left = '0';
|
|
183
|
+
container.style.width = '100%';
|
|
184
|
+
container.style.height = '100%';
|
|
185
|
+
container.style.zIndex = '2147483647';
|
|
186
|
+
container.style.pointerEvents = 'none';
|
|
187
|
+
document.body.appendChild(container);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const rect = element.getBoundingClientRect();
|
|
191
|
+
if (!rect || rect.width === 0 || rect.height === 0) return;
|
|
192
|
+
|
|
193
|
+
const colors = [
|
|
194
|
+
'#FF0000', '#00FF00', '#0000FF', '#FFA500', '#800080', '#008080', '#FF69B4', '#4B0082', '#FF4500', '#2E8B57', '#DC143C', '#4682B4'
|
|
195
|
+
];
|
|
196
|
+
const baseColor = colors[index % colors.length];
|
|
197
|
+
const backgroundColor = baseColor + '1A'; // Low opacity
|
|
198
|
+
|
|
199
|
+
const fragment = document.createDocumentFragment();
|
|
200
|
+
let iframeOffset = { x: 0, y: 0 };
|
|
201
|
+
if (parentIframe) {
|
|
202
|
+
const iframeRect = parentIframe.getBoundingClientRect();
|
|
203
|
+
iframeOffset.x = iframeRect.left;
|
|
204
|
+
iframeOffset.y = iframeRect.top;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const overlay = document.createElement('div');
|
|
208
|
+
overlay.style.position = 'fixed';
|
|
209
|
+
overlay.style.border = \`2px solid \${baseColor}\`;
|
|
210
|
+
overlay.style.backgroundColor = backgroundColor;
|
|
211
|
+
overlay.style.pointerEvents = 'none';
|
|
212
|
+
overlay.style.boxSizing = 'border-box';
|
|
213
|
+
overlay.style.top = \`\${rect.top + iframeOffset.y}px\`;
|
|
214
|
+
overlay.style.left = \`\${rect.left + iframeOffset.x}px\`;
|
|
215
|
+
overlay.style.width = \`\${rect.width}px\`;
|
|
216
|
+
overlay.style.height = \`\${rect.height}px\`;
|
|
217
|
+
fragment.appendChild(overlay);
|
|
218
|
+
|
|
219
|
+
// Label
|
|
220
|
+
const label = document.createElement('div');
|
|
221
|
+
label.className = 'vnav-highlight-label';
|
|
222
|
+
label.style.position = 'fixed';
|
|
223
|
+
label.style.background = baseColor;
|
|
224
|
+
label.style.color = 'white';
|
|
225
|
+
label.style.padding = '1px 4px';
|
|
226
|
+
label.style.borderRadius = '4px';
|
|
227
|
+
label.style.fontSize = '12px';
|
|
228
|
+
label.style.fontWeight = 'bold';
|
|
229
|
+
label.textContent = index.toString();
|
|
230
|
+
|
|
231
|
+
let labelTop = rect.top + iframeOffset.y;
|
|
232
|
+
let labelLeft = rect.left + iframeOffset.x; // Position at top-left
|
|
233
|
+
|
|
234
|
+
// Adjust slightly so it doesn't overlap completely with the border
|
|
235
|
+
labelTop -= 10;
|
|
236
|
+
labelLeft -= 10;
|
|
237
|
+
|
|
238
|
+
if (labelLeft < 0) labelLeft = 0;
|
|
239
|
+
if (labelTop < 0) labelTop = 0;
|
|
240
|
+
|
|
241
|
+
label.style.top = \`\${labelTop}px\`;
|
|
242
|
+
label.style.left = \`\${labelLeft}px\`;
|
|
243
|
+
|
|
244
|
+
fragment.appendChild(label);
|
|
245
|
+
container.appendChild(fragment);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Helper to remove existing highlights
|
|
249
|
+
function removeHighlights() {
|
|
250
|
+
const container = document.getElementById(HIGHLIGHT_CONTAINER_ID);
|
|
251
|
+
if (container) container.remove();
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function buildDomTree(node, highlightIndexRef) {
|
|
255
|
+
if (!node) return null;
|
|
256
|
+
if (node.id === HIGHLIGHT_CONTAINER_ID) return null;
|
|
257
|
+
|
|
258
|
+
// Skip script/style etc
|
|
259
|
+
const denyList = new Set(['script', 'style', 'noscript', 'meta', 'link', 'svg']);
|
|
260
|
+
if (denyList.has(node.tagName?.toLowerCase())) return null;
|
|
261
|
+
|
|
262
|
+
if (node.nodeType === Node.TEXT_NODE) {
|
|
263
|
+
const text = node.textContent.trim();
|
|
264
|
+
if (!text) return null;
|
|
265
|
+
if (!isElementVisible(node.parentElement)) return null;
|
|
266
|
+
return {
|
|
267
|
+
type: 'TEXT_NODE',
|
|
268
|
+
text: text,
|
|
269
|
+
isVisible: true
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
if (node.nodeType !== Node.ELEMENT_NODE) return null;
|
|
274
|
+
if (!isElementVisible(node)) return null;
|
|
275
|
+
|
|
276
|
+
const nodeData = {
|
|
277
|
+
tagName: node.tagName.toLowerCase(),
|
|
278
|
+
attributes: {},
|
|
279
|
+
children: [],
|
|
280
|
+
isInteractive: false,
|
|
281
|
+
highlightIndex: null
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
// Attributes
|
|
285
|
+
if (node.hasAttributes()) {
|
|
286
|
+
for (const attr of node.attributes) {
|
|
287
|
+
nodeData.attributes[attr.name] = attr.value;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// Interactivity
|
|
292
|
+
if (isInteractiveElement(node)) {
|
|
293
|
+
nodeData.isInteractive = true;
|
|
294
|
+
nodeData.highlightIndex = highlightIndexRef.current++;
|
|
295
|
+
highlightElement(node, nodeData.highlightIndex, null);
|
|
296
|
+
|
|
297
|
+
// Add vnav-id to element for easy lookup later if needed
|
|
298
|
+
node.setAttribute('data-vnav-id', nodeData.highlightIndex);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// Children
|
|
302
|
+
if (node.shadowRoot) {
|
|
303
|
+
for (const child of node.shadowRoot.childNodes) {
|
|
304
|
+
const childData = buildDomTree(child, highlightIndexRef);
|
|
305
|
+
if (childData) nodeData.children.push(childData);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
for (const child of node.childNodes) {
|
|
309
|
+
const childData = buildDomTree(child, highlightIndexRef);
|
|
310
|
+
if (childData) nodeData.children.push(childData);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
return nodeData;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// Main execution
|
|
317
|
+
removeHighlights();
|
|
318
|
+
DOM_CACHE.clearCache();
|
|
319
|
+
const highlightIndexRef = { current: 1 };
|
|
320
|
+
|
|
321
|
+
const tree = buildDomTree(document.body, highlightIndexRef);
|
|
322
|
+
|
|
323
|
+
return {
|
|
324
|
+
tree,
|
|
325
|
+
elementCount: highlightIndexRef.current - 1
|
|
326
|
+
};
|
|
327
|
+
})()
|
|
328
|
+
`;
|