@aixnn/aixautots 0.0.0-stage → 0.5.3

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.
@@ -0,0 +1,1250 @@
1
+ import { useAixT } from '../chunk-64Q7BBIQ.js';
2
+ import { Select, Button, cn, Input, Dialog } from '../chunk-EVHTAXQG.js';
3
+ import { useInspectorView, useDevicesView, useLocators } from '../chunk-O34SNDQB.js';
4
+ import '../chunk-V4MXFPDX.js';
5
+ import '../chunk-IJVNII5N.js';
6
+ import { useClient } from '../chunk-UINEPEIE.js';
7
+ import '../chunk-OXIM6EYO.js';
8
+ import '../chunk-TATL3HTE.js';
9
+ import '../chunk-NWFYTFMI.js';
10
+ import { useState, useMemo, useEffect, useCallback, useRef } from 'react';
11
+ import { Upload, RefreshCw, ChevronsUp, ChevronsDown, MousePointer2, Circle, Square, Search, Laptop, Smartphone, Check, Copy, Crosshair, TimerReset, Eraser, ImageDown, Keyboard, Save, Trash2, X, ChevronDown, ChevronRight, ArrowLeft, Home, SquareStack, CornerDownLeft, Delete } from 'lucide-react';
12
+ import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
13
+
14
+ function ScreenshotPanel({
15
+ screenshot,
16
+ selectedElement,
17
+ loading,
18
+ deviceName,
19
+ platform,
20
+ interactMode = false,
21
+ matches = [],
22
+ onCoordinateClick
23
+ }) {
24
+ const imgRef = useRef(null);
25
+ const [scale, setScale] = useState(1);
26
+ const syncScale = useCallback(() => {
27
+ const img = imgRef.current;
28
+ if (img && img.naturalWidth > 0) {
29
+ setScale(img.clientWidth / img.naturalWidth);
30
+ }
31
+ }, []);
32
+ useEffect(() => {
33
+ const img = imgRef.current;
34
+ if (!img) return;
35
+ const ro = new ResizeObserver(syncScale);
36
+ ro.observe(img);
37
+ return () => ro.disconnect();
38
+ }, [syncScale, screenshot]);
39
+ const isDesktop = platform === "macos" || platform === "windows";
40
+ const DeviceIcon = isDesktop ? Laptop : Smartphone;
41
+ return /* @__PURE__ */ jsxs("div", { className: "flex flex-col bg-secondary/30 overflow-hidden", children: [
42
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-3 px-4 h-11 border-b border-border bg-background/40 flex-shrink-0", children: [
43
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 text-muted-foreground", children: [
44
+ /* @__PURE__ */ jsx(DeviceIcon, { className: "w-4 h-4" }),
45
+ /* @__PURE__ */ jsx("span", { className: "text-body-sm font-medium text-foreground", children: deviceName ?? "Device Mirror" })
46
+ ] }),
47
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
48
+ matches.length > 0 && /* @__PURE__ */ jsxs("span", { className: "px-1.5 py-0.5 rounded-full bg-warning/15 text-warning text-[10px] font-medium", children: [
49
+ matches.length,
50
+ " match",
51
+ matches.length === 1 ? "" : "es"
52
+ ] }),
53
+ interactMode && /* @__PURE__ */ jsx("span", { className: "px-1.5 py-0.5 rounded-full bg-primary/15 text-primary text-[10px] font-medium", children: "Interact" }),
54
+ screenshot && imgRef.current && /* @__PURE__ */ jsxs("span", { className: "text-[11px] font-mono text-muted-foreground", children: [
55
+ imgRef.current.naturalWidth,
56
+ "\xD7",
57
+ imgRef.current.naturalHeight,
58
+ " \xB7",
59
+ " ",
60
+ Math.round(scale * 100),
61
+ "%"
62
+ ] })
63
+ ] })
64
+ ] }),
65
+ /* @__PURE__ */ jsx("div", { className: "flex-1 flex items-center justify-center p-8 overflow-auto", children: screenshot ? /* @__PURE__ */ jsx(
66
+ "div",
67
+ {
68
+ className: cn(
69
+ "relative bg-foreground shadow-xl ring-1 ring-border",
70
+ isDesktop ? "rounded-lg p-1.5" : "rounded-[2.25rem] p-2"
71
+ ),
72
+ children: /* @__PURE__ */ jsxs(
73
+ "div",
74
+ {
75
+ className: cn(
76
+ "relative overflow-hidden bg-surface cursor-crosshair",
77
+ isDesktop ? "rounded-md" : "rounded-[1.75rem]"
78
+ ),
79
+ onClick: (e) => {
80
+ const img = imgRef.current;
81
+ if (!img || scale <= 0) return;
82
+ const rect = img.getBoundingClientRect();
83
+ onCoordinateClick(
84
+ (e.clientX - rect.left) / scale,
85
+ (e.clientY - rect.top) / scale
86
+ );
87
+ },
88
+ children: [
89
+ /* @__PURE__ */ jsx(
90
+ "img",
91
+ {
92
+ ref: imgRef,
93
+ src: screenshot,
94
+ alt: "Device screenshot",
95
+ className: "block max-h-[70vh] w-auto",
96
+ onLoad: syncScale
97
+ }
98
+ ),
99
+ matches.map((m, i) => /* @__PURE__ */ jsx(
100
+ "div",
101
+ {
102
+ className: "absolute border border-warning bg-warning/15 pointer-events-none",
103
+ style: {
104
+ left: m.rect.x * scale,
105
+ top: m.rect.y * scale,
106
+ width: m.rect.width * scale,
107
+ height: m.rect.height * scale
108
+ }
109
+ },
110
+ i
111
+ )),
112
+ selectedElement && /* @__PURE__ */ jsx(
113
+ "div",
114
+ {
115
+ className: "absolute border-2 border-primary bg-primary/10 pointer-events-none",
116
+ style: {
117
+ left: selectedElement.rect.x * scale,
118
+ top: selectedElement.rect.y * scale,
119
+ width: selectedElement.rect.width * scale,
120
+ height: selectedElement.rect.height * scale
121
+ },
122
+ children: /* @__PURE__ */ jsxs("span", { className: "absolute -top-5 left-0 px-1.5 py-px rounded-sm bg-primary text-primary-foreground text-[10px] leading-none font-mono whitespace-nowrap", children: [
123
+ selectedElement.class_name ?? "node",
124
+ " [",
125
+ Math.round(selectedElement.rect.x),
126
+ ", ",
127
+ Math.round(selectedElement.rect.y),
128
+ "]"
129
+ ] })
130
+ }
131
+ )
132
+ ]
133
+ }
134
+ )
135
+ }
136
+ ) : /* @__PURE__ */ jsxs(
137
+ "div",
138
+ {
139
+ className: cn(
140
+ "flex flex-col items-center gap-2 text-muted-foreground",
141
+ loading && "animate-pulse"
142
+ ),
143
+ children: [
144
+ /* @__PURE__ */ jsx(DeviceIcon, { className: "w-8 h-8 opacity-40" }),
145
+ /* @__PURE__ */ jsx("p", { className: "text-body-sm", children: loading ? "Capturing screen..." : deviceName ? "No screenshot available" : "Select a device to start inspecting" })
146
+ ]
147
+ }
148
+ ) })
149
+ ] });
150
+ }
151
+ function nodeTag(node) {
152
+ return node.class_name ?? "node";
153
+ }
154
+ function nodeLabel(node) {
155
+ const attrs = [];
156
+ if (node.label) attrs.push(`label="${node.label}"`);
157
+ if (node.text) attrs.push(`"${node.text.slice(0, 24)}"`);
158
+ return `<${nodeTag(node)}${attrs.length ? " " + attrs.join(" ") : ""}>`;
159
+ }
160
+ function matchesFilter(node, query) {
161
+ if (!query) return true;
162
+ const haystack = `${nodeTag(node)} ${node.label ?? ""} ${node.text ?? ""} ${node.resource_id ?? ""}`.toLowerCase();
163
+ if (haystack.includes(query)) return true;
164
+ return (node.children ?? []).some((c) => matchesFilter(c, query));
165
+ }
166
+ function TreeNode({
167
+ node,
168
+ depth,
169
+ selectedElement,
170
+ onSelect,
171
+ query
172
+ }) {
173
+ const [expanded, setExpanded] = useState(true);
174
+ const isSelected = selectedElement?.xpath === node.xpath;
175
+ const hasChildren = (node.children?.length ?? 0) > 0;
176
+ if (!matchesFilter(node, query)) return null;
177
+ return /* @__PURE__ */ jsxs("div", { children: [
178
+ /* @__PURE__ */ jsxs(
179
+ "div",
180
+ {
181
+ className: cn(
182
+ "group flex items-center gap-1 rounded-sm pr-1 transition-colors",
183
+ isSelected ? "bg-primary/15 text-primary" : "text-muted-foreground hover:bg-secondary"
184
+ ),
185
+ style: { paddingLeft: `${depth * 14 + 4}px` },
186
+ children: [
187
+ hasChildren ? /* @__PURE__ */ jsx(
188
+ "button",
189
+ {
190
+ onClick: () => setExpanded((v) => !v),
191
+ className: "p-0.5 rounded-sm text-muted-foreground hover:text-foreground transition-colors flex-shrink-0",
192
+ "aria-label": expanded ? "Collapse node" : "Expand node",
193
+ children: expanded ? /* @__PURE__ */ jsx(ChevronDown, { className: "w-3.5 h-3.5" }) : /* @__PURE__ */ jsx(ChevronRight, { className: "w-3.5 h-3.5" })
194
+ }
195
+ ) : /* @__PURE__ */ jsx("span", { className: "w-[1.125rem] flex-shrink-0" }),
196
+ /* @__PURE__ */ jsx(
197
+ "button",
198
+ {
199
+ onClick: () => onSelect(node),
200
+ className: cn(
201
+ "flex-1 min-w-0 text-left py-1 text-code-sm font-mono truncate",
202
+ isSelected ? "font-semibold text-primary" : "text-success"
203
+ ),
204
+ title: nodeLabel(node),
205
+ children: nodeLabel(node)
206
+ }
207
+ )
208
+ ]
209
+ }
210
+ ),
211
+ expanded && node.children?.map((child) => /* @__PURE__ */ jsx(
212
+ TreeNode,
213
+ {
214
+ node: child,
215
+ depth: depth + 1,
216
+ selectedElement,
217
+ onSelect,
218
+ query
219
+ },
220
+ child.xpath
221
+ ))
222
+ ] });
223
+ }
224
+ function UITreePanel({ tree, selectedElement, onSelectElement }) {
225
+ const [query, setQuery] = useState("");
226
+ const normalized = useMemo(() => query.trim().toLowerCase(), [query]);
227
+ return /* @__PURE__ */ jsxs("div", { className: "flex flex-col border-r border-border bg-surface overflow-hidden", children: [
228
+ /* @__PURE__ */ jsx("div", { className: "flex items-center justify-between gap-3 px-4 h-11 border-b border-border bg-background/40 flex-shrink-0", children: /* @__PURE__ */ jsx("span", { className: "text-label-caps text-muted-foreground", children: "UI Hierarchy" }) }),
229
+ /* @__PURE__ */ jsx("div", { className: "px-3 py-2 border-b border-border flex-shrink-0", children: /* @__PURE__ */ jsx(
230
+ Input,
231
+ {
232
+ value: query,
233
+ onChange: (e) => setQuery(e.target.value),
234
+ placeholder: "Filter nodes...",
235
+ icon: /* @__PURE__ */ jsx(Search, { className: "w-3.5 h-3.5" }),
236
+ className: "h-8 py-1"
237
+ }
238
+ ) }),
239
+ /* @__PURE__ */ jsx("div", { className: "flex-1 overflow-auto px-2 py-2", children: tree ? /* @__PURE__ */ jsx(
240
+ TreeNode,
241
+ {
242
+ node: tree,
243
+ depth: 0,
244
+ selectedElement,
245
+ onSelect: onSelectElement,
246
+ query: normalized
247
+ }
248
+ ) : /* @__PURE__ */ jsx("p", { className: "text-code-sm text-muted-foreground px-2 py-1.5", children: "No UI tree data" }) })
249
+ ] });
250
+ }
251
+
252
+ // src/ui/inspector/inspectorLocators.ts
253
+ var XPATH = { value: "xpath", label: "XPath" };
254
+ var STRATEGY_OPTIONS = {
255
+ android: [
256
+ { value: "resource_id", label: "Resource ID" },
257
+ { value: "content_desc", label: "Content Desc (a11y)" },
258
+ { value: "text", label: "Text" },
259
+ { value: "text_contains", label: "Text Contains" },
260
+ { value: "class_name", label: "Class Name" },
261
+ XPATH
262
+ ],
263
+ ios: [
264
+ { value: "label", label: "Accessibility ID / Label" },
265
+ { value: "text", label: "Value" },
266
+ { value: "class_name", label: "Element Type" },
267
+ { value: "predicate", label: "Predicate (NSPredicate)" },
268
+ XPATH
269
+ ],
270
+ windows: [
271
+ { value: "automation_id", label: "Automation ID" },
272
+ { value: "label", label: "Name" },
273
+ { value: "control_type", label: "Control Type" },
274
+ { value: "class_name", label: "Class Name" },
275
+ XPATH
276
+ ],
277
+ macos: [
278
+ { value: "automation_id", label: "AXIdentifier" },
279
+ { value: "label", label: "AXTitle / Label" },
280
+ { value: "role", label: "AXRole" },
281
+ { value: "class_name", label: "Class Name" },
282
+ XPATH
283
+ ]
284
+ };
285
+ var GENERIC_STRATEGY_OPTIONS = [
286
+ XPATH,
287
+ { value: "resource_id", label: "Resource ID" },
288
+ { value: "label", label: "Label" },
289
+ { value: "text", label: "Text" },
290
+ { value: "class_name", label: "Class Name" }
291
+ ];
292
+ function strategyOptionsFor(platform) {
293
+ return platform && STRATEGY_OPTIONS[platform] || GENERIC_STRATEGY_OPTIONS;
294
+ }
295
+ function strategyValue(el, strategy) {
296
+ switch (strategy) {
297
+ case "resource_id":
298
+ return el.resource_id;
299
+ case "content_desc":
300
+ return el.attributes["content_desc"] ?? el.label;
301
+ case "label":
302
+ return el.label ?? el.resource_id;
303
+ case "text":
304
+ case "text_contains":
305
+ return el.text;
306
+ case "class_name":
307
+ return el.class_name;
308
+ case "automation_id":
309
+ return el.attributes["automation_id"] ?? null;
310
+ case "control_type":
311
+ return el.attributes["control_type"] ?? null;
312
+ case "role":
313
+ return el.attributes["role"] ?? null;
314
+ case "xpath":
315
+ return el.xpath;
316
+ default:
317
+ return null;
318
+ }
319
+ }
320
+ function suggestLocators(el, platform) {
321
+ return strategyOptionsFor(platform).filter((o) => o.value !== "predicate").flatMap((o) => {
322
+ const value = strategyValue(el, o.value);
323
+ return value ? [{ strategy: o.value, value }] : [];
324
+ });
325
+ }
326
+ function cropElementImage(screenshot, rect) {
327
+ return new Promise((resolve, reject) => {
328
+ const img = new Image();
329
+ img.onload = () => {
330
+ const w = Math.max(1, Math.round(rect.width));
331
+ const h = Math.max(1, Math.round(rect.height));
332
+ const canvas = document.createElement("canvas");
333
+ canvas.width = w;
334
+ canvas.height = h;
335
+ const ctx = canvas.getContext("2d");
336
+ if (!ctx) {
337
+ reject(new Error("canvas 2d context unavailable"));
338
+ return;
339
+ }
340
+ ctx.drawImage(img, rect.x, rect.y, w, h, 0, 0, w, h);
341
+ resolve(canvas.toDataURL("image/png"));
342
+ };
343
+ img.onerror = () => reject(new Error("screenshot decode failed"));
344
+ img.src = screenshot;
345
+ });
346
+ }
347
+ function downloadDataUrl(dataUrl, filename) {
348
+ const a = document.createElement("a");
349
+ a.href = dataUrl;
350
+ a.download = filename;
351
+ document.body.appendChild(a);
352
+ a.click();
353
+ a.remove();
354
+ }
355
+ function bestLocator(el, platform) {
356
+ return suggestLocators(el, platform)[0] ?? { strategy: "xpath", value: el.xpath };
357
+ }
358
+ function tapLine(el, platform) {
359
+ return `await device.tapElement(${JSON.stringify(bestLocator(el, platform))});`;
360
+ }
361
+ function longPressLine(el, _platform, durationSecs = 1) {
362
+ const cx = Math.round(el.rect.x + el.rect.width / 2);
363
+ const cy = Math.round(el.rect.y + el.rect.height / 2);
364
+ return `await device.longPress(${cx}, ${cy}, ${durationSecs});`;
365
+ }
366
+ function inputTextLine(el, platform, text) {
367
+ return `await device.tapElement(${JSON.stringify(bestLocator(el, platform))});
368
+ await device.inputText(${JSON.stringify(text)});`;
369
+ }
370
+ function clearTextLine(el, platform) {
371
+ return `await device.clearText(${JSON.stringify(bestLocator(el, platform))});`;
372
+ }
373
+ function pressKeyLine(key) {
374
+ return `await device.pressKey(${JSON.stringify(key)});`;
375
+ }
376
+ function swipeLine(fromX, fromY, toX, toY, duration = 0.3) {
377
+ return `await device.swipe(${Math.round(fromX)}, ${Math.round(fromY)}, ${Math.round(toX)}, ${Math.round(toY)}, ${duration});`;
378
+ }
379
+ function VerifyLocatorSection({ platform, seed }) {
380
+ const { verify, clearMatches, matches, verifying, verifyLocator } = useInspectorView();
381
+ const options = strategyOptionsFor(platform);
382
+ const [strategy, setStrategy] = useState(options[0]?.value ?? "xpath");
383
+ const [value, setValue] = useState("");
384
+ useEffect(() => {
385
+ if (seed) {
386
+ setStrategy(seed.strategy);
387
+ setValue(seed.value);
388
+ void verify(seed.strategy, seed.value);
389
+ }
390
+ }, [seed]);
391
+ const run = () => void verify(strategy, value);
392
+ const hasResult = verifyLocator !== null;
393
+ return /* @__PURE__ */ jsxs("section", { className: "px-4 py-4 border-b border-border", children: [
394
+ /* @__PURE__ */ jsx("h3", { className: "text-label-caps text-muted-foreground mb-2", children: "Verify Locator" }),
395
+ /* @__PURE__ */ jsxs("div", { className: "space-y-1.5", children: [
396
+ /* @__PURE__ */ jsx(Select, { value: strategy, onChange: setStrategy, options }),
397
+ /* @__PURE__ */ jsxs("div", { className: "flex items-stretch gap-1.5", children: [
398
+ /* @__PURE__ */ jsx(
399
+ Input,
400
+ {
401
+ value,
402
+ onChange: (e) => setValue(e.target.value),
403
+ onKeyDown: (e) => e.key === "Enter" && run(),
404
+ placeholder: "Locator value",
405
+ className: "flex-1 h-8 py-1 font-mono"
406
+ }
407
+ ),
408
+ /* @__PURE__ */ jsx(
409
+ "button",
410
+ {
411
+ onClick: run,
412
+ disabled: verifying || !value.trim(),
413
+ className: "flex-shrink-0 grid place-items-center w-9 rounded-md bg-primary text-primary-foreground hover:bg-primary/90 transition-colors disabled:opacity-50",
414
+ "aria-label": "Verify locator",
415
+ children: /* @__PURE__ */ jsx(Search, { className: "w-3.5 h-3.5" })
416
+ }
417
+ ),
418
+ hasResult && /* @__PURE__ */ jsx(
419
+ "button",
420
+ {
421
+ onClick: clearMatches,
422
+ className: "flex-shrink-0 grid place-items-center w-9 rounded-md bg-secondary border border-border text-muted-foreground hover:text-foreground transition-colors",
423
+ "aria-label": "Clear matches",
424
+ children: /* @__PURE__ */ jsx(X, { className: "w-3.5 h-3.5" })
425
+ }
426
+ )
427
+ ] })
428
+ ] }),
429
+ hasResult && /* @__PURE__ */ jsx(
430
+ "p",
431
+ {
432
+ className: `mt-2 text-code-sm ${matches.length > 0 ? "text-success" : "text-destructive"}`,
433
+ children: verifying ? "Searching\u2026" : matches.length > 0 ? `${matches.length} element${matches.length === 1 ? "" : "s"} matched \xB7 highlighted on screen` : "No elements matched"
434
+ }
435
+ )
436
+ ] });
437
+ }
438
+ function PanelShell({ children }) {
439
+ return /* @__PURE__ */ jsxs("div", { className: "flex flex-col border-l border-border bg-surface overflow-hidden", children: [
440
+ /* @__PURE__ */ jsx("div", { className: "flex items-center px-4 h-11 border-b border-border bg-background/40 flex-shrink-0", children: /* @__PURE__ */ jsx("span", { className: "text-label-caps text-muted-foreground", children: "Properties" }) }),
441
+ children
442
+ ] });
443
+ }
444
+ function ElementDetailsPanel({
445
+ element,
446
+ platform,
447
+ screenshot,
448
+ onSaveLocator,
449
+ onTap,
450
+ onLongPress,
451
+ onInputText,
452
+ onClearText
453
+ }) {
454
+ const [copiedKey, setCopiedKey] = useState(null);
455
+ const [captureError, setCaptureError] = useState(null);
456
+ const [typeText, setTypeText] = useState("");
457
+ const [verifySeed, setVerifySeed] = useState(null);
458
+ useEffect(() => setTypeText(""), [element?.xpath]);
459
+ if (!element) {
460
+ return /* @__PURE__ */ jsxs(PanelShell, { children: [
461
+ /* @__PURE__ */ jsx(VerifyLocatorSection, { platform, seed: verifySeed }),
462
+ /* @__PURE__ */ jsx("div", { className: "flex-1 flex items-center justify-center p-6", children: /* @__PURE__ */ jsx("p", { className: "text-body-sm text-muted-foreground text-center", children: "Select an element to inspect its properties" }) })
463
+ ] });
464
+ }
465
+ const attrs = Object.entries(element.attributes);
466
+ const suggestions = suggestLocators(element, platform);
467
+ const strategyLabel = (s) => strategyOptionsFor(platform).find((o) => o.value === s)?.label ?? s;
468
+ const copy = async (key, value) => {
469
+ try {
470
+ await navigator.clipboard.writeText(value);
471
+ setCopiedKey(key);
472
+ setTimeout(() => setCopiedKey(null), 1500);
473
+ } catch {
474
+ }
475
+ };
476
+ const handleCaptureImage = async () => {
477
+ if (!screenshot) return;
478
+ setCaptureError(null);
479
+ try {
480
+ const dataUrl = await cropElementImage(screenshot, element.rect);
481
+ const base = element.resource_id?.split("/").pop() || element.label || element.class_name?.split(".").pop() || "element";
482
+ downloadDataUrl(dataUrl, `${base.replace(/[^\w.-]+/g, "_")}.png`);
483
+ } catch (err) {
484
+ setCaptureError(String(err));
485
+ }
486
+ };
487
+ return /* @__PURE__ */ jsx(PanelShell, { children: /* @__PURE__ */ jsxs("div", { className: "flex-1 overflow-auto", children: [
488
+ /* @__PURE__ */ jsx(VerifyLocatorSection, { platform, seed: verifySeed }),
489
+ /* @__PURE__ */ jsxs("section", { className: "px-4 py-4 border-b border-border", children: [
490
+ /* @__PURE__ */ jsxs("h3", { className: "text-label-caps text-muted-foreground mb-2", children: [
491
+ "Suggested Locators",
492
+ platform ? ` \xB7 ${platform}` : ""
493
+ ] }),
494
+ /* @__PURE__ */ jsx("div", { className: "space-y-1.5", children: suggestions.map((s) => /* @__PURE__ */ jsxs("div", { className: "flex items-stretch gap-1.5", children: [
495
+ /* @__PURE__ */ jsx("span", { className: "flex-shrink-0 grid place-items-center px-2 rounded-md bg-secondary/60 border border-border text-[10px] font-medium text-muted-foreground whitespace-nowrap", children: strategyLabel(s.strategy) }),
496
+ /* @__PURE__ */ jsx("code", { className: "flex-1 min-w-0 px-2 py-1.5 rounded-md bg-secondary border border-border text-code-sm font-mono text-primary break-all", children: s.value }),
497
+ /* @__PURE__ */ jsx(
498
+ "button",
499
+ {
500
+ onClick: () => setVerifySeed({ strategy: s.strategy, value: s.value }),
501
+ className: "flex-shrink-0 grid place-items-center w-8 rounded-md bg-secondary border border-border text-muted-foreground hover:text-primary hover:bg-secondary/70 transition-colors",
502
+ "aria-label": `Verify ${strategyLabel(s.strategy)}`,
503
+ title: "Verify this locator on the device",
504
+ children: /* @__PURE__ */ jsx(Search, { className: "w-3.5 h-3.5" })
505
+ }
506
+ ),
507
+ /* @__PURE__ */ jsx(
508
+ "button",
509
+ {
510
+ onClick: () => copy(s.strategy, s.value),
511
+ className: "flex-shrink-0 grid place-items-center w-8 rounded-md bg-secondary border border-border text-muted-foreground hover:text-foreground hover:bg-secondary/70 transition-colors",
512
+ "aria-label": `Copy ${strategyLabel(s.strategy)}`,
513
+ children: copiedKey === s.strategy ? /* @__PURE__ */ jsx(Check, { className: "w-3.5 h-3.5 text-success" }) : /* @__PURE__ */ jsx(Copy, { className: "w-3.5 h-3.5" })
514
+ }
515
+ )
516
+ ] }, s.strategy)) })
517
+ ] }),
518
+ /* @__PURE__ */ jsxs("section", { className: "px-4 py-4 border-b border-border", children: [
519
+ /* @__PURE__ */ jsx("h3", { className: "text-label-caps text-muted-foreground mb-2", children: "Attributes" }),
520
+ /* @__PURE__ */ jsx("div", { className: "rounded-md border border-border overflow-hidden", children: /* @__PURE__ */ jsxs("table", { className: "w-full text-code-sm font-mono border-collapse", children: [
521
+ /* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsxs("tr", { className: "bg-secondary/60 text-foreground", children: [
522
+ /* @__PURE__ */ jsx("th", { className: "text-left py-1.5 px-2.5 font-semibold w-2/5 border-b border-border", children: "Key" }),
523
+ /* @__PURE__ */ jsx("th", { className: "text-left py-1.5 px-2.5 font-semibold border-b border-border", children: "Value" })
524
+ ] }) }),
525
+ /* @__PURE__ */ jsxs("tbody", { children: [
526
+ /* @__PURE__ */ jsx(AttrRow, { index: 0, k: "class", value: element.class_name ?? "\u2014", accent: true }),
527
+ element.resource_id && /* @__PURE__ */ jsx(AttrRow, { index: 1, k: "resource-id", value: element.resource_id }),
528
+ element.label && /* @__PURE__ */ jsx(AttrRow, { index: 2, k: "label", value: element.label }),
529
+ element.text && /* @__PURE__ */ jsx(AttrRow, { index: 3, k: "text", value: element.text }),
530
+ /* @__PURE__ */ jsx(AttrRow, { index: 4, k: "enabled", value: String(element.enabled) }),
531
+ /* @__PURE__ */ jsx(AttrRow, { index: 5, k: "visible", value: String(element.visible) }),
532
+ /* @__PURE__ */ jsx(AttrRow, { index: 6, k: "x", value: String(element.rect.x) }),
533
+ /* @__PURE__ */ jsx(AttrRow, { index: 7, k: "y", value: String(element.rect.y) }),
534
+ /* @__PURE__ */ jsx(AttrRow, { index: 8, k: "width", value: String(element.rect.width) }),
535
+ /* @__PURE__ */ jsx(AttrRow, { index: 9, k: "height", value: String(element.rect.height) }),
536
+ attrs.map(([k, v], i) => /* @__PURE__ */ jsx(AttrRow, { index: 10 + i, k, value: v }, k))
537
+ ] })
538
+ ] }) })
539
+ ] }),
540
+ /* @__PURE__ */ jsxs("section", { className: "px-4 py-4", children: [
541
+ /* @__PURE__ */ jsx("h3", { className: "text-label-caps text-muted-foreground mb-2", children: "Actions" }),
542
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap gap-2", children: [
543
+ /* @__PURE__ */ jsx(ActionButton, { label: "Tap", onClick: () => onTap?.(element), disabled: !onTap, primary: true, children: /* @__PURE__ */ jsx(Crosshair, { className: "w-3.5 h-3.5" }) }),
544
+ /* @__PURE__ */ jsx(
545
+ ActionButton,
546
+ {
547
+ label: "Long Press",
548
+ onClick: () => onLongPress?.(element),
549
+ disabled: !onLongPress,
550
+ children: /* @__PURE__ */ jsx(TimerReset, { className: "w-3.5 h-3.5" })
551
+ }
552
+ ),
553
+ /* @__PURE__ */ jsx(
554
+ ActionButton,
555
+ {
556
+ label: "Clear",
557
+ onClick: () => onClearText?.(element),
558
+ disabled: !onClearText,
559
+ children: /* @__PURE__ */ jsx(Eraser, { className: "w-3.5 h-3.5" })
560
+ }
561
+ ),
562
+ /* @__PURE__ */ jsx(
563
+ ActionButton,
564
+ {
565
+ label: "Capture Image",
566
+ onClick: handleCaptureImage,
567
+ disabled: !screenshot,
568
+ children: /* @__PURE__ */ jsx(ImageDown, { className: "w-3.5 h-3.5" })
569
+ }
570
+ )
571
+ ] }),
572
+ /* @__PURE__ */ jsxs("div", { className: "flex items-stretch gap-1.5 mt-3", children: [
573
+ /* @__PURE__ */ jsx(
574
+ Input,
575
+ {
576
+ value: typeText,
577
+ onChange: (e) => setTypeText(e.target.value),
578
+ onKeyDown: (e) => {
579
+ if (e.key === "Enter" && typeText) {
580
+ onInputText?.(element, typeText);
581
+ setTypeText("");
582
+ }
583
+ },
584
+ placeholder: "Text to send\u2026",
585
+ className: "flex-1 h-8 py-1"
586
+ }
587
+ ),
588
+ /* @__PURE__ */ jsxs(
589
+ "button",
590
+ {
591
+ onClick: () => {
592
+ if (typeText) {
593
+ onInputText?.(element, typeText);
594
+ setTypeText("");
595
+ }
596
+ },
597
+ disabled: !onInputText || !typeText,
598
+ className: "flex-shrink-0 flex items-center gap-1 px-2.5 rounded-md border border-border bg-secondary text-muted-foreground hover:text-foreground transition-colors disabled:opacity-50 text-body-sm font-medium",
599
+ children: [
600
+ /* @__PURE__ */ jsx(Keyboard, { className: "w-3.5 h-3.5" }),
601
+ "Send"
602
+ ]
603
+ }
604
+ )
605
+ ] }),
606
+ captureError && /* @__PURE__ */ jsx("p", { className: "mt-2 text-xs text-destructive", children: captureError }),
607
+ onSaveLocator && /* @__PURE__ */ jsx(
608
+ Button,
609
+ {
610
+ variant: "secondary",
611
+ size: "sm",
612
+ className: "w-full mt-3",
613
+ onClick: () => onSaveLocator(element),
614
+ children: "Save as Locator"
615
+ }
616
+ )
617
+ ] })
618
+ ] }) });
619
+ }
620
+ function ActionButton({
621
+ label,
622
+ onClick,
623
+ disabled,
624
+ primary,
625
+ children
626
+ }) {
627
+ return /* @__PURE__ */ jsxs(
628
+ "button",
629
+ {
630
+ onClick,
631
+ disabled,
632
+ className: cn(
633
+ "flex items-center gap-1.5 px-2.5 py-1 rounded-md text-body-sm font-medium border transition-colors disabled:opacity-50",
634
+ primary ? "border-primary text-primary hover:bg-primary/10" : "border-border text-muted-foreground hover:text-foreground hover:bg-secondary"
635
+ ),
636
+ children: [
637
+ children,
638
+ label
639
+ ]
640
+ }
641
+ );
642
+ }
643
+ function AttrRow({
644
+ index,
645
+ k,
646
+ value,
647
+ accent
648
+ }) {
649
+ return /* @__PURE__ */ jsxs("tr", { className: cn("border-b border-border last:border-b-0", index % 2 === 1 && "bg-secondary/30"), children: [
650
+ /* @__PURE__ */ jsx("td", { className: "py-1.5 px-2.5 text-muted-foreground align-top", children: k }),
651
+ /* @__PURE__ */ jsx("td", { className: cn("py-1.5 px-2.5 break-all", accent ? "text-primary" : "text-foreground"), children: value })
652
+ ] });
653
+ }
654
+ function SaveLocatorDialog({ element, platform, onClose }) {
655
+ const { create } = useLocators();
656
+ const [name, setName] = useState("");
657
+ const [strategy, setStrategy] = useState("xpath");
658
+ const [value, setValue] = useState("");
659
+ const [saveError, setSaveError] = useState(null);
660
+ const [saving, setSaving] = useState(false);
661
+ const strategyOptions = strategyOptionsFor(platform);
662
+ useEffect(() => {
663
+ if (element) {
664
+ const best = bestLocator(element, platform);
665
+ setName(
666
+ element.label || element.text || element.resource_id || element.class_name || best.value
667
+ );
668
+ setStrategy(best.strategy);
669
+ setValue(best.value);
670
+ setSaveError(null);
671
+ }
672
+ }, [element, platform]);
673
+ const handleStrategyChange = (s) => {
674
+ setStrategy(s);
675
+ if (!element) return;
676
+ const match = suggestLocators(element, platform).find((x) => x.strategy === s);
677
+ if (match) setValue(match.value);
678
+ };
679
+ const handleSave = async () => {
680
+ if (!name.trim()) return;
681
+ setSaving(true);
682
+ setSaveError(null);
683
+ try {
684
+ await create({
685
+ name: name.trim(),
686
+ strategy,
687
+ value,
688
+ platform: platform ?? "android",
689
+ element_label: element?.label ?? void 0
690
+ });
691
+ onClose();
692
+ } catch (err) {
693
+ setSaveError(String(err));
694
+ } finally {
695
+ setSaving(false);
696
+ }
697
+ };
698
+ return /* @__PURE__ */ jsx(
699
+ Dialog,
700
+ {
701
+ open: element !== null,
702
+ onClose,
703
+ title: "Save Locator",
704
+ description: `Save this element as a reusable locator${platform ? ` (${platform})` : ""}.`,
705
+ children: /* @__PURE__ */ jsxs("div", { className: "space-y-3", children: [
706
+ /* @__PURE__ */ jsxs("div", { children: [
707
+ /* @__PURE__ */ jsxs("label", { className: "text-[10px] font-medium uppercase tracking-wider text-muted-foreground block mb-1", children: [
708
+ "Name ",
709
+ /* @__PURE__ */ jsx("span", { className: "text-destructive", children: "*" })
710
+ ] }),
711
+ /* @__PURE__ */ jsx(
712
+ Input,
713
+ {
714
+ value: name,
715
+ onChange: (e) => setName(e.target.value),
716
+ placeholder: "Locator name"
717
+ }
718
+ )
719
+ ] }),
720
+ /* @__PURE__ */ jsxs("div", { children: [
721
+ /* @__PURE__ */ jsx("label", { className: "text-[10px] font-medium uppercase tracking-wider text-muted-foreground block mb-1", children: "Strategy" }),
722
+ /* @__PURE__ */ jsx(Select, { value: strategy, onChange: handleStrategyChange, options: strategyOptions })
723
+ ] }),
724
+ /* @__PURE__ */ jsxs("div", { children: [
725
+ /* @__PURE__ */ jsx("label", { className: "text-[10px] font-medium uppercase tracking-wider text-muted-foreground block mb-1", children: "Value" }),
726
+ /* @__PURE__ */ jsx(
727
+ Input,
728
+ {
729
+ value,
730
+ onChange: (e) => setValue(e.target.value),
731
+ placeholder: "Locator value"
732
+ }
733
+ )
734
+ ] }),
735
+ saveError && /* @__PURE__ */ jsx("p", { className: "text-xs text-destructive", children: saveError }),
736
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-end gap-2 pt-1", children: [
737
+ /* @__PURE__ */ jsx(Button, { variant: "ghost", size: "sm", onClick: onClose, disabled: saving, children: "Cancel" }),
738
+ /* @__PURE__ */ jsx(
739
+ Button,
740
+ {
741
+ variant: "primary",
742
+ size: "sm",
743
+ onClick: handleSave,
744
+ disabled: saving || !name.trim(),
745
+ children: saving ? "Saving\u2026" : "Save"
746
+ }
747
+ )
748
+ ] })
749
+ ] })
750
+ }
751
+ );
752
+ }
753
+ var TEXT = (s) => /* @__PURE__ */ jsx("span", { className: "text-[11px] font-semibold leading-none", children: s });
754
+ function keysFor(platform) {
755
+ switch (platform) {
756
+ case "ios":
757
+ return [
758
+ { key: "Home", label: "Home", node: /* @__PURE__ */ jsx(Home, { className: "w-3.5 h-3.5" }) },
759
+ { key: "Enter", label: "Return", node: /* @__PURE__ */ jsx(CornerDownLeft, { className: "w-3.5 h-3.5" }) }
760
+ ];
761
+ case "macos":
762
+ case "windows":
763
+ return [
764
+ { key: "Escape", label: "Esc", node: TEXT("esc") },
765
+ { key: "Enter", label: "Enter", node: /* @__PURE__ */ jsx(CornerDownLeft, { className: "w-3.5 h-3.5" }) },
766
+ { key: "Tab", label: "Tab", node: TEXT("tab") },
767
+ { key: "Backspace", label: "Backspace", node: /* @__PURE__ */ jsx(Delete, { className: "w-3.5 h-3.5" }) }
768
+ ];
769
+ case "android":
770
+ default:
771
+ return [
772
+ { key: "Back", label: "Back", node: /* @__PURE__ */ jsx(ArrowLeft, { className: "w-3.5 h-3.5" }) },
773
+ { key: "Home", label: "Home", node: /* @__PURE__ */ jsx(Home, { className: "w-3.5 h-3.5" }) },
774
+ { key: "AppSwitch", label: "Recents", node: /* @__PURE__ */ jsx(SquareStack, { className: "w-3.5 h-3.5" }) },
775
+ { key: "Enter", label: "Enter", node: /* @__PURE__ */ jsx(CornerDownLeft, { className: "w-3.5 h-3.5" }) }
776
+ ];
777
+ }
778
+ }
779
+ function ToolButton({
780
+ label,
781
+ onClick,
782
+ disabled,
783
+ children
784
+ }) {
785
+ return /* @__PURE__ */ jsx(
786
+ "button",
787
+ {
788
+ onClick,
789
+ disabled,
790
+ title: label,
791
+ "aria-label": label,
792
+ className: "grid place-items-center h-7 min-w-7 px-1.5 rounded-md border border-border bg-secondary text-muted-foreground hover:text-foreground hover:bg-secondary/70 transition-colors disabled:opacity-40 disabled:cursor-not-allowed",
793
+ children
794
+ }
795
+ );
796
+ }
797
+ function InspectorToolbar({
798
+ disabled,
799
+ platform,
800
+ mode,
801
+ onModeChange,
802
+ recording,
803
+ recordedCount,
804
+ onToggleRecording,
805
+ onPressKey,
806
+ onScroll
807
+ }) {
808
+ const keys = keysFor(platform);
809
+ const showScroll = platform === "android" || platform === "ios" || platform === null;
810
+ const interactLabel = platform === "macos" || platform === "windows" ? "Click" : "Interact";
811
+ return /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3 px-4 h-10 border-b border-border bg-background/40 flex-shrink-0", children: [
812
+ /* @__PURE__ */ jsx("div", { className: "flex items-center gap-1", children: keys.map((k) => /* @__PURE__ */ jsx(
813
+ ToolButton,
814
+ {
815
+ label: k.label,
816
+ onClick: () => onPressKey(k.key),
817
+ disabled,
818
+ children: k.node
819
+ },
820
+ k.key
821
+ )) }),
822
+ showScroll && /* @__PURE__ */ jsxs(Fragment, { children: [
823
+ /* @__PURE__ */ jsx("span", { className: "w-px h-4 bg-border" }),
824
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1", children: [
825
+ /* @__PURE__ */ jsx(ToolButton, { label: "Scroll up", onClick: () => onScroll("up"), disabled, children: /* @__PURE__ */ jsx(ChevronsUp, { className: "w-3.5 h-3.5" }) }),
826
+ /* @__PURE__ */ jsx(ToolButton, { label: "Scroll down", onClick: () => onScroll("down"), disabled, children: /* @__PURE__ */ jsx(ChevronsDown, { className: "w-3.5 h-3.5" }) })
827
+ ] })
828
+ ] }),
829
+ /* @__PURE__ */ jsx("span", { className: "w-px h-4 bg-border" }),
830
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center rounded-md border border-border overflow-hidden", children: [
831
+ /* @__PURE__ */ jsxs(
832
+ "button",
833
+ {
834
+ onClick: () => onModeChange("inspect"),
835
+ className: cn(
836
+ "flex items-center gap-1 px-2 h-7 text-[11px] font-medium transition-colors",
837
+ mode === "inspect" ? "bg-primary/15 text-primary" : "bg-secondary text-muted-foreground hover:text-foreground"
838
+ ),
839
+ title: "Click selects the element under the cursor",
840
+ children: [
841
+ /* @__PURE__ */ jsx(MousePointer2, { className: "w-3.5 h-3.5" }),
842
+ "Inspect"
843
+ ]
844
+ }
845
+ ),
846
+ /* @__PURE__ */ jsxs(
847
+ "button",
848
+ {
849
+ onClick: () => onModeChange("interact"),
850
+ disabled,
851
+ className: cn(
852
+ "flex items-center gap-1 px-2 h-7 text-[11px] font-medium transition-colors disabled:opacity-40",
853
+ mode === "interact" ? "bg-primary/15 text-primary" : "bg-secondary text-muted-foreground hover:text-foreground"
854
+ ),
855
+ title: "Click performs a real tap/click at the clicked coordinate",
856
+ children: [
857
+ /* @__PURE__ */ jsx(Circle, { className: "w-3 h-3" }),
858
+ interactLabel
859
+ ]
860
+ }
861
+ )
862
+ ] }),
863
+ /* @__PURE__ */ jsx("div", { className: "flex-1" }),
864
+ /* @__PURE__ */ jsxs(
865
+ "button",
866
+ {
867
+ onClick: onToggleRecording,
868
+ disabled,
869
+ className: cn(
870
+ "flex items-center gap-1.5 px-2.5 h-7 rounded-md border text-[11px] font-medium transition-colors disabled:opacity-40",
871
+ recording ? "border-destructive text-destructive bg-destructive/10" : "border-border text-muted-foreground bg-secondary hover:text-foreground"
872
+ ),
873
+ title: recording ? "Stop recording" : "Record actions into a script",
874
+ children: [
875
+ recording ? /* @__PURE__ */ jsx(Square, { className: "w-3 h-3 fill-current" }) : /* @__PURE__ */ jsx(Circle, { className: "w-3 h-3" }),
876
+ recording ? "Recording" : "Record",
877
+ recordedCount > 0 && /* @__PURE__ */ jsx("span", { className: "ml-0.5 px-1 rounded-full bg-foreground/10 text-foreground text-[10px]", children: recordedCount })
878
+ ]
879
+ }
880
+ )
881
+ ] });
882
+ }
883
+ function RecorderBar({ lines, platform, onRemoveLine, onClear }) {
884
+ const client = useClient();
885
+ const [name, setName] = useState("");
886
+ const [saving, setSaving] = useState(false);
887
+ const [savedId, setSavedId] = useState(null);
888
+ const [error, setError] = useState(null);
889
+ const script = () => ({
890
+ name: name.trim() || `Recorded ${(/* @__PURE__ */ new Date()).toLocaleString()}`,
891
+ platform: platform ?? "android",
892
+ language: "ts",
893
+ code: lines.join("\n"),
894
+ variables: {}
895
+ });
896
+ const handleSave = async () => {
897
+ setSaving(true);
898
+ setError(null);
899
+ setSavedId(null);
900
+ try {
901
+ const created = await client.scripts.create(script());
902
+ setSavedId(created.id);
903
+ } catch (err) {
904
+ setError(String(err));
905
+ } finally {
906
+ setSaving(false);
907
+ }
908
+ };
909
+ const handleExport = () => {
910
+ const blob = new Blob([JSON.stringify(script(), null, 2)], {
911
+ type: "application/json"
912
+ });
913
+ const url = URL.createObjectURL(blob);
914
+ const a = document.createElement("a");
915
+ a.href = url;
916
+ a.download = `script-${Date.now()}.json`;
917
+ document.body.appendChild(a);
918
+ a.click();
919
+ a.remove();
920
+ URL.revokeObjectURL(url);
921
+ };
922
+ return /* @__PURE__ */ jsxs("div", { className: "flex flex-col border-t border-border bg-surface flex-shrink-0 max-h-44", children: [
923
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 px-4 h-10 border-b border-border bg-background/40 flex-shrink-0", children: [
924
+ /* @__PURE__ */ jsxs("span", { className: "text-label-caps text-muted-foreground", children: [
925
+ "Recorder \xB7 ",
926
+ lines.length,
927
+ " line",
928
+ lines.length === 1 ? "" : "s"
929
+ ] }),
930
+ /* @__PURE__ */ jsx("div", { className: "flex-1" }),
931
+ /* @__PURE__ */ jsx("div", { className: "w-48", children: /* @__PURE__ */ jsx(
932
+ Input,
933
+ {
934
+ value: name,
935
+ onChange: (e) => setName(e.target.value),
936
+ placeholder: "Script name",
937
+ className: "h-7 py-1 text-xs"
938
+ }
939
+ ) }),
940
+ /* @__PURE__ */ jsxs(
941
+ Button,
942
+ {
943
+ variant: "primary",
944
+ size: "sm",
945
+ onClick: handleSave,
946
+ disabled: saving || lines.length === 0,
947
+ children: [
948
+ /* @__PURE__ */ jsx(Save, { className: "w-3.5 h-3.5" }),
949
+ saving ? "Saving\u2026" : "Save as Script"
950
+ ]
951
+ }
952
+ ),
953
+ /* @__PURE__ */ jsxs(
954
+ Button,
955
+ {
956
+ variant: "secondary",
957
+ size: "sm",
958
+ onClick: handleExport,
959
+ disabled: lines.length === 0,
960
+ children: [
961
+ /* @__PURE__ */ jsx(Upload, { className: "w-3.5 h-3.5" }),
962
+ "Export"
963
+ ]
964
+ }
965
+ ),
966
+ /* @__PURE__ */ jsxs(Button, { variant: "ghost", size: "sm", onClick: onClear, disabled: lines.length === 0, children: [
967
+ /* @__PURE__ */ jsx(Trash2, { className: "w-3.5 h-3.5" }),
968
+ "Clear"
969
+ ] })
970
+ ] }),
971
+ (savedId || error) && /* @__PURE__ */ jsx(
972
+ "div",
973
+ {
974
+ className: `px-4 py-1.5 text-code-sm flex-shrink-0 ${error ? "text-destructive bg-destructive/5" : "text-success bg-success/5"}`,
975
+ children: error ?? `Saved as script ${savedId}`
976
+ }
977
+ ),
978
+ /* @__PURE__ */ jsx("div", { className: "flex-1 overflow-auto px-3 py-2", children: lines.length === 0 ? /* @__PURE__ */ jsx("p", { className: "text-code-sm text-muted-foreground px-1", children: "Recording on \u2014 perform taps, typing, key presses, or swipes to capture code lines." }) : /* @__PURE__ */ jsx("ol", { className: "flex flex-col gap-1", children: lines.map((line, i) => /* @__PURE__ */ jsxs(
979
+ "li",
980
+ {
981
+ className: "group flex items-center gap-2 px-2 py-1 rounded-md bg-secondary/40 border border-border text-code-sm font-mono",
982
+ children: [
983
+ /* @__PURE__ */ jsx("span", { className: "text-muted-foreground w-5 flex-shrink-0", children: i + 1 }),
984
+ /* @__PURE__ */ jsx("span", { className: "flex-1 min-w-0 truncate text-foreground whitespace-pre", children: line }),
985
+ /* @__PURE__ */ jsx(
986
+ "button",
987
+ {
988
+ onClick: () => onRemoveLine(i),
989
+ className: "opacity-0 group-hover:opacity-100 grid place-items-center w-5 h-5 rounded text-muted-foreground hover:text-destructive transition-all flex-shrink-0",
990
+ "aria-label": "Remove line",
991
+ children: /* @__PURE__ */ jsx(X, { className: "w-3.5 h-3.5" })
992
+ }
993
+ )
994
+ ]
995
+ },
996
+ i
997
+ )) }) })
998
+ ] });
999
+ }
1000
+ function findNodeAt(node, x, y) {
1001
+ if (!node) return null;
1002
+ const { rect } = node;
1003
+ const inside = x >= rect.x && x <= rect.x + rect.width && y >= rect.y && y <= rect.y + rect.height;
1004
+ if (!inside) return null;
1005
+ for (const child of node.children) {
1006
+ const hit = findNodeAt(child, x, y);
1007
+ if (hit) return hit;
1008
+ }
1009
+ return node;
1010
+ }
1011
+ var PLATFORM_LABEL = {
1012
+ android: "Android",
1013
+ ios: "iOS",
1014
+ macos: "macOS",
1015
+ windows: "Windows"
1016
+ };
1017
+ function InspectorPage() {
1018
+ const t = useAixT();
1019
+ const {
1020
+ screenshot,
1021
+ tree,
1022
+ selectedElement,
1023
+ loading,
1024
+ error,
1025
+ matches,
1026
+ recording,
1027
+ recordedLines,
1028
+ refresh,
1029
+ reset,
1030
+ selectElement,
1031
+ tap,
1032
+ inputText,
1033
+ pressKey,
1034
+ swipe,
1035
+ longPress,
1036
+ setRecording,
1037
+ recordLine,
1038
+ removeLine,
1039
+ clearRecording
1040
+ } = useInspectorView();
1041
+ const { devices, fetchDevices, selectedDeviceId, selectDevice } = useDevicesView();
1042
+ const [locatorDialogOpen, setLocatorDialogOpen] = useState(null);
1043
+ const [mode, setMode] = useState("inspect");
1044
+ const device = useMemo(
1045
+ () => devices.find((d) => d.id === selectedDeviceId) ?? null,
1046
+ [devices, selectedDeviceId]
1047
+ );
1048
+ const platform = device?.platform ?? null;
1049
+ useEffect(() => {
1050
+ fetchDevices();
1051
+ }, [fetchDevices]);
1052
+ useEffect(() => {
1053
+ reset();
1054
+ if (selectedDeviceId) refresh();
1055
+ }, [selectedDeviceId, reset, refresh]);
1056
+ const deviceOptions = useMemo(
1057
+ () => devices.map((d) => ({
1058
+ value: d.id,
1059
+ label: `${d.name} \xB7 ${PLATFORM_LABEL[d.platform] ?? d.platform}${d.online ? "" : " (offline)"}`
1060
+ })),
1061
+ [devices]
1062
+ );
1063
+ const handleCoordinateClick = useCallback(
1064
+ (x, y) => {
1065
+ if (mode === "interact") {
1066
+ void tap(x, y);
1067
+ return;
1068
+ }
1069
+ const el = findNodeAt(tree, Math.round(x), Math.round(y));
1070
+ if (el) selectElement(el);
1071
+ },
1072
+ [mode, tree, tap, selectElement]
1073
+ );
1074
+ const handleTapElement = useCallback(
1075
+ (el) => {
1076
+ recordLine(tapLine(el, platform));
1077
+ void tap(el.rect.x + el.rect.width / 2, el.rect.y + el.rect.height / 2);
1078
+ },
1079
+ [tap, recordLine, platform]
1080
+ );
1081
+ const handleLongPress = useCallback(
1082
+ (el) => {
1083
+ recordLine(longPressLine(el));
1084
+ void longPress(el.rect.x + el.rect.width / 2, el.rect.y + el.rect.height / 2);
1085
+ },
1086
+ [longPress, recordLine, platform]
1087
+ );
1088
+ const handleInputText = useCallback(
1089
+ (el, text) => {
1090
+ recordLine(inputTextLine(el, platform, text));
1091
+ void (async () => {
1092
+ await tap(el.rect.x + el.rect.width / 2, el.rect.y + el.rect.height / 2);
1093
+ await inputText(text);
1094
+ })();
1095
+ },
1096
+ [tap, inputText, recordLine, platform]
1097
+ );
1098
+ const handleClearText = useCallback(
1099
+ (el) => {
1100
+ recordLine(clearTextLine(el, platform));
1101
+ void (async () => {
1102
+ await tap(el.rect.x + el.rect.width / 2, el.rect.y + el.rect.height / 2);
1103
+ await pressKey("Delete");
1104
+ })();
1105
+ },
1106
+ [tap, pressKey, recordLine, platform]
1107
+ );
1108
+ const handlePressKey = useCallback(
1109
+ (key) => {
1110
+ recordLine(pressKeyLine(key));
1111
+ void pressKey(key);
1112
+ },
1113
+ [pressKey, recordLine]
1114
+ );
1115
+ const handleScroll = useCallback(
1116
+ (dir) => {
1117
+ const w = tree?.rect.width ?? 1080;
1118
+ const h = tree?.rect.height ?? 1920;
1119
+ const cx = w / 2;
1120
+ const y1 = dir === "down" ? h * 0.7 : h * 0.3;
1121
+ const y2 = dir === "down" ? h * 0.3 : h * 0.7;
1122
+ recordLine(swipeLine(cx, y1, cx, y2));
1123
+ void swipe(cx, y1, cx, y2);
1124
+ },
1125
+ [tree, swipe, recordLine]
1126
+ );
1127
+ const handleExport = useCallback(() => {
1128
+ if (!tree) return;
1129
+ const payload = {
1130
+ exported_at: (/* @__PURE__ */ new Date()).toISOString(),
1131
+ device_id: selectedDeviceId,
1132
+ platform,
1133
+ tree,
1134
+ screenshot: screenshot ?? null
1135
+ };
1136
+ const blob = new Blob([JSON.stringify(payload, null, 2)], {
1137
+ type: "application/json"
1138
+ });
1139
+ const url = URL.createObjectURL(blob);
1140
+ const a = document.createElement("a");
1141
+ a.href = url;
1142
+ a.download = `inspector-${Date.now()}.json`;
1143
+ document.body.appendChild(a);
1144
+ a.click();
1145
+ a.remove();
1146
+ URL.revokeObjectURL(url);
1147
+ }, [tree, screenshot, selectedDeviceId, platform]);
1148
+ const showRecorder = recording || recordedLines.length > 0;
1149
+ return /* @__PURE__ */ jsxs("div", { className: "flex flex-col h-full bg-background", children: [
1150
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-end gap-2 border-b border-border px-4 h-12 flex-shrink-0", children: [
1151
+ /* @__PURE__ */ jsx("div", { className: "w-40 sm:w-56", children: /* @__PURE__ */ jsx(
1152
+ Select,
1153
+ {
1154
+ value: selectedDeviceId ?? "",
1155
+ onChange: (id) => selectDevice(id || null),
1156
+ options: deviceOptions,
1157
+ placeholder: t("inspector.selectDevice")
1158
+ }
1159
+ ) }),
1160
+ platform && /* @__PURE__ */ jsx("span", { className: "px-2 py-0.5 rounded-full border border-border bg-secondary text-[11px] font-medium text-muted-foreground", children: PLATFORM_LABEL[platform] }),
1161
+ /* @__PURE__ */ jsxs(Button, { variant: "secondary", size: "sm", disabled: !tree, onClick: handleExport, children: [
1162
+ /* @__PURE__ */ jsx(Upload, { className: "w-3.5 h-3.5" }),
1163
+ t("inspector.export")
1164
+ ] }),
1165
+ /* @__PURE__ */ jsxs(
1166
+ Button,
1167
+ {
1168
+ variant: "primary",
1169
+ size: "sm",
1170
+ onClick: refresh,
1171
+ disabled: loading || !selectedDeviceId,
1172
+ children: [
1173
+ /* @__PURE__ */ jsx(RefreshCw, { className: cn("w-3.5 h-3.5", loading && "animate-spin") }),
1174
+ t("common.refresh")
1175
+ ]
1176
+ }
1177
+ )
1178
+ ] }),
1179
+ /* @__PURE__ */ jsx(
1180
+ InspectorToolbar,
1181
+ {
1182
+ disabled: !selectedDeviceId,
1183
+ platform,
1184
+ mode,
1185
+ onModeChange: setMode,
1186
+ recording,
1187
+ recordedCount: recordedLines.length,
1188
+ onToggleRecording: () => setRecording(!recording),
1189
+ onPressKey: handlePressKey,
1190
+ onScroll: handleScroll
1191
+ }
1192
+ ),
1193
+ error && /* @__PURE__ */ jsx("div", { className: "flex items-center justify-center h-10 text-destructive text-code-sm bg-destructive/5 border-b border-border flex-shrink-0", children: error }),
1194
+ /* @__PURE__ */ jsxs("div", { className: "flex-1 grid grid-cols-[14rem_minmax(0,1fr)_15rem] lg:grid-cols-[17rem_minmax(0,1fr)_18rem] xl:grid-cols-[20rem_minmax(0,1fr)_22rem] overflow-hidden", children: [
1195
+ /* @__PURE__ */ jsx(
1196
+ UITreePanel,
1197
+ {
1198
+ tree,
1199
+ selectedElement,
1200
+ onSelectElement: selectElement
1201
+ }
1202
+ ),
1203
+ /* @__PURE__ */ jsx(
1204
+ ScreenshotPanel,
1205
+ {
1206
+ screenshot,
1207
+ selectedElement,
1208
+ loading,
1209
+ deviceName: device?.name ?? null,
1210
+ platform,
1211
+ interactMode: mode === "interact",
1212
+ matches,
1213
+ onCoordinateClick: handleCoordinateClick
1214
+ }
1215
+ ),
1216
+ /* @__PURE__ */ jsx(
1217
+ ElementDetailsPanel,
1218
+ {
1219
+ element: selectedElement,
1220
+ platform,
1221
+ screenshot,
1222
+ onSaveLocator: setLocatorDialogOpen,
1223
+ onTap: handleTapElement,
1224
+ onLongPress: handleLongPress,
1225
+ onInputText: handleInputText,
1226
+ onClearText: handleClearText
1227
+ }
1228
+ )
1229
+ ] }),
1230
+ showRecorder && /* @__PURE__ */ jsx(
1231
+ RecorderBar,
1232
+ {
1233
+ lines: recordedLines,
1234
+ platform,
1235
+ onRemoveLine: removeLine,
1236
+ onClear: clearRecording
1237
+ }
1238
+ ),
1239
+ /* @__PURE__ */ jsx(
1240
+ SaveLocatorDialog,
1241
+ {
1242
+ element: locatorDialogOpen,
1243
+ platform,
1244
+ onClose: () => setLocatorDialogOpen(null)
1245
+ }
1246
+ )
1247
+ ] });
1248
+ }
1249
+
1250
+ export { InspectorPage };