agex 0.2.8 → 0.2.10
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 +30 -0
- package/assets/assets/cursor.js +115 -37
- package/assets/assets/effects.js +150 -8
- package/assets/scripts/ab-click.sh +85 -0
- package/assets/scripts/ab-close.sh +3 -3
- package/assets/scripts/ab-record-start.sh +2 -2
- package/assets/scripts/ab-record.sh +2 -2
- package/assets/scripts/ab-type.sh +47 -0
- package/assets/scripts/agent-browser-wrapper.sh +2 -1
- package/assets/scripts/fx-highlight.sh +11 -3
- package/assets/scripts/fx-subtitle.sh +14 -0
- package/dist/{chunk-FCCNIKUD.js → chunk-HPJHT4WX.js} +93 -30
- package/dist/cli.js +387 -25
- package/dist/index.js +1 -1
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -75,6 +75,36 @@ agex prove "youtube homepage shows at least 6 video thumbnails with titles and v
|
|
|
75
75
|
|
|
76
76
|
---
|
|
77
77
|
|
|
78
|
+
### `agex demo` - Record demo videos
|
|
79
|
+
|
|
80
|
+
Record a narrated browser walkthrough as a video. The agent opens a URL, performs the task, and captures the session.
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
# Record a demo of selecting a brand design from a menu
|
|
84
|
+
agex demo "how to select a brand design from the menu" --url https://example.com
|
|
85
|
+
|
|
86
|
+
# With subtitles and custom colors (via task text)
|
|
87
|
+
agex demo "how to create a new project. use subtitles with white text on red background" \
|
|
88
|
+
--url https://app.example.com --agent cursor
|
|
89
|
+
|
|
90
|
+
# Non-headless for debugging
|
|
91
|
+
agex demo "sign up for a free trial" --url https://example.com --no-headless
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
| Flag | Description | Default |
|
|
95
|
+
|------|-------------|---------|
|
|
96
|
+
| `--agent, -a` | Agent to use: `cursor`, `claude`, `codex` | auto-detected |
|
|
97
|
+
| `--url, -u` | URL to navigate to | - |
|
|
98
|
+
| `--output, -o` | Output directory | `./demo-results` |
|
|
99
|
+
| `--video` | Enable video recording | `true` |
|
|
100
|
+
| `--screenshots` | Enable screenshots | `true` |
|
|
101
|
+
| `--model, -m` | Model to use | - |
|
|
102
|
+
| `--timeout, -t` | Timeout in ms | `300000` |
|
|
103
|
+
| `--viewport` | Viewport size (WxH) | `1920x1080` |
|
|
104
|
+
| `--headless` | Run headless | `true` |
|
|
105
|
+
|
|
106
|
+
---
|
|
107
|
+
|
|
78
108
|
### `agex prove-pr` - PR verification in CI
|
|
79
109
|
|
|
80
110
|
Run it in your CI. It reads the diff, thinks, acts, and reports autonomously.
|
package/assets/assets/cursor.js
CHANGED
|
@@ -3,6 +3,19 @@
|
|
|
3
3
|
let clickRippleEnabled = true;
|
|
4
4
|
let trailEnabled = false;
|
|
5
5
|
let trailPoints = [];
|
|
6
|
+
let currentMode = 'default'; // 'default' or 'pointer'
|
|
7
|
+
let animationLock = false; // when true, mousemove won't update demo cursor position
|
|
8
|
+
|
|
9
|
+
const ARROW_SVG = `<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32" fill="none">
|
|
10
|
+
<path d="M8.5 3L8.5 24.2l5-5.6 3.6 7.7 3.5-1.5-3.7-7.6 7.1-.5L8.5 3z" fill="#000" stroke="#fff" stroke-width="1.8" stroke-linejoin="round"></path>
|
|
11
|
+
</svg>`;
|
|
12
|
+
|
|
13
|
+
const POINTER_SVG = `<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32" fill="none">
|
|
14
|
+
<g transform="rotate(-20, 16, 16)">
|
|
15
|
+
<path d="M14 2 C14 2 11.5 2 11.5 4.5 L11.5 15 L10 13.5 C10 13.5 8.5 12 7 12.5 C5.5 13 5.8 14.5 6.5 15.8 L10.5 22.5 C12 25.5 14.5 27.5 18 27.5 L19.5 27.5 C23.5 27.5 26.5 24 26.5 20 L26.5 15.5 C26.5 14 25 13 23.5 13.5 C23 13.7 22.5 14.2 22.5 15 L22.5 14 C22.5 12.5 21 11.5 19.5 12 C19 12.2 18.5 12.7 18.5 13.5 L18.5 12.5 C18.5 11 17 10 15.5 10.5 C15 10.7 14.5 11.3 14.5 12 L14.5 4.5 C14.5 4.5 14.5 2 14 2 Z" fill="#000" stroke="#fff" stroke-width="1.6" stroke-linejoin="round" stroke-linecap="round"/>
|
|
16
|
+
<line x1="18.5" y1="13" x2="18.5" y2="17" stroke="#fff" stroke-width="0.6" opacity="0.3"/><line x1="22.5" y1="14.5" x2="22.5" y2="17" stroke="#fff" stroke-width="0.6" opacity="0.3"/>
|
|
17
|
+
</g>
|
|
18
|
+
</svg>`;
|
|
6
19
|
|
|
7
20
|
function createCursor() {
|
|
8
21
|
if (document.getElementById('agex-cursor')) return;
|
|
@@ -10,21 +23,36 @@
|
|
|
10
23
|
const cursor = document.createElement('div');
|
|
11
24
|
cursor.id = 'agex-cursor';
|
|
12
25
|
cursor.style.cssText = `
|
|
13
|
-
width: 24px;
|
|
14
|
-
height: 24px;
|
|
15
|
-
background: rgba(255, 50, 50, 0.85);
|
|
16
|
-
border: 2px solid white;
|
|
17
|
-
border-radius: 50%;
|
|
18
26
|
position: fixed;
|
|
19
27
|
pointer-events: none;
|
|
20
28
|
z-index: 2147483647;
|
|
21
|
-
transform: translate(-50%, -50%);
|
|
22
|
-
box-shadow: 0 0 8px rgba(0, 0, 0, 0.3);
|
|
23
29
|
left: -100px;
|
|
24
30
|
top: -100px;
|
|
25
31
|
opacity: 0;
|
|
26
|
-
transition:
|
|
32
|
+
transition: opacity 0.15s, transform 0.1s ease-out;
|
|
33
|
+
transform-origin: 2px 2px;
|
|
34
|
+
will-change: left, top;
|
|
35
|
+
`;
|
|
36
|
+
|
|
37
|
+
const dot = document.createElement('div');
|
|
38
|
+
dot.className = 'agex-cursor-dot';
|
|
39
|
+
dot.style.cssText = `
|
|
40
|
+
position: absolute;
|
|
41
|
+
width: 38px;
|
|
42
|
+
height: 38px;
|
|
43
|
+
border-radius: 50%;
|
|
44
|
+
background: rgb(251, 255, 0, 0.45);
|
|
45
|
+
top: -16px;
|
|
46
|
+
left: -10px;
|
|
47
|
+
pointer-events: none;
|
|
27
48
|
`;
|
|
49
|
+
|
|
50
|
+
const svg = document.createElement('div');
|
|
51
|
+
svg.className = 'agex-cursor-svg';
|
|
52
|
+
svg.innerHTML = ARROW_SVG;
|
|
53
|
+
|
|
54
|
+
cursor.appendChild(dot);
|
|
55
|
+
cursor.appendChild(svg);
|
|
28
56
|
document.body.appendChild(cursor);
|
|
29
57
|
|
|
30
58
|
const style = document.createElement('style');
|
|
@@ -33,20 +61,28 @@
|
|
|
33
61
|
.agex-cursor-ripple {
|
|
34
62
|
position: fixed;
|
|
35
63
|
border-radius: 50%;
|
|
36
|
-
background: rgba(255, 50, 50, 0.4);
|
|
37
64
|
pointer-events: none;
|
|
38
65
|
z-index: 2147483646;
|
|
39
66
|
transform: translate(-50%, -50%) scale(0);
|
|
40
|
-
animation: agex-ripple 0.
|
|
67
|
+
animation: agex-ripple 0.5s ease-out forwards;
|
|
68
|
+
}
|
|
69
|
+
.agex-cursor-ripple-ring {
|
|
70
|
+
position: fixed;
|
|
71
|
+
border-radius: 50%;
|
|
72
|
+
border: 3px solid rgba(0, 0, 0, 0.4);
|
|
73
|
+
pointer-events: none;
|
|
74
|
+
z-index: 2147483646;
|
|
75
|
+
transform: translate(-50%, -50%) scale(0);
|
|
76
|
+
animation: agex-ripple 0.6s ease-out forwards;
|
|
41
77
|
}
|
|
42
78
|
@keyframes agex-ripple {
|
|
43
79
|
to { transform: translate(-50%, -50%) scale(1); opacity: 0; }
|
|
44
80
|
}
|
|
45
81
|
.agex-cursor-trail {
|
|
46
82
|
position: fixed;
|
|
47
|
-
width:
|
|
48
|
-
height:
|
|
49
|
-
background: rgba(
|
|
83
|
+
width: 6px;
|
|
84
|
+
height: 6px;
|
|
85
|
+
background: rgba(0, 0, 0, 0.12);
|
|
50
86
|
border-radius: 50%;
|
|
51
87
|
pointer-events: none;
|
|
52
88
|
z-index: 2147483645;
|
|
@@ -55,16 +91,29 @@
|
|
|
55
91
|
`;
|
|
56
92
|
document.head.appendChild(style);
|
|
57
93
|
|
|
94
|
+
const CLICKABLE = 'a, button, [role="button"], input[type="submit"], input[type="button"], select, label[for], [onclick], [data-action], summary, [tabindex]';
|
|
95
|
+
|
|
58
96
|
document.addEventListener('mousemove', (e) => {
|
|
59
97
|
const c = document.getElementById('agex-cursor');
|
|
60
98
|
if (c) {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
hasMoved
|
|
65
|
-
|
|
99
|
+
if (!animationLock) {
|
|
100
|
+
c.style.left = `${e.clientX}px`;
|
|
101
|
+
c.style.top = `${e.clientY}px`;
|
|
102
|
+
if (!hasMoved) {
|
|
103
|
+
hasMoved = true;
|
|
104
|
+
c.style.opacity = '1';
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const hovered = document.elementFromPoint(e.clientX, e.clientY);
|
|
109
|
+
const isClickable = hovered && hovered.closest(CLICKABLE);
|
|
110
|
+
const newMode = isClickable ? 'pointer' : 'default';
|
|
111
|
+
if (newMode !== currentMode) {
|
|
112
|
+
currentMode = newMode;
|
|
113
|
+
const svgEl = c.querySelector('.agex-cursor-svg');
|
|
114
|
+
if (svgEl) svgEl.innerHTML = currentMode === 'pointer' ? POINTER_SVG : ARROW_SVG;
|
|
66
115
|
}
|
|
67
|
-
|
|
116
|
+
|
|
68
117
|
if (trailEnabled) {
|
|
69
118
|
createTrailPoint(e.clientX, e.clientY);
|
|
70
119
|
}
|
|
@@ -74,9 +123,8 @@
|
|
|
74
123
|
document.addEventListener('mousedown', (e) => {
|
|
75
124
|
const c = document.getElementById('agex-cursor');
|
|
76
125
|
if (c) {
|
|
77
|
-
c.style.transform = '
|
|
78
|
-
|
|
79
|
-
|
|
126
|
+
c.style.transform = 'scale(0.85)';
|
|
127
|
+
|
|
80
128
|
if (clickRippleEnabled) {
|
|
81
129
|
createClickRipple(e.clientX, e.clientY);
|
|
82
130
|
}
|
|
@@ -86,21 +134,30 @@
|
|
|
86
134
|
document.addEventListener('mouseup', () => {
|
|
87
135
|
const c = document.getElementById('agex-cursor');
|
|
88
136
|
if (c) {
|
|
89
|
-
c.style.transform = '
|
|
90
|
-
c.style.background = 'rgba(255, 50, 50, 0.85)';
|
|
137
|
+
c.style.transform = 'scale(1)';
|
|
91
138
|
}
|
|
92
139
|
}, true);
|
|
93
140
|
}
|
|
94
141
|
|
|
95
142
|
function createClickRipple(x, y) {
|
|
96
|
-
const
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
143
|
+
const fill = document.createElement('div');
|
|
144
|
+
fill.className = 'agex-cursor-ripple';
|
|
145
|
+
fill.style.left = `${x}px`;
|
|
146
|
+
fill.style.top = `${y}px`;
|
|
147
|
+
fill.style.width = '50px';
|
|
148
|
+
fill.style.height = '50px';
|
|
149
|
+
fill.style.background = 'rgba(251, 255, 0, 0.35)';
|
|
150
|
+
document.body.appendChild(fill);
|
|
151
|
+
setTimeout(() => fill.remove(), 500);
|
|
152
|
+
|
|
153
|
+
const ring = document.createElement('div');
|
|
154
|
+
ring.className = 'agex-cursor-ripple-ring';
|
|
155
|
+
ring.style.left = `${x}px`;
|
|
156
|
+
ring.style.top = `${y}px`;
|
|
157
|
+
ring.style.width = '70px';
|
|
158
|
+
ring.style.height = '70px';
|
|
159
|
+
document.body.appendChild(ring);
|
|
160
|
+
setTimeout(() => ring.remove(), 600);
|
|
104
161
|
}
|
|
105
162
|
|
|
106
163
|
function createTrailPoint(x, y) {
|
|
@@ -122,15 +179,16 @@
|
|
|
122
179
|
}, 400);
|
|
123
180
|
}
|
|
124
181
|
|
|
125
|
-
// Expose cursor configuration API
|
|
126
182
|
window.__agexCursor = {
|
|
183
|
+
setAnimationLock(locked) {
|
|
184
|
+
animationLock = locked;
|
|
185
|
+
},
|
|
127
186
|
setSize(size) {
|
|
128
187
|
const cursor = document.getElementById('agex-cursor');
|
|
129
188
|
if (!cursor) return;
|
|
130
|
-
const
|
|
131
|
-
const s =
|
|
132
|
-
cursor.style.
|
|
133
|
-
cursor.style.height = `${s}px`;
|
|
189
|
+
const scales = { normal: 0.75, large: 1, 'extra-large': 1.3 };
|
|
190
|
+
const s = scales[size] || scales.large;
|
|
191
|
+
cursor.style.transform = `scale(${s})`;
|
|
134
192
|
},
|
|
135
193
|
setClickRipple(enabled) {
|
|
136
194
|
clickRippleEnabled = enabled;
|
|
@@ -143,6 +201,26 @@
|
|
|
143
201
|
if (options.clickRipple !== undefined) this.setClickRipple(options.clickRipple);
|
|
144
202
|
if (options.trail !== undefined) this.setTrail(options.trail);
|
|
145
203
|
},
|
|
204
|
+
setMode(mode) {
|
|
205
|
+
const cursor = document.getElementById('agex-cursor');
|
|
206
|
+
if (!cursor) return;
|
|
207
|
+
const svgEl = cursor.querySelector('.agex-cursor-svg');
|
|
208
|
+
if (!svgEl) return;
|
|
209
|
+
if (mode === 'pointer') {
|
|
210
|
+
currentMode = 'pointer';
|
|
211
|
+
svgEl.innerHTML = POINTER_SVG;
|
|
212
|
+
} else {
|
|
213
|
+
currentMode = 'default';
|
|
214
|
+
svgEl.innerHTML = ARROW_SVG;
|
|
215
|
+
}
|
|
216
|
+
},
|
|
217
|
+
showClickRipple() {
|
|
218
|
+
const cursor = document.getElementById('agex-cursor');
|
|
219
|
+
if (!cursor) return;
|
|
220
|
+
const x = parseFloat(cursor.style.left) || 0;
|
|
221
|
+
const y = parseFloat(cursor.style.top) || 0;
|
|
222
|
+
createClickRipple(x, y);
|
|
223
|
+
},
|
|
146
224
|
sync() {
|
|
147
225
|
const cursor = document.getElementById('agex-cursor');
|
|
148
226
|
if (!cursor) return Promise.resolve('no cursor');
|
package/assets/assets/effects.js
CHANGED
|
@@ -95,6 +95,18 @@
|
|
|
95
95
|
0%, 100% { box-shadow: 0 0 20px var(--color, #ff4444), inset 0 0 20px var(--color, #ff4444); }
|
|
96
96
|
50% { box-shadow: 0 0 35px var(--color, #ff4444), inset 0 0 35px var(--color, #ff4444); }
|
|
97
97
|
}
|
|
98
|
+
.${EFFECTS_ID}-input-focus {
|
|
99
|
+
position: absolute;
|
|
100
|
+
pointer-events: none;
|
|
101
|
+
z-index: 2147483639;
|
|
102
|
+
border-radius: 6px;
|
|
103
|
+
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.5), 0 0 16px rgba(59, 130, 246, 0.3);
|
|
104
|
+
animation: ${EFFECTS_ID}-input-focus-pulse 1.5s ease-in-out infinite;
|
|
105
|
+
}
|
|
106
|
+
@keyframes ${EFFECTS_ID}-input-focus-pulse {
|
|
107
|
+
0%, 100% { box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.5), 0 0 16px rgba(59, 130, 246, 0.3); }
|
|
108
|
+
50% { box-shadow: 0 0 0 4px rgba(59, 130, 246, 0.7), 0 0 24px rgba(59, 130, 246, 0.5); }
|
|
109
|
+
}
|
|
98
110
|
.${EFFECTS_ID}-highlight.pulse {
|
|
99
111
|
animation: ${EFFECTS_ID}-pulse 1s ease-in-out infinite;
|
|
100
112
|
border: 3px solid var(--color, #ff4444);
|
|
@@ -150,17 +162,22 @@
|
|
|
150
162
|
font-family: system-ui, sans-serif;
|
|
151
163
|
transition: all 0.3s ease;
|
|
152
164
|
}
|
|
165
|
+
@keyframes ${EFFECTS_ID}-subtitle-slide-down {
|
|
166
|
+
from { opacity: 0; transform: translateX(-50%) translateY(-20px); }
|
|
167
|
+
to { opacity: 1; transform: translateX(-50%) translateY(0); }
|
|
168
|
+
}
|
|
153
169
|
.${EFFECTS_ID}-subtitle {
|
|
154
170
|
position: fixed;
|
|
155
171
|
left: 50%;
|
|
156
172
|
transform: translateX(-50%);
|
|
157
|
-
background: rgba(0, 0, 0, 0.8);
|
|
158
|
-
color: white;
|
|
173
|
+
background: var(--subtitle-bg, rgba(0, 0, 0, 0.8));
|
|
174
|
+
color: var(--subtitle-fg, white);
|
|
159
175
|
padding: 12px 24px;
|
|
160
176
|
border-radius: 8px;
|
|
161
177
|
font-size: 18px;
|
|
162
178
|
max-width: 80%;
|
|
163
179
|
text-align: center;
|
|
180
|
+
animation: ${EFFECTS_ID}-subtitle-slide-down 0.35s ease-out;
|
|
164
181
|
}
|
|
165
182
|
.${EFFECTS_ID}-drawing-canvas {
|
|
166
183
|
position: fixed;
|
|
@@ -182,6 +199,7 @@
|
|
|
182
199
|
// HIGHLIGHT
|
|
183
200
|
highlight: {
|
|
184
201
|
add(selector, { color = '#ff4444', style = 'border', duration } = {}) {
|
|
202
|
+
injectStyles();
|
|
185
203
|
const el = utils.getElement(selector);
|
|
186
204
|
if (!el) return false;
|
|
187
205
|
|
|
@@ -213,6 +231,7 @@
|
|
|
213
231
|
// ANNOTATION
|
|
214
232
|
annotation: {
|
|
215
233
|
add(selector, { text, position = 'top', color = '#333', arrow = true } = {}) {
|
|
234
|
+
injectStyles();
|
|
216
235
|
const el = utils.getElement(selector);
|
|
217
236
|
if (!el) return false;
|
|
218
237
|
|
|
@@ -247,6 +266,7 @@
|
|
|
247
266
|
// SPOTLIGHT
|
|
248
267
|
spotlight: {
|
|
249
268
|
show(selector, { opacity = 0.7, label = '' } = {}) {
|
|
269
|
+
injectStyles();
|
|
250
270
|
const el = utils.getElement(selector);
|
|
251
271
|
if (!el) return false;
|
|
252
272
|
|
|
@@ -355,10 +375,9 @@
|
|
|
355
375
|
const cursor = document.getElementById('agex-cursor');
|
|
356
376
|
if (!cursor) return;
|
|
357
377
|
|
|
358
|
-
const
|
|
359
|
-
const s =
|
|
360
|
-
cursor.style.
|
|
361
|
-
cursor.style.height = `${s}px`;
|
|
378
|
+
const scales = { normal: 0.75, large: 1, 'extra-large': 1.3 };
|
|
379
|
+
const s = scales[size] || 1;
|
|
380
|
+
cursor.style.transform = `scale(${s})`;
|
|
362
381
|
|
|
363
382
|
cursor.dataset.clickRipple = clickRipple;
|
|
364
383
|
cursor.dataset.trail = trail;
|
|
@@ -390,7 +409,7 @@
|
|
|
390
409
|
return true;
|
|
391
410
|
},
|
|
392
411
|
|
|
393
|
-
showClick(x, y, color = '
|
|
412
|
+
showClick(x, y, color = 'rgba(0, 0, 0, 0.15)') {
|
|
394
413
|
const ripple = utils.createElement('div', {
|
|
395
414
|
position: 'fixed',
|
|
396
415
|
left: `${x}px`,
|
|
@@ -457,6 +476,117 @@
|
|
|
457
476
|
await this.moveTo(points[i + 1].x, points[i + 1].y, segmentDuration);
|
|
458
477
|
}
|
|
459
478
|
return true;
|
|
479
|
+
},
|
|
480
|
+
|
|
481
|
+
_lastPos: null,
|
|
482
|
+
|
|
483
|
+
animateToPosition(targetX, targetY) {
|
|
484
|
+
const c = document.getElementById('agex-cursor');
|
|
485
|
+
if (!c) return 0;
|
|
486
|
+
|
|
487
|
+
// Lock demo cursor so native mousemove won't fight the animation
|
|
488
|
+
if (window.__agexCursor) window.__agexCursor.setAnimationLock(true);
|
|
489
|
+
|
|
490
|
+
const vh = window.innerHeight;
|
|
491
|
+
const wasHidden = c.style.opacity === '0' || c.style.opacity === '' || c.style.left === '-100px';
|
|
492
|
+
const startX = wasHidden ? (this._lastPos ? this._lastPos.x : 0) : parseFloat(c.style.left) || 0;
|
|
493
|
+
const startY = wasHidden ? (this._lastPos ? this._lastPos.y : vh / 2) : parseFloat(c.style.top) || vh / 2;
|
|
494
|
+
|
|
495
|
+
c.style.transition = 'none';
|
|
496
|
+
c.style.left = startX + 'px';
|
|
497
|
+
c.style.top = startY + 'px';
|
|
498
|
+
c.style.opacity = '1';
|
|
499
|
+
c.offsetHeight;
|
|
500
|
+
|
|
501
|
+
if (isNaN(targetX) || isNaN(targetY)) {
|
|
502
|
+
if (window.__agexCursor) window.__agexCursor.setAnimationLock(false);
|
|
503
|
+
return 0;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
this._lastPos = { x: targetX, y: targetY };
|
|
507
|
+
|
|
508
|
+
const dist = Math.sqrt((targetX - startX) ** 2 + (targetY - startY) ** 2);
|
|
509
|
+
const duration = Math.max(1200, Math.round(dist / 0.3));
|
|
510
|
+
const start = performance.now();
|
|
511
|
+
const self = this;
|
|
512
|
+
|
|
513
|
+
const step = (now) => {
|
|
514
|
+
const elapsed = now - start;
|
|
515
|
+
const t = Math.min(elapsed / duration, 1);
|
|
516
|
+
const ease = t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t;
|
|
517
|
+
c.style.left = (startX + (targetX - startX) * ease) + 'px';
|
|
518
|
+
c.style.top = (startY + (targetY - startY) * ease) + 'px';
|
|
519
|
+
if (t < 1) {
|
|
520
|
+
requestAnimationFrame(step);
|
|
521
|
+
} else {
|
|
522
|
+
// Animation done — release lock
|
|
523
|
+
if (window.__agexCursor) window.__agexCursor.setAnimationLock(false);
|
|
524
|
+
}
|
|
525
|
+
};
|
|
526
|
+
requestAnimationFrame(step);
|
|
527
|
+
return duration;
|
|
528
|
+
},
|
|
529
|
+
|
|
530
|
+
showInputFocus(selector) {
|
|
531
|
+
this.clearInputFocus();
|
|
532
|
+
var el = utils.getElement(selector);
|
|
533
|
+
if (!el) return;
|
|
534
|
+
var rect = el.getBoundingClientRect();
|
|
535
|
+
var overlay = document.createElement('div');
|
|
536
|
+
overlay.id = EFFECTS_ID + '-input-focus';
|
|
537
|
+
overlay.className = EFFECTS_ID + '-input-focus';
|
|
538
|
+
overlay.style.left = (rect.left + window.scrollX - 2) + 'px';
|
|
539
|
+
overlay.style.top = (rect.top + window.scrollY - 2) + 'px';
|
|
540
|
+
overlay.style.width = (rect.width + 4) + 'px';
|
|
541
|
+
overlay.style.height = (rect.height + 4) + 'px';
|
|
542
|
+
document.body.appendChild(overlay);
|
|
543
|
+
},
|
|
544
|
+
|
|
545
|
+
clearInputFocus() {
|
|
546
|
+
var el = document.getElementById(EFFECTS_ID + '-input-focus');
|
|
547
|
+
if (el) el.remove();
|
|
548
|
+
},
|
|
549
|
+
|
|
550
|
+
typeText(selector, text, charDelay) {
|
|
551
|
+
charDelay = charDelay || 80;
|
|
552
|
+
var el = utils.getElement(selector);
|
|
553
|
+
if (!el) return 0;
|
|
554
|
+
el.focus();
|
|
555
|
+
var i = 0;
|
|
556
|
+
var len = text.length;
|
|
557
|
+
function typeNext() {
|
|
558
|
+
if (i >= len) return;
|
|
559
|
+
var ch = text[i];
|
|
560
|
+
el.dispatchEvent(new KeyboardEvent('keydown', { key: ch, bubbles: true }));
|
|
561
|
+
el.dispatchEvent(new KeyboardEvent('keypress', { key: ch, bubbles: true }));
|
|
562
|
+
// For contenteditable elements, insert text differently
|
|
563
|
+
if (el.isContentEditable) {
|
|
564
|
+
document.execCommand('insertText', false, ch);
|
|
565
|
+
} else {
|
|
566
|
+
// Use InputEvent for proper React/framework compatibility
|
|
567
|
+
var nativeInputValueSetter = Object.getOwnPropertyDescriptor(
|
|
568
|
+
Object.getPrototypeOf(el), 'value'
|
|
569
|
+
);
|
|
570
|
+
if (nativeInputValueSetter && nativeInputValueSetter.set) {
|
|
571
|
+
nativeInputValueSetter.set.call(el, el.value + ch);
|
|
572
|
+
} else {
|
|
573
|
+
el.value = el.value + ch;
|
|
574
|
+
}
|
|
575
|
+
el.dispatchEvent(new Event('input', { bubbles: true }));
|
|
576
|
+
}
|
|
577
|
+
el.dispatchEvent(new KeyboardEvent('keyup', { key: ch, bubbles: true }));
|
|
578
|
+
i++;
|
|
579
|
+
if (i < len) setTimeout(typeNext, charDelay);
|
|
580
|
+
}
|
|
581
|
+
typeNext();
|
|
582
|
+
return len * charDelay;
|
|
583
|
+
},
|
|
584
|
+
|
|
585
|
+
fadeOut() {
|
|
586
|
+
const c = document.getElementById('agex-cursor');
|
|
587
|
+
if (!c) return;
|
|
588
|
+
c.style.transition = 'opacity 0.4s ease-out';
|
|
589
|
+
c.style.opacity = '0';
|
|
460
590
|
}
|
|
461
591
|
},
|
|
462
592
|
|
|
@@ -1289,12 +1419,15 @@
|
|
|
1289
1419
|
});
|
|
1290
1420
|
},
|
|
1291
1421
|
|
|
1292
|
-
showSubtitle(text, duration = 3000, position = 'bottom') {
|
|
1422
|
+
showSubtitle(text, duration = 3000, position = 'bottom', fg = '', bg = '') {
|
|
1423
|
+
injectStyles();
|
|
1293
1424
|
this.clear();
|
|
1294
1425
|
const subtitle = utils.createElement('div', {
|
|
1295
1426
|
[position]: '40px'
|
|
1296
1427
|
}, `${EFFECTS_ID}-subtitle`);
|
|
1297
1428
|
subtitle.classList.add(`${EFFECTS_ID}-subtitle`);
|
|
1429
|
+
if (fg) subtitle.style.setProperty('--subtitle-fg', fg);
|
|
1430
|
+
if (bg) subtitle.style.setProperty('--subtitle-bg', bg);
|
|
1298
1431
|
subtitle.textContent = text;
|
|
1299
1432
|
document.body.appendChild(subtitle);
|
|
1300
1433
|
|
|
@@ -1305,6 +1438,7 @@
|
|
|
1305
1438
|
|
|
1306
1439
|
clear() {
|
|
1307
1440
|
utils.removeElements(`#${EFFECTS_ID}-subtitle`);
|
|
1441
|
+
utils.removeElements(`.${EFFECTS_ID}-subtitle`);
|
|
1308
1442
|
if ('speechSynthesis' in window) {
|
|
1309
1443
|
speechSynthesis.cancel();
|
|
1310
1444
|
}
|
|
@@ -1499,6 +1633,14 @@
|
|
|
1499
1633
|
if (el) el.scrollIntoView({ block: 'center', behavior: 'instant' });
|
|
1500
1634
|
},
|
|
1501
1635
|
|
|
1636
|
+
scrollElementToCenter(selector) {
|
|
1637
|
+
var el = typeof selector === 'string' ? document.querySelector(selector) : selector;
|
|
1638
|
+
if (!el) return false;
|
|
1639
|
+
// scrollIntoView with block:'center' handles nested scrollable containers
|
|
1640
|
+
el.scrollIntoView({ block: 'center', inline: 'nearest', behavior: 'instant' });
|
|
1641
|
+
return true;
|
|
1642
|
+
},
|
|
1643
|
+
|
|
1502
1644
|
scrollToCoords(y, viewportHeight, elHeight) {
|
|
1503
1645
|
window.scrollTo({
|
|
1504
1646
|
top: window.scrollY + y - (viewportHeight / 2) + (elHeight / 2),
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Smooth click: animates visual cursor to element, clicks, then fades cursor out
|
|
3
|
+
# 1. If cursor not visible, starts from left-center of viewport
|
|
4
|
+
# 2. Smoothly moves visual cursor to element's center
|
|
5
|
+
# 3. Performs the actual click via agent-browser
|
|
6
|
+
# 4. Fades the cursor out
|
|
7
|
+
# Usage: ab-click <selector|@ref>
|
|
8
|
+
SELECTOR="$1"
|
|
9
|
+
if [ -z "$SELECTOR" ]; then
|
|
10
|
+
echo "Usage: ab-click <selector|@ref>"
|
|
11
|
+
exit 1
|
|
12
|
+
fi
|
|
13
|
+
|
|
14
|
+
# Scroll element into center of its container before measuring position
|
|
15
|
+
if [[ "$SELECTOR" == @* ]]; then
|
|
16
|
+
agent-browser scrollintoview "$SELECTOR" {{SESSION_ARG}} >/dev/null 2>&1
|
|
17
|
+
else
|
|
18
|
+
ESCAPED_SELECTOR=$(printf '%s' "$SELECTOR" | sed "s/'/\\\\'/g")
|
|
19
|
+
agent-browser eval "window.__agexEffects.proof.scrollElementToCenter('${ESCAPED_SELECTOR}')" {{SESSION_ARG}} >/dev/null 2>&1
|
|
20
|
+
fi
|
|
21
|
+
agent-browser wait 300 {{SESSION_ARG}} >/dev/null 2>&1
|
|
22
|
+
|
|
23
|
+
# Get element bounding box without interacting with it
|
|
24
|
+
if [[ "$SELECTOR" == @* ]]; then
|
|
25
|
+
# For @ref: use agent-browser get styles --json which returns bounding box
|
|
26
|
+
STYLES=$(agent-browser get styles "$SELECTOR" --json {{SESSION_ARG}} 2>&1) || true
|
|
27
|
+
EL_X=$(echo "$STYLES" | grep -o '"x":[0-9.-]*' | head -1 | cut -d: -f2)
|
|
28
|
+
EL_Y=$(echo "$STYLES" | grep -o '"y":[0-9.-]*' | head -1 | cut -d: -f2)
|
|
29
|
+
EL_WIDTH=$(echo "$STYLES" | grep -o '"width":[0-9.-]*' | head -1 | cut -d: -f2)
|
|
30
|
+
EL_HEIGHT=$(echo "$STYLES" | grep -o '"height":[0-9.-]*' | head -1 | cut -d: -f2)
|
|
31
|
+
|
|
32
|
+
if [ -z "$EL_X" ] || [ -z "$EL_Y" ] || [ -z "$EL_WIDTH" ] || [ -z "$EL_HEIGHT" ]; then
|
|
33
|
+
# Last resort: just click without animation
|
|
34
|
+
agent-browser click "$SELECTOR" {{SESSION_ARG}} >/dev/null 2>&1
|
|
35
|
+
echo "clicked $SELECTOR (no animation - could not get position)"
|
|
36
|
+
exit 0
|
|
37
|
+
fi
|
|
38
|
+
else
|
|
39
|
+
BOX=$(agent-browser eval "JSON.stringify(window.__agexEffects.proof.getBoundingBox('${ESCAPED_SELECTOR}'))" {{SESSION_ARG}} 2>&1) || true
|
|
40
|
+
if [ -z "$BOX" ] || [[ "$BOX" == "null" ]] || [[ "$BOX" == *"error"* ]]; then
|
|
41
|
+
echo "element not found: $SELECTOR"
|
|
42
|
+
exit 1
|
|
43
|
+
fi
|
|
44
|
+
EL_X=$(echo "$BOX" | grep -o '"x":[0-9.-]*' | cut -d: -f2)
|
|
45
|
+
EL_Y=$(echo "$BOX" | grep -o '"y":[0-9.-]*' | cut -d: -f2)
|
|
46
|
+
EL_WIDTH=$(echo "$BOX" | grep -o '"width":[0-9.-]*' | cut -d: -f2)
|
|
47
|
+
EL_HEIGHT=$(echo "$BOX" | grep -o '"height":[0-9.-]*' | cut -d: -f2)
|
|
48
|
+
fi
|
|
49
|
+
|
|
50
|
+
# Click target: element center
|
|
51
|
+
CLICK_X=$(echo "$EL_X + $EL_WIDTH / 2" | bc -l)
|
|
52
|
+
CLICK_Y=$(echo "$EL_Y + $EL_HEIGHT / 2" | bc -l)
|
|
53
|
+
|
|
54
|
+
# Cursor target: center of element
|
|
55
|
+
TARGET_X=$CLICK_X
|
|
56
|
+
TARGET_Y=$CLICK_Y
|
|
57
|
+
|
|
58
|
+
# Start animation (fire-and-forget via requestAnimationFrame) and get duration synchronously
|
|
59
|
+
ANIM_DURATION=$(agent-browser eval "'' + window.__agexEffects.cursor.animateToPosition(${TARGET_X}, ${TARGET_Y})" {{SESSION_ARG}} 2>/dev/null)
|
|
60
|
+
|
|
61
|
+
# Parse duration
|
|
62
|
+
ANIM_DURATION=$(echo "$ANIM_DURATION" | tr -d '[:space:]"')
|
|
63
|
+
if [ -z "$ANIM_DURATION" ] || [ "$ANIM_DURATION" = "0" ]; then
|
|
64
|
+
ANIM_DURATION=2000
|
|
65
|
+
fi
|
|
66
|
+
|
|
67
|
+
# Wait for animation to complete + pause so viewer can see the target
|
|
68
|
+
WAIT_MS=$((ANIM_DURATION + 300))
|
|
69
|
+
agent-browser wait "$WAIT_MS" {{SESSION_ARG}} >/dev/null 2>&1
|
|
70
|
+
agent-browser wait 1000 {{SESSION_ARG}} >/dev/null 2>&1
|
|
71
|
+
|
|
72
|
+
# Show click ripple and perform the actual click
|
|
73
|
+
agent-browser eval "window.__agexEffects.cursor.showClick(${CLICK_X}, ${CLICK_Y})" {{SESSION_ARG}} >/dev/null 2>&1
|
|
74
|
+
agent-browser click "$SELECTOR" {{SESSION_ARG}} >/dev/null 2>&1
|
|
75
|
+
|
|
76
|
+
# Sync _lastPos to the actual native click position (element center)
|
|
77
|
+
# so the next animateToPosition starts from where the native mouse really is
|
|
78
|
+
agent-browser eval "window.__agexEffects.cursor._lastPos = { x: ${CLICK_X}, y: ${CLICK_Y} }" {{SESSION_ARG}} >/dev/null 2>&1
|
|
79
|
+
|
|
80
|
+
# Fade cursor out
|
|
81
|
+
agent-browser eval "window.__agexEffects.cursor.fadeOut()" {{SESSION_ARG}} >/dev/null 2>&1
|
|
82
|
+
|
|
83
|
+
agent-browser wait 450 {{SESSION_ARG}} >/dev/null 2>&1
|
|
84
|
+
|
|
85
|
+
echo "clicked $SELECTOR"
|
|
@@ -13,11 +13,11 @@ deduplicate_video() {
|
|
|
13
13
|
|
|
14
14
|
echo "Removing duplicate frames (output fps: $fps)..."
|
|
15
15
|
# Suppress ffmpeg/ffprobe progress output (extremely verbose, confuses agent)
|
|
16
|
-
if ffmpeg -y -i "$input" -vf "mpdecimate,setpts=N/FRAME_RATE/TB" -r "$fps" -c:v libx264 "$output" 2>/dev/null; then
|
|
16
|
+
if ffmpeg -y -i "$input" -vf "mpdecimate=hi=200:lo=100:frac=0.1:max=8,setpts=N/FRAME_RATE/TB" -r "$fps" -c:v libx264 -crf 18 -preset slow -pix_fmt yuv420p -minrate 500k -maxrate 5M -bufsize 2M "$output" 2>/dev/null; then
|
|
17
17
|
local orig_frames=$(ffprobe -v error -count_frames -select_streams v:0 -show_entries stream=nb_read_frames -of default=noprint_wrappers=1:nokey=1 "$input" 2>/dev/null)
|
|
18
18
|
local new_frames=$(ffprobe -v error -count_frames -select_streams v:0 -show_entries stream=nb_read_frames -of default=noprint_wrappers=1:nokey=1 "$output" 2>/dev/null)
|
|
19
19
|
echo "Deduplicated: $orig_frames -> $new_frames frames"
|
|
20
|
-
|
|
20
|
+
# Keep raw video for debugging (don't delete $input)
|
|
21
21
|
return 0
|
|
22
22
|
else
|
|
23
23
|
echo "Warning: deduplication failed, keeping original"
|
|
@@ -97,7 +97,7 @@ else
|
|
|
97
97
|
fi
|
|
98
98
|
|
|
99
99
|
# Run ffmpeg with crossfade
|
|
100
|
-
FFMPEG_CMD="ffmpeg $INPUTS -filter_complex \"$FILTER\" $OUTPUT_MAP -y \"{{RAW_VIDEO_PATH}}\""
|
|
100
|
+
FFMPEG_CMD="ffmpeg $INPUTS -filter_complex \"$FILTER\" $OUTPUT_MAP -c:v libx264 -crf 18 -preset slow -pix_fmt yuv420p -minrate 500k -maxrate 5M -bufsize 2M -y \"{{RAW_VIDEO_PATH}}\""
|
|
101
101
|
|
|
102
102
|
# Suppress ffmpeg progress output (extremely verbose, confuses agent)
|
|
103
103
|
if eval "$FFMPEG_CMD" 2>/dev/null; then
|
|
@@ -8,8 +8,8 @@ SEGMENT_PATH="{{SEGMENTS_DIR}}/$SEGMENT_NAME"
|
|
|
8
8
|
|
|
9
9
|
agent-browser record start "$SEGMENT_PATH" {{SESSION_ARG}}
|
|
10
10
|
|
|
11
|
-
#
|
|
12
|
-
|
|
11
|
+
# Recording creates a fresh browser context — restore viewport + zoom + effects
|
|
12
|
+
agent-browser set viewport {{VIEWPORT_WIDTH}} {{VIEWPORT_HEIGHT}} {{SESSION_ARG}}
|
|
13
13
|
agent-browser eval --stdin {{SESSION_ARG}} < "{{INIT_SCRIPT_PATH}}" >/dev/null
|
|
14
14
|
|
|
15
15
|
# Wait to capture initial frame
|
|
@@ -19,8 +19,8 @@ SEGMENT_PATH="{{SEGMENTS_DIR}}/$SEGMENT_NAME"
|
|
|
19
19
|
agent-browser record start "$SEGMENT_PATH" {{SESSION_ARG}}
|
|
20
20
|
echo "Recording segment $SEGMENT_NAME: $TITLE"
|
|
21
21
|
|
|
22
|
-
#
|
|
23
|
-
|
|
22
|
+
# Recording creates a fresh browser context — restore viewport + zoom + effects
|
|
23
|
+
agent-browser set viewport {{VIEWPORT_WIDTH}} {{VIEWPORT_HEIGHT}} {{SESSION_ARG}}
|
|
24
24
|
agent-browser eval --stdin {{SESSION_ARG}} < "{{INIT_SCRIPT_PATH}}" >/dev/null
|
|
25
25
|
|
|
26
26
|
# Inject title overlay
|