arise-browser 0.1.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/LICENSE +190 -0
- package/README.md +247 -0
- package/deploy/neko/CONTEXT.md +37 -0
- package/deploy/neko/arise-browser.service +13 -0
- package/deploy/neko/neko.yaml +12 -0
- package/deploy/neko/openbox.xml +763 -0
- package/deploy/neko/policies.json +28 -0
- package/deploy/neko/pulseaudio.pa +16 -0
- package/deploy/neko/setup.sh +308 -0
- package/deploy/neko/xorg.conf +118 -0
- package/dist/bin/arise-browser.d.ts +26 -0
- package/dist/bin/arise-browser.d.ts.map +1 -0
- package/dist/bin/arise-browser.js +224 -0
- package/dist/bin/arise-browser.js.map +1 -0
- package/dist/src/browser/action-executor.d.ts +98 -0
- package/dist/src/browser/action-executor.d.ts.map +1 -0
- package/dist/src/browser/action-executor.js +2726 -0
- package/dist/src/browser/action-executor.js.map +1 -0
- package/dist/src/browser/behavior-recorder.d.ts +61 -0
- package/dist/src/browser/behavior-recorder.d.ts.map +1 -0
- package/dist/src/browser/behavior-recorder.js +442 -0
- package/dist/src/browser/behavior-recorder.js.map +1 -0
- package/dist/src/browser/browser-session.d.ts +202 -0
- package/dist/src/browser/browser-session.d.ts.map +1 -0
- package/dist/src/browser/browser-session.js +1647 -0
- package/dist/src/browser/browser-session.js.map +1 -0
- package/dist/src/browser/config.d.ts +43 -0
- package/dist/src/browser/config.d.ts.map +1 -0
- package/dist/src/browser/config.js +59 -0
- package/dist/src/browser/config.js.map +1 -0
- package/dist/src/browser/page-snapshot.d.ts +38 -0
- package/dist/src/browser/page-snapshot.d.ts.map +1 -0
- package/dist/src/browser/page-snapshot.js +241 -0
- package/dist/src/browser/page-snapshot.js.map +1 -0
- package/dist/src/browser/scripts/behavior_tracker.js +424 -0
- package/dist/src/browser/scripts/unified_analyzer.js +1576 -0
- package/dist/src/index.d.ts +15 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +15 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/lock.d.ts +11 -0
- package/dist/src/lock.d.ts.map +1 -0
- package/dist/src/lock.js +47 -0
- package/dist/src/lock.js.map +1 -0
- package/dist/src/logger.d.ts +17 -0
- package/dist/src/logger.d.ts.map +1 -0
- package/dist/src/logger.js +29 -0
- package/dist/src/logger.js.map +1 -0
- package/dist/src/server/middleware/auth.d.ts +6 -0
- package/dist/src/server/middleware/auth.d.ts.map +1 -0
- package/dist/src/server/middleware/auth.js +24 -0
- package/dist/src/server/middleware/auth.js.map +1 -0
- package/dist/src/server/route-utils.d.ts +15 -0
- package/dist/src/server/route-utils.d.ts.map +1 -0
- package/dist/src/server/route-utils.js +33 -0
- package/dist/src/server/route-utils.js.map +1 -0
- package/dist/src/server/routes/action.d.ts +5 -0
- package/dist/src/server/routes/action.d.ts.map +1 -0
- package/dist/src/server/routes/action.js +69 -0
- package/dist/src/server/routes/action.js.map +1 -0
- package/dist/src/server/routes/actions.d.ts +3 -0
- package/dist/src/server/routes/actions.d.ts.map +1 -0
- package/dist/src/server/routes/actions.js +53 -0
- package/dist/src/server/routes/actions.js.map +1 -0
- package/dist/src/server/routes/cookies.d.ts +3 -0
- package/dist/src/server/routes/cookies.d.ts.map +1 -0
- package/dist/src/server/routes/cookies.js +27 -0
- package/dist/src/server/routes/cookies.js.map +1 -0
- package/dist/src/server/routes/download.d.ts +3 -0
- package/dist/src/server/routes/download.d.ts.map +1 -0
- package/dist/src/server/routes/download.js +35 -0
- package/dist/src/server/routes/download.js.map +1 -0
- package/dist/src/server/routes/evaluate.d.ts +3 -0
- package/dist/src/server/routes/evaluate.d.ts.map +1 -0
- package/dist/src/server/routes/evaluate.js +27 -0
- package/dist/src/server/routes/evaluate.js.map +1 -0
- package/dist/src/server/routes/health.d.ts +3 -0
- package/dist/src/server/routes/health.d.ts.map +1 -0
- package/dist/src/server/routes/health.js +11 -0
- package/dist/src/server/routes/health.js.map +1 -0
- package/dist/src/server/routes/navigate.d.ts +3 -0
- package/dist/src/server/routes/navigate.d.ts.map +1 -0
- package/dist/src/server/routes/navigate.js +36 -0
- package/dist/src/server/routes/navigate.js.map +1 -0
- package/dist/src/server/routes/page-model.d.ts +3 -0
- package/dist/src/server/routes/page-model.d.ts.map +1 -0
- package/dist/src/server/routes/page-model.js +22 -0
- package/dist/src/server/routes/page-model.js.map +1 -0
- package/dist/src/server/routes/pdf.d.ts +3 -0
- package/dist/src/server/routes/pdf.d.ts.map +1 -0
- package/dist/src/server/routes/pdf.js +20 -0
- package/dist/src/server/routes/pdf.js.map +1 -0
- package/dist/src/server/routes/recording.d.ts +5 -0
- package/dist/src/server/routes/recording.d.ts.map +1 -0
- package/dist/src/server/routes/recording.js +217 -0
- package/dist/src/server/routes/recording.js.map +1 -0
- package/dist/src/server/routes/screenshot.d.ts +3 -0
- package/dist/src/server/routes/screenshot.d.ts.map +1 -0
- package/dist/src/server/routes/screenshot.js +32 -0
- package/dist/src/server/routes/screenshot.js.map +1 -0
- package/dist/src/server/routes/snapshot.d.ts +3 -0
- package/dist/src/server/routes/snapshot.d.ts.map +1 -0
- package/dist/src/server/routes/snapshot.js +454 -0
- package/dist/src/server/routes/snapshot.js.map +1 -0
- package/dist/src/server/routes/tab-lock.d.ts +3 -0
- package/dist/src/server/routes/tab-lock.d.ts.map +1 -0
- package/dist/src/server/routes/tab-lock.js +30 -0
- package/dist/src/server/routes/tab-lock.js.map +1 -0
- package/dist/src/server/routes/tab.d.ts +3 -0
- package/dist/src/server/routes/tab.d.ts.map +1 -0
- package/dist/src/server/routes/tab.js +47 -0
- package/dist/src/server/routes/tab.js.map +1 -0
- package/dist/src/server/routes/tabs.d.ts +3 -0
- package/dist/src/server/routes/tabs.d.ts.map +1 -0
- package/dist/src/server/routes/tabs.js +13 -0
- package/dist/src/server/routes/tabs.js.map +1 -0
- package/dist/src/server/routes/text.d.ts +3 -0
- package/dist/src/server/routes/text.d.ts.map +1 -0
- package/dist/src/server/routes/text.js +20 -0
- package/dist/src/server/routes/text.js.map +1 -0
- package/dist/src/server/routes/upload.d.ts +3 -0
- package/dist/src/server/routes/upload.d.ts.map +1 -0
- package/dist/src/server/routes/upload.js +38 -0
- package/dist/src/server/routes/upload.js.map +1 -0
- package/dist/src/server/server.d.ts +7 -0
- package/dist/src/server/server.d.ts.map +1 -0
- package/dist/src/server/server.js +69 -0
- package/dist/src/server/server.js.map +1 -0
- package/dist/src/types/index.d.ts +125 -0
- package/dist/src/types/index.d.ts.map +1 -0
- package/dist/src/types/index.js +5 -0
- package/dist/src/types/index.js.map +1 -0
- package/dist/src/virtual-display/manager.d.ts +37 -0
- package/dist/src/virtual-display/manager.d.ts.map +1 -0
- package/dist/src/virtual-display/manager.js +229 -0
- package/dist/src/virtual-display/manager.js.map +1 -0
- package/dist/src/virtual-display/process-runner.d.ts +43 -0
- package/dist/src/virtual-display/process-runner.d.ts.map +1 -0
- package/dist/src/virtual-display/process-runner.js +174 -0
- package/dist/src/virtual-display/process-runner.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -0
- package/package.json +57 -0
- package/plugin/openclaw.plugin.json +148 -0
- package/skill/arise-browser/SKILL.md +275 -0
- package/skill/arise-browser/TRUST.md +42 -0
- package/skill/arise-browser/references/api.md +198 -0
- package/src/browser/scripts/behavior_tracker.js +424 -0
- package/src/browser/scripts/unified_analyzer.js +1576 -0
|
@@ -0,0 +1,424 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Behavior Tracker - Captures user interactions using aria-ref system
|
|
3
|
+
*
|
|
4
|
+
* Records user actions in the same format as action_executor.py expects,
|
|
5
|
+
* using the persistent ref system from unified_analyzer.js.
|
|
6
|
+
*
|
|
7
|
+
* Output format matches ActionExecutor actions:
|
|
8
|
+
* { type: "click", ref: "e42", text: "Submit", role: "button" }
|
|
9
|
+
* { type: "type", ref: "e15", text: "hello", role: "textbox" }
|
|
10
|
+
*/
|
|
11
|
+
(function() {
|
|
12
|
+
// Prevent multiple initialization
|
|
13
|
+
if (window._behaviorTrackerInitialized) return;
|
|
14
|
+
window._behaviorTrackerInitialized = true;
|
|
15
|
+
|
|
16
|
+
console.log("🎯 Behavior Tracker initialized (ref-based)");
|
|
17
|
+
|
|
18
|
+
// =========================================================================
|
|
19
|
+
// Element Info Collector - Uses aria-ref from unified_analyzer.js
|
|
20
|
+
// =========================================================================
|
|
21
|
+
|
|
22
|
+
function getElementRef(element) {
|
|
23
|
+
if (!element) return null;
|
|
24
|
+
|
|
25
|
+
// Get aria-ref assigned by unified_analyzer.js
|
|
26
|
+
const ref = element.getAttribute('aria-ref');
|
|
27
|
+
if (ref) return ref;
|
|
28
|
+
|
|
29
|
+
// Walk up to find nearest parent with aria-ref
|
|
30
|
+
let parent = element.parentElement;
|
|
31
|
+
let depth = 0;
|
|
32
|
+
while (parent && depth < 5) {
|
|
33
|
+
const parentRef = parent.getAttribute('aria-ref');
|
|
34
|
+
if (parentRef) return parentRef;
|
|
35
|
+
parent = parent.parentElement;
|
|
36
|
+
depth++;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function getElementRole(element) {
|
|
43
|
+
if (!element) return null;
|
|
44
|
+
|
|
45
|
+
// Check explicit role attribute
|
|
46
|
+
const role = element.getAttribute('role');
|
|
47
|
+
if (role) return role;
|
|
48
|
+
|
|
49
|
+
// Infer role from tag name
|
|
50
|
+
const tagName = element.tagName.toLowerCase();
|
|
51
|
+
const roleMap = {
|
|
52
|
+
'a': 'link',
|
|
53
|
+
'button': 'button',
|
|
54
|
+
'input': element.type === 'checkbox' ? 'checkbox'
|
|
55
|
+
: element.type === 'radio' ? 'radio'
|
|
56
|
+
: 'textbox',
|
|
57
|
+
'select': 'combobox',
|
|
58
|
+
'textarea': 'textbox',
|
|
59
|
+
'h1': 'heading', 'h2': 'heading', 'h3': 'heading',
|
|
60
|
+
'h4': 'heading', 'h5': 'heading', 'h6': 'heading',
|
|
61
|
+
'img': 'img',
|
|
62
|
+
'nav': 'navigation',
|
|
63
|
+
'main': 'main',
|
|
64
|
+
'ul': 'list', 'ol': 'list',
|
|
65
|
+
'li': 'listitem',
|
|
66
|
+
'table': 'table',
|
|
67
|
+
'tr': 'row',
|
|
68
|
+
'td': 'cell', 'th': 'cell'
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
return roleMap[tagName] || 'generic';
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function getElementText(element) {
|
|
75
|
+
if (!element) return '';
|
|
76
|
+
|
|
77
|
+
// Check aria-label first
|
|
78
|
+
const ariaLabel = element.getAttribute('aria-label');
|
|
79
|
+
if (ariaLabel) return ariaLabel.trim();
|
|
80
|
+
|
|
81
|
+
// Check aria-labelledby
|
|
82
|
+
const labelledBy = element.getAttribute('aria-labelledby');
|
|
83
|
+
if (labelledBy) {
|
|
84
|
+
const labelEl = document.getElementById(labelledBy);
|
|
85
|
+
if (labelEl) return labelEl.textContent.trim();
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// For inputs, use placeholder or value
|
|
89
|
+
if (element.tagName === 'INPUT' || element.tagName === 'TEXTAREA') {
|
|
90
|
+
return element.placeholder || '';
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Get visible text content (limited)
|
|
94
|
+
const text = (element.textContent || '').trim();
|
|
95
|
+
return text.slice(0, 100);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function getElementInfo(element) {
|
|
99
|
+
if (!element) return null;
|
|
100
|
+
|
|
101
|
+
const ref = getElementRef(element);
|
|
102
|
+
const role = getElementRole(element);
|
|
103
|
+
const text = getElementText(element);
|
|
104
|
+
const tagName = element.tagName.toLowerCase();
|
|
105
|
+
|
|
106
|
+
// For links, capture href (useful when ref is missing during navigation)
|
|
107
|
+
let href = null;
|
|
108
|
+
if (tagName === 'a') {
|
|
109
|
+
href = element.href;
|
|
110
|
+
} else {
|
|
111
|
+
// Check if element is inside a link
|
|
112
|
+
const linkParent = element.closest('a');
|
|
113
|
+
if (linkParent) {
|
|
114
|
+
href = linkParent.href;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return {
|
|
119
|
+
ref: ref,
|
|
120
|
+
role: role,
|
|
121
|
+
text: text,
|
|
122
|
+
tagName: tagName,
|
|
123
|
+
href: href
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// =========================================================================
|
|
128
|
+
// Operation Reporter
|
|
129
|
+
// =========================================================================
|
|
130
|
+
|
|
131
|
+
function report(type, elementInfo, additionalData) {
|
|
132
|
+
const timestamp = new Date().toISOString();
|
|
133
|
+
|
|
134
|
+
const operation = {
|
|
135
|
+
type: type,
|
|
136
|
+
timestamp: timestamp,
|
|
137
|
+
url: window.location.href
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
// Add element info if available
|
|
141
|
+
if (elementInfo) {
|
|
142
|
+
if (elementInfo.ref) operation.ref = elementInfo.ref;
|
|
143
|
+
if (elementInfo.text) operation.text = elementInfo.text;
|
|
144
|
+
if (elementInfo.role) operation.role = elementInfo.role;
|
|
145
|
+
if (elementInfo.href) operation.href = elementInfo.href;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Merge additional data
|
|
149
|
+
if (additionalData) {
|
|
150
|
+
Object.assign(operation, additionalData);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Report via CDP binding or postMessage
|
|
154
|
+
if (window.reportUserBehavior) {
|
|
155
|
+
try {
|
|
156
|
+
window.reportUserBehavior(JSON.stringify(operation));
|
|
157
|
+
} catch (e) {
|
|
158
|
+
console.warn('Failed to report via CDP:', e);
|
|
159
|
+
}
|
|
160
|
+
} else {
|
|
161
|
+
// Chrome Extension fallback
|
|
162
|
+
window.postMessage({
|
|
163
|
+
source: 'arise-tracker',
|
|
164
|
+
operation: operation
|
|
165
|
+
}, '*');
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// =========================================================================
|
|
170
|
+
// Click Handler
|
|
171
|
+
// =========================================================================
|
|
172
|
+
|
|
173
|
+
let isDragging = false;
|
|
174
|
+
let dragStartX = 0;
|
|
175
|
+
let dragStartY = 0;
|
|
176
|
+
|
|
177
|
+
document.addEventListener('mousedown', function(e) {
|
|
178
|
+
isDragging = false;
|
|
179
|
+
dragStartX = e.clientX;
|
|
180
|
+
dragStartY = e.clientY;
|
|
181
|
+
}, true);
|
|
182
|
+
|
|
183
|
+
document.addEventListener('mousemove', function(e) {
|
|
184
|
+
if (e.buttons === 1) {
|
|
185
|
+
const distance = Math.sqrt(
|
|
186
|
+
Math.pow(e.clientX - dragStartX, 2) +
|
|
187
|
+
Math.pow(e.clientY - dragStartY, 2)
|
|
188
|
+
);
|
|
189
|
+
if (distance > 5) {
|
|
190
|
+
isDragging = true;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}, true);
|
|
194
|
+
|
|
195
|
+
document.addEventListener('click', function(e) {
|
|
196
|
+
// Skip if this was a drag operation
|
|
197
|
+
if (isDragging) {
|
|
198
|
+
isDragging = false;
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Flush all pending type events before click (may trigger navigation)
|
|
203
|
+
flushAllPendingTypes();
|
|
204
|
+
|
|
205
|
+
const info = getElementInfo(e.target);
|
|
206
|
+
// Always report click, even without ref (e.g., navigation links)
|
|
207
|
+
report('click', info);
|
|
208
|
+
}, true);
|
|
209
|
+
|
|
210
|
+
// =========================================================================
|
|
211
|
+
// Input Handler (debounced)
|
|
212
|
+
// =========================================================================
|
|
213
|
+
|
|
214
|
+
const inputTimeouts = new Map(); // key -> { timeout, element, info }
|
|
215
|
+
const INPUT_DEBOUNCE_MS = 1500;
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Flush a pending type event immediately (cancel its debounce timer).
|
|
219
|
+
* Called before navigation-triggering actions (Enter, form submit) to
|
|
220
|
+
* ensure the type event is reported before the page unloads.
|
|
221
|
+
*/
|
|
222
|
+
function flushPendingType(key) {
|
|
223
|
+
const pending = inputTimeouts.get(key);
|
|
224
|
+
if (!pending) return;
|
|
225
|
+
|
|
226
|
+
clearTimeout(pending.timeout);
|
|
227
|
+
inputTimeouts.delete(key);
|
|
228
|
+
|
|
229
|
+
const value = (pending.element.value || '').trim();
|
|
230
|
+
if (value) {
|
|
231
|
+
report('type', pending.info, { value: value });
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function flushAllPendingTypes() {
|
|
236
|
+
for (const key of Array.from(inputTimeouts.keys())) {
|
|
237
|
+
flushPendingType(key);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
document.addEventListener('input', function(e) {
|
|
242
|
+
const element = e.target;
|
|
243
|
+
const tagName = element.tagName.toLowerCase();
|
|
244
|
+
|
|
245
|
+
// Only track actual input fields
|
|
246
|
+
if (tagName !== 'input' && tagName !== 'textarea' && element.contentEditable !== 'true') {
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Skip password fields
|
|
251
|
+
if (element.type === 'password' || element.type === 'hidden') {
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const info = getElementInfo(element);
|
|
256
|
+
if (!info) return;
|
|
257
|
+
|
|
258
|
+
// Debounce by element ref (fall back to tagName to avoid null-key collisions)
|
|
259
|
+
const key = info.ref || ('__' + tagName + '_' + (element.name || element.id || 'anon'));
|
|
260
|
+
if (inputTimeouts.has(key)) {
|
|
261
|
+
clearTimeout(inputTimeouts.get(key).timeout);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const timeout = setTimeout(() => {
|
|
265
|
+
const value = element.value || '';
|
|
266
|
+
if (value.trim()) {
|
|
267
|
+
report('type', info, {
|
|
268
|
+
value: value
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
inputTimeouts.delete(key);
|
|
272
|
+
}, INPUT_DEBOUNCE_MS);
|
|
273
|
+
|
|
274
|
+
inputTimeouts.set(key, { timeout, element, info });
|
|
275
|
+
}, true);
|
|
276
|
+
|
|
277
|
+
// =========================================================================
|
|
278
|
+
// Navigation Handler (SPA detection)
|
|
279
|
+
// CDP Page.frameNavigated catches full page loads; JS polling catches
|
|
280
|
+
// pushState/replaceState SPA navigations. Deduplication is done in Python.
|
|
281
|
+
// =========================================================================
|
|
282
|
+
|
|
283
|
+
let currentUrl = window.location.href;
|
|
284
|
+
|
|
285
|
+
// Only in main frame
|
|
286
|
+
if (window.self === window.top) {
|
|
287
|
+
// URL polling for SPA navigation
|
|
288
|
+
setInterval(function() {
|
|
289
|
+
if (window.location.href !== currentUrl) {
|
|
290
|
+
// Flush pending type events before SPA navigation
|
|
291
|
+
flushAllPendingTypes();
|
|
292
|
+
|
|
293
|
+
const fromUrl = currentUrl;
|
|
294
|
+
currentUrl = window.location.href;
|
|
295
|
+
|
|
296
|
+
report('navigate', null, {
|
|
297
|
+
url: currentUrl,
|
|
298
|
+
from_url: fromUrl
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
}, 500);
|
|
302
|
+
|
|
303
|
+
// Popstate for browser back/forward
|
|
304
|
+
window.addEventListener('popstate', function() {
|
|
305
|
+
flushAllPendingTypes();
|
|
306
|
+
|
|
307
|
+
const fromUrl = currentUrl;
|
|
308
|
+
currentUrl = window.location.href;
|
|
309
|
+
|
|
310
|
+
report('navigate', null, {
|
|
311
|
+
url: currentUrl,
|
|
312
|
+
from_url: fromUrl
|
|
313
|
+
});
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// Flush all pending type events before page unload
|
|
318
|
+
window.addEventListener('beforeunload', flushAllPendingTypes);
|
|
319
|
+
|
|
320
|
+
// =========================================================================
|
|
321
|
+
// Scroll Handler (throttled)
|
|
322
|
+
// =========================================================================
|
|
323
|
+
|
|
324
|
+
let scrollTimeout;
|
|
325
|
+
let lastScrollY = window.scrollY;
|
|
326
|
+
const SCROLL_THRESHOLD = 100;
|
|
327
|
+
|
|
328
|
+
window.addEventListener('scroll', function() {
|
|
329
|
+
clearTimeout(scrollTimeout);
|
|
330
|
+
|
|
331
|
+
scrollTimeout = setTimeout(function() {
|
|
332
|
+
const currentScrollY = window.scrollY;
|
|
333
|
+
const delta = currentScrollY - lastScrollY;
|
|
334
|
+
|
|
335
|
+
if (Math.abs(delta) > SCROLL_THRESHOLD) {
|
|
336
|
+
report('scroll', null, {
|
|
337
|
+
direction: delta > 0 ? 'down' : 'up',
|
|
338
|
+
amount: Math.abs(Math.round(delta))
|
|
339
|
+
});
|
|
340
|
+
lastScrollY = currentScrollY;
|
|
341
|
+
}
|
|
342
|
+
}, 150);
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
// =========================================================================
|
|
346
|
+
// Select (Dropdown) Handler
|
|
347
|
+
// =========================================================================
|
|
348
|
+
|
|
349
|
+
document.addEventListener('change', function(e) {
|
|
350
|
+
const element = e.target;
|
|
351
|
+
|
|
352
|
+
if (element.tagName.toLowerCase() === 'select') {
|
|
353
|
+
const info = getElementInfo(element);
|
|
354
|
+
if (info) {
|
|
355
|
+
report('select', info, {
|
|
356
|
+
value: element.value
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
}, true);
|
|
361
|
+
|
|
362
|
+
// =========================================================================
|
|
363
|
+
// Enter Key Handler
|
|
364
|
+
// =========================================================================
|
|
365
|
+
|
|
366
|
+
document.addEventListener('keydown', function(e) {
|
|
367
|
+
if (e.key === 'Enter') {
|
|
368
|
+
// Flush pending type for this element before enter triggers navigation
|
|
369
|
+
const info = getElementInfo(e.target);
|
|
370
|
+
const key = info && info.ref
|
|
371
|
+
? info.ref
|
|
372
|
+
: ('__' + e.target.tagName.toLowerCase() + '_' + (e.target.name || e.target.id || 'anon'));
|
|
373
|
+
flushPendingType(key);
|
|
374
|
+
report('enter', info);
|
|
375
|
+
}
|
|
376
|
+
}, true);
|
|
377
|
+
|
|
378
|
+
// =========================================================================
|
|
379
|
+
// Copy/Paste Handlers
|
|
380
|
+
// =========================================================================
|
|
381
|
+
|
|
382
|
+
document.addEventListener('copy', function(e) {
|
|
383
|
+
const selection = window.getSelection();
|
|
384
|
+
const text = selection ? selection.toString() : '';
|
|
385
|
+
|
|
386
|
+
if (text) {
|
|
387
|
+
report('copy', null, {
|
|
388
|
+
text: text.slice(0, 200)
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
document.addEventListener('paste', function(e) {
|
|
394
|
+
const info = getElementInfo(e.target);
|
|
395
|
+
const clipboardData = e.clipboardData || window.clipboardData;
|
|
396
|
+
const text = clipboardData ? clipboardData.getData('text') : '';
|
|
397
|
+
|
|
398
|
+
report('paste', info, {
|
|
399
|
+
text: text.slice(0, 200)
|
|
400
|
+
});
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
// =========================================================================
|
|
404
|
+
// Text Selection Handler (drag-based)
|
|
405
|
+
// =========================================================================
|
|
406
|
+
|
|
407
|
+
document.addEventListener('mouseup', function(e) {
|
|
408
|
+
if (isDragging) {
|
|
409
|
+
setTimeout(function() {
|
|
410
|
+
const selection = window.getSelection();
|
|
411
|
+
const text = selection ? selection.toString().trim() : '';
|
|
412
|
+
|
|
413
|
+
if (text.length > 0) {
|
|
414
|
+
report('select_text', null, {
|
|
415
|
+
text: text.slice(0, 200)
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
}, 10);
|
|
419
|
+
}
|
|
420
|
+
isDragging = false;
|
|
421
|
+
}, true);
|
|
422
|
+
|
|
423
|
+
console.log("✅ Behavior Tracker ready");
|
|
424
|
+
})();
|