@nexface/tools 0.1.15-alpha.6 → 0.1.15-alpha.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/operation-view.js +125 -34
- package/dist/styles.css +9 -6
- package/package.json +1 -1
package/dist/operation-view.js
CHANGED
|
@@ -5,6 +5,19 @@ import { useCallback, useLayoutEffect, useEffect, useRef, useState, } from "reac
|
|
|
5
5
|
import { createPortal } from "react-dom";
|
|
6
6
|
import { PageCommandError } from "./browser/locator.js";
|
|
7
7
|
import { lockOperationSurface } from "./operation-lock.js";
|
|
8
|
+
const intersect = (a, b) => {
|
|
9
|
+
if (!a)
|
|
10
|
+
return undefined;
|
|
11
|
+
const left = Math.max(a.left, b.left);
|
|
12
|
+
const top = Math.max(a.top, b.top);
|
|
13
|
+
const right = Math.min(a.left + a.width, b.left + b.width);
|
|
14
|
+
const bottom = Math.min(a.top + a.height, b.top + b.height);
|
|
15
|
+
return right > left && bottom > top
|
|
16
|
+
? { left, top, width: right - left, height: bottom - top }
|
|
17
|
+
: undefined;
|
|
18
|
+
};
|
|
19
|
+
const sameRect = (a, b) => a === b ||
|
|
20
|
+
Boolean(a && b && a.left === b.left && a.top === b.top && a.width === b.width && a.height === b.height);
|
|
8
21
|
const center = (rect) => ({
|
|
9
22
|
left: rect.left + rect.width / 2,
|
|
10
23
|
top: rect.top + rect.height / 2,
|
|
@@ -14,7 +27,7 @@ const interrupted = () => new PageCommandError({
|
|
|
14
27
|
code: "EXECUTION_CANCELLED",
|
|
15
28
|
message: "Page action presentation was interrupted.",
|
|
16
29
|
});
|
|
17
|
-
function presentPointer(pointer, target, presentation) {
|
|
30
|
+
function presentPointer(pointer, target, resolvePosition, presentation) {
|
|
18
31
|
let finished = false;
|
|
19
32
|
const finish = (error) => {
|
|
20
33
|
if (finished)
|
|
@@ -25,9 +38,13 @@ function presentPointer(pointer, target, presentation) {
|
|
|
25
38
|
presentation.signal?.removeEventListener("abort", onAbort);
|
|
26
39
|
presentation.finish(error);
|
|
27
40
|
};
|
|
28
|
-
const reached = () =>
|
|
29
|
-
|
|
30
|
-
|
|
41
|
+
const reached = () => {
|
|
42
|
+
const position = resolvePosition();
|
|
43
|
+
return Boolean(position &&
|
|
44
|
+
pointer.isConnected &&
|
|
45
|
+
target.isConnected &&
|
|
46
|
+
distance(pointer.getBoundingClientRect(), position) < 2);
|
|
47
|
+
};
|
|
31
48
|
const onTransition = (event) => {
|
|
32
49
|
if (event.target === pointer && event.propertyName === "transform" && reached())
|
|
33
50
|
finish();
|
|
@@ -49,17 +66,68 @@ function presentPointer(pointer, target, presentation) {
|
|
|
49
66
|
});
|
|
50
67
|
return () => finish(interrupted());
|
|
51
68
|
}
|
|
52
|
-
function FeedbackLabel({ feedback }) {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
69
|
+
function FeedbackLabel({ feedback, position, durationMs = 0, }) {
|
|
70
|
+
const label = useRef(null);
|
|
71
|
+
const measured = useRef(undefined);
|
|
72
|
+
const [placement, setPlacement] = useState();
|
|
73
|
+
const width = window.innerWidth;
|
|
74
|
+
const height = window.innerHeight;
|
|
75
|
+
useLayoutEffect(() => {
|
|
76
|
+
const element = label.current;
|
|
77
|
+
if (!element)
|
|
78
|
+
return;
|
|
79
|
+
const key = [feedback.label, feedback.effect, feedback.centered, width, height].join(";");
|
|
80
|
+
const resized = measured.current?.key !== key;
|
|
81
|
+
if (resized) {
|
|
82
|
+
const labelWidth = element.offsetWidth;
|
|
83
|
+
const labelHeight = element.offsetHeight;
|
|
84
|
+
measured.current = {
|
|
85
|
+
key,
|
|
86
|
+
width: labelWidth,
|
|
87
|
+
height: labelHeight,
|
|
88
|
+
left: element.offsetLeft - (feedback.centered ? labelWidth / 2 : 0),
|
|
89
|
+
top: element.offsetTop - (feedback.centered ? labelHeight / 2 : 0),
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
const size = measured.current;
|
|
93
|
+
const left = position.left + size.left;
|
|
94
|
+
const top = position.top + size.top;
|
|
95
|
+
const next = {
|
|
96
|
+
left: position.left + Math.max(12, Math.min(left, width - 12 - size.width)) - left,
|
|
97
|
+
top: position.top + Math.max(12, Math.min(top, height - 12 - size.height)) - top,
|
|
98
|
+
durationMs: resized ? 0 : durationMs,
|
|
99
|
+
};
|
|
100
|
+
setPlacement((current) => current?.left === next.left &&
|
|
101
|
+
current.top === next.top &&
|
|
102
|
+
(!resized || current.durationMs === next.durationMs)
|
|
103
|
+
? current
|
|
104
|
+
: next);
|
|
105
|
+
}, [
|
|
106
|
+
feedback.label,
|
|
107
|
+
feedback.effect,
|
|
108
|
+
feedback.centered,
|
|
109
|
+
position.left,
|
|
110
|
+
position.top,
|
|
111
|
+
width,
|
|
112
|
+
height,
|
|
113
|
+
durationMs,
|
|
114
|
+
]);
|
|
115
|
+
const current = placement ?? { ...position, durationMs: 0 };
|
|
116
|
+
return createPortal(React.createElement("div", { "aria-hidden": true, "data-nexface-ignore": true, className: "agent-operation__floating-label", style: {
|
|
117
|
+
"--agent-operation-pointer-x": `${current.left}px`,
|
|
118
|
+
"--agent-operation-pointer-y": `${current.top}px`,
|
|
119
|
+
"--agent-operation-pointer-duration": `${current.durationMs}ms`,
|
|
120
|
+
} },
|
|
121
|
+
React.createElement("span", { ref: label, style: { maxWidth: Math.max(0, Math.min(300, width * 0.48, width - 24)) }, className: "agent-operation__pointer-label", "data-browser-tool": feedback.centered ? "true" : undefined, "data-operation-effect": feedback.effect, title: feedback.label },
|
|
122
|
+
feedback.effect === "processing" ? (React.createElement("span", { className: "agent-operation__pointer-processing" })) : null,
|
|
123
|
+
feedback.effect === "read" ? (React.createElement("span", { className: "agent-operation__pointer-read-scan" },
|
|
124
|
+
React.createElement("i", null),
|
|
125
|
+
React.createElement("i", null),
|
|
126
|
+
React.createElement("i", null),
|
|
127
|
+
React.createElement("i", null),
|
|
128
|
+
React.createElement("i", null))) : null,
|
|
129
|
+
feedback.effect === "navigate" ? (React.createElement("span", { className: "agent-operation__pointer-navigation-trail" })) : null,
|
|
130
|
+
React.createElement("span", { className: "agent-operation__pointer-label-text" }, feedback.label))), document.body);
|
|
63
131
|
}
|
|
64
132
|
function FeedbackIndicator({ feedback, position, }) {
|
|
65
133
|
const [lastPosition, setLastPosition] = useState(position ?? feedback.position);
|
|
@@ -73,9 +141,9 @@ function FeedbackIndicator({ feedback, position, }) {
|
|
|
73
141
|
"--agent-operation-pointer-x": `${current.left}px`,
|
|
74
142
|
"--agent-operation-pointer-y": `${current.top}px`,
|
|
75
143
|
} },
|
|
76
|
-
React.createElement(FeedbackLabel, { feedback: feedback }))) : null;
|
|
144
|
+
React.createElement(FeedbackLabel, { feedback: feedback, position: current }))) : null;
|
|
77
145
|
}
|
|
78
|
-
function AgentPointer({ active, feedback, targetPosition, surfaceCenter, }) {
|
|
146
|
+
function AgentPointer({ active, feedback, targetPosition, surfaceCenter, resolvePosition, }) {
|
|
79
147
|
const pointer = useRef(null);
|
|
80
148
|
const [motion, setMotion] = useState();
|
|
81
149
|
const action = feedback?.effect === "action" && (!feedback.requiresTarget || Boolean(targetPosition));
|
|
@@ -124,8 +192,15 @@ function AgentPointer({ active, feedback, targetPosition, surfaceCenter, }) {
|
|
|
124
192
|
return;
|
|
125
193
|
}
|
|
126
194
|
if (show && pointer.current)
|
|
127
|
-
return presentPointer(pointer.current, feedback.target, feedback.presentation);
|
|
128
|
-
}, [
|
|
195
|
+
return presentPointer(pointer.current, feedback.target, resolvePosition, feedback.presentation);
|
|
196
|
+
}, [
|
|
197
|
+
active,
|
|
198
|
+
show,
|
|
199
|
+
feedback?.presentation,
|
|
200
|
+
feedback?.target,
|
|
201
|
+
Boolean(targetPosition),
|
|
202
|
+
resolvePosition,
|
|
203
|
+
]);
|
|
129
204
|
return show && motion ? (React.createElement("div", { "aria-hidden": true, ref: pointer, className: "agent-operation__pointer-effect", "data-active": action, "data-operation-effect": action ? "action" : undefined, style: {
|
|
130
205
|
"--agent-operation-pointer-duration": `${motion.durationMs}ms`,
|
|
131
206
|
"--agent-operation-pointer-x": `${motion.position.left}px`,
|
|
@@ -133,10 +208,11 @@ function AgentPointer({ active, feedback, targetPosition, surfaceCenter, }) {
|
|
|
133
208
|
} },
|
|
134
209
|
React.createElement("span", { className: "agent-operation__pointer-cursor" },
|
|
135
210
|
React.createElement(Cursor, { className: "agent-operation__pointer", size: 24, weight: "fill" })),
|
|
136
|
-
action && feedback ? React.createElement(FeedbackLabel, { feedback: feedback }) : null)) : null;
|
|
211
|
+
action && feedback ? (React.createElement(FeedbackLabel, { feedback: feedback, position: motion.position, durationMs: motion.durationMs })) : null)) : null;
|
|
137
212
|
}
|
|
138
213
|
export function OperationOverlayView({ operation, feedback, progressHistory = [], running, humanHasControl = false, cancelling = false, onCancel: cancel, resolveSurface, }) {
|
|
139
|
-
const [, setGeometry] = useState(
|
|
214
|
+
const [geometry, setGeometry] = useState();
|
|
215
|
+
const geometryCache = useRef(undefined);
|
|
140
216
|
const [historyOpen, setHistoryOpen] = useState(false);
|
|
141
217
|
const [historyId] = useState(() => `nexface-operation-history-${globalThis.crypto?.randomUUID?.() ?? `${Date.now()}-${Math.random()}`}`);
|
|
142
218
|
const controllerShouldBeVisible = operation.status !== "cancelled" &&
|
|
@@ -168,6 +244,7 @@ export function OperationOverlayView({ operation, feedback, progressHistory = []
|
|
|
168
244
|
const resolvedTarget = feedback?.resolveTarget ? feedback.resolveTarget() : feedback?.target;
|
|
169
245
|
const target = resolvedTarget?.isConnected && surface?.contains(resolvedTarget) ? resolvedTarget : undefined;
|
|
170
246
|
return {
|
|
247
|
+
viewport: { left: 0, top: 0, width: window.innerWidth, height: window.innerHeight },
|
|
171
248
|
surfaceRect: surface?.isConnected ? surface.getBoundingClientRect() : undefined,
|
|
172
249
|
target,
|
|
173
250
|
targetRect: target?.getBoundingClientRect(),
|
|
@@ -175,15 +252,17 @@ export function OperationOverlayView({ operation, feedback, progressHistory = []
|
|
|
175
252
|
}, [resolveSurface, feedback?.target, feedback?.resolveTarget]);
|
|
176
253
|
useLayoutEffect(() => {
|
|
177
254
|
const update = () => {
|
|
178
|
-
const
|
|
179
|
-
const
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
255
|
+
const next = readGeometry();
|
|
256
|
+
const previous = geometryCache.current;
|
|
257
|
+
if (previous?.source === readGeometry &&
|
|
258
|
+
previous.target === next.target &&
|
|
259
|
+
sameRect(previous.surfaceRect, next.surfaceRect) &&
|
|
260
|
+
sameRect(previous.targetRect, next.targetRect) &&
|
|
261
|
+
sameRect(previous.viewport, next.viewport))
|
|
262
|
+
return;
|
|
263
|
+
const measured = { ...next, source: readGeometry };
|
|
264
|
+
geometryCache.current = measured;
|
|
265
|
+
setGeometry(measured);
|
|
187
266
|
};
|
|
188
267
|
update();
|
|
189
268
|
if (!running && !humanHasControl)
|
|
@@ -194,11 +273,23 @@ export function OperationOverlayView({ operation, feedback, progressHistory = []
|
|
|
194
273
|
});
|
|
195
274
|
return () => cancelAnimationFrame(frame);
|
|
196
275
|
}, [readGeometry, operation, running, humanHasControl]);
|
|
276
|
+
const resolvePosition = useCallback(() => {
|
|
277
|
+
const current = readGeometry();
|
|
278
|
+
const surface = intersect(current.surfaceRect, current.viewport);
|
|
279
|
+
const visible = surface ? intersect(current.targetRect, surface) : undefined;
|
|
280
|
+
return visible ? center(visible) : undefined;
|
|
281
|
+
}, [readGeometry]);
|
|
197
282
|
if (typeof document === "undefined")
|
|
198
283
|
return null;
|
|
199
|
-
const
|
|
284
|
+
const measured = geometry?.source === readGeometry ? geometry : readGeometry();
|
|
285
|
+
const surfaceRect = intersect(measured.surfaceRect, measured.viewport);
|
|
286
|
+
const { target } = measured;
|
|
287
|
+
const rect = surfaceRect ? intersect(measured.targetRect, surfaceRect) : undefined;
|
|
288
|
+
const targetInvisible = Boolean(target && !rect);
|
|
200
289
|
const structuredTargetLost = Boolean(feedback?.requiresTarget && !target);
|
|
201
|
-
const activeFeedback = !cancelling && !humanHasControl && !structuredTargetLost
|
|
290
|
+
const activeFeedback = !cancelling && !humanHasControl && !structuredTargetLost && !targetInvisible && surfaceRect
|
|
291
|
+
? feedback
|
|
292
|
+
: undefined;
|
|
202
293
|
const effect = activeFeedback?.effect;
|
|
203
294
|
const highlightedRect = effect === "navigate" || activeFeedback?.centered ? undefined : rect;
|
|
204
295
|
const operationFeedbackActive = Boolean(activeFeedback);
|
|
@@ -236,9 +327,9 @@ export function OperationOverlayView({ operation, feedback, progressHistory = []
|
|
|
236
327
|
top: highlightedRect.top,
|
|
237
328
|
width: highlightedRect.width,
|
|
238
329
|
} })) : null,
|
|
239
|
-
React.createElement(AgentPointer, { active: agentControlsSurface && !cancelling, feedback: feedback?.presentation ? feedback : activeFeedback, targetPosition: targetPosition, surfaceCenter: surfaceRect ? center(surfaceRect) : undefined }),
|
|
330
|
+
React.createElement(AgentPointer, { active: agentControlsSurface && !cancelling && Boolean(surfaceRect) && !targetInvisible, feedback: feedback?.presentation ? feedback : activeFeedback, targetPosition: targetPosition, surfaceCenter: surfaceRect ? center(surfaceRect) : undefined, resolvePosition: resolvePosition }),
|
|
240
331
|
activeFeedback && effect !== "action" && surfaceRect ? (React.createElement(FeedbackIndicator, { key: activeFeedback.id, feedback: activeFeedback, position: activeFeedback.centered ? center(surfaceRect) : targetPosition })) : null,
|
|
241
|
-
controllerMounted ? (React.createElement("section", { "aria-live": "polite", className: "agent-operation__controller", "data-visibility": controllerExiting ? "exiting" : "visible", style: surfaceRect
|
|
332
|
+
controllerMounted ? (React.createElement("section", { "aria-live": "polite", className: "agent-operation__controller", "data-visibility": controllerExiting ? "exiting" : "visible", style: surfaceRect && surfaceRect.width >= 240
|
|
242
333
|
? {
|
|
243
334
|
left: surfaceRect.left + surfaceRect.width / 2,
|
|
244
335
|
right: "auto",
|
package/dist/styles.css
CHANGED
|
@@ -108,13 +108,10 @@
|
|
|
108
108
|
0 0 0 4px rgba(99, 102, 241, 0.16),
|
|
109
109
|
0 0 22px rgba(34, 211, 238, 0.34),
|
|
110
110
|
0 0 48px rgba(79, 70, 229, 0.2);
|
|
111
|
-
transition:
|
|
112
|
-
inset 0.12s ease,
|
|
113
|
-
width 0.12s ease,
|
|
114
|
-
height 0.12s ease;
|
|
115
111
|
animation: agent-operation-pulse 2.6s ease-in-out infinite;
|
|
116
112
|
}
|
|
117
|
-
.agent-operation__pointer-effect
|
|
113
|
+
.agent-operation__pointer-effect,
|
|
114
|
+
.agent-operation__floating-label {
|
|
118
115
|
position: fixed;
|
|
119
116
|
z-index: 2;
|
|
120
117
|
top: 0;
|
|
@@ -129,6 +126,9 @@
|
|
|
129
126
|
opacity 0.12s ease-out;
|
|
130
127
|
will-change: transform, opacity;
|
|
131
128
|
}
|
|
129
|
+
.agent-operation__floating-label {
|
|
130
|
+
z-index: 10001;
|
|
131
|
+
}
|
|
132
132
|
.agent-operation__pointer-cursor {
|
|
133
133
|
position: absolute;
|
|
134
134
|
z-index: 1;
|
|
@@ -152,7 +152,8 @@
|
|
|
152
152
|
top: 27px;
|
|
153
153
|
left: 19px;
|
|
154
154
|
display: flex;
|
|
155
|
-
max-width: min(300px, 48vw);
|
|
155
|
+
max-width: min(300px, 48vw, calc(100vw - 24px));
|
|
156
|
+
box-sizing: border-box;
|
|
156
157
|
overflow: hidden;
|
|
157
158
|
align-items: center;
|
|
158
159
|
gap: 9px;
|
|
@@ -188,6 +189,7 @@
|
|
|
188
189
|
transform: translate(-50%, -50%);
|
|
189
190
|
}
|
|
190
191
|
.agent-operation__pointer-label-text {
|
|
192
|
+
min-width: 0;
|
|
191
193
|
overflow: hidden;
|
|
192
194
|
text-overflow: ellipsis;
|
|
193
195
|
}
|
|
@@ -196,6 +198,7 @@
|
|
|
196
198
|
[data-operation-effect="navigate"]
|
|
197
199
|
)
|
|
198
200
|
.agent-operation__pointer-label-text {
|
|
201
|
+
min-width: 0;
|
|
199
202
|
display: flex;
|
|
200
203
|
box-sizing: border-box;
|
|
201
204
|
height: 32px;
|