@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,866 @@
1
+ import { LogConsole } from './chunk-FW23FLAF.js';
2
+ import { cn, Button, Select } from './chunk-EVHTAXQG.js';
3
+ import { useDevices, useScripts, useScriptRun } from './chunk-O34SNDQB.js';
4
+ import { variablesToSchema, normalizeDevices, schemaToVariables } from './chunk-V4MXFPDX.js';
5
+ import { useClient } from './chunk-UINEPEIE.js';
6
+ import { useMemo, useState, useEffect, useRef, useCallback } from 'react';
7
+ import { Trash2, Plus, Play, Pause, Square, ArrowDownToLine, WrapText, ChevronRight, Save, XCircle, CheckCircle2, Loader2, Circle } from 'lucide-react';
8
+ import CodeMirror, { EditorView } from '@uiw/react-codemirror';
9
+ import { javascript } from '@codemirror/lang-javascript';
10
+ import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
11
+
12
+ function CodeEditor({
13
+ value,
14
+ onChange,
15
+ height = "100%",
16
+ language = "ts",
17
+ onSelect
18
+ }) {
19
+ const extensions = [javascript({ typescript: language === "ts" })];
20
+ if (onSelect) {
21
+ extensions.push(
22
+ EditorView.updateListener.of((update) => {
23
+ if (!update.selectionSet) return;
24
+ const range = update.state.selection.main;
25
+ onSelect(update.state.sliceDoc(range.from, range.to));
26
+ })
27
+ );
28
+ }
29
+ return /* @__PURE__ */ jsx(
30
+ CodeMirror,
31
+ {
32
+ value,
33
+ onChange,
34
+ extensions,
35
+ theme: "dark",
36
+ height,
37
+ basicSetup: { lineNumbers: true, foldGutter: true },
38
+ style: { height: "100%", fontSize: 13 }
39
+ }
40
+ );
41
+ }
42
+ var TYPES = ["text", "textarea", "number", "select", "boolean"];
43
+ var inputCls = "bg-secondary rounded px-2 py-1 text-code-sm outline-none focus:ring-1 focus:ring-primary";
44
+ function ScriptParamEditor({ variables, onChange }) {
45
+ const schema = useMemo(() => variablesToSchema(variables), [variables]);
46
+ const commit = (next) => onChange(schemaToVariables(next, variables));
47
+ const update = (i, patch) => {
48
+ const next = schema.map((p, idx) => idx === i ? { ...p, ...patch } : p);
49
+ commit(next);
50
+ };
51
+ const setTarget = (i, on) => {
52
+ const next = schema.map((p, idx) => ({
53
+ ...p,
54
+ isTarget: on ? idx === i : idx === i ? false : p.isTarget
55
+ }));
56
+ commit(next);
57
+ };
58
+ const remove = (i) => commit(schema.filter((_, idx) => idx !== i));
59
+ const add = () => {
60
+ let n = schema.length + 1;
61
+ let key = `param_${n}`;
62
+ const keys = new Set(schema.map((p) => p.key));
63
+ while (keys.has(key)) key = `param_${++n}`;
64
+ commit([...schema, { key, type: "text" }]);
65
+ };
66
+ return /* @__PURE__ */ jsxs("div", { className: "h-full space-y-2 overflow-auto p-4", children: [
67
+ schema.length === 0 && /* @__PURE__ */ jsx("p", { className: "text-body-sm text-muted-foreground", children: "\u6682\u65E0\u53C2\u6570\u3002\u70B9\u4E0B\u65B9\u300C\u6DFB\u52A0\u53C2\u6570\u300D\u58F0\u660E\u811A\u672C\u5165\u53C2\u3002" }),
68
+ schema.map((p, i) => /* @__PURE__ */ jsxs("div", { className: "space-y-2 rounded-md border border-border bg-card/40 p-2.5", children: [
69
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
70
+ /* @__PURE__ */ jsx(
71
+ "input",
72
+ {
73
+ className: `${inputCls} w-1/3 font-mono`,
74
+ value: p.key,
75
+ placeholder: "\u952E\u540D",
76
+ onChange: (e) => update(i, { key: e.target.value })
77
+ }
78
+ ),
79
+ /* @__PURE__ */ jsx(
80
+ "input",
81
+ {
82
+ className: `${inputCls} flex-1`,
83
+ value: p.label ?? "",
84
+ placeholder: "\u663E\u793A\u6807\u7B7E\uFF08\u53EF\u9009\uFF09",
85
+ onChange: (e) => update(i, { label: e.target.value })
86
+ }
87
+ ),
88
+ /* @__PURE__ */ jsx(
89
+ "button",
90
+ {
91
+ type: "button",
92
+ "aria-label": "\u5220\u9664\u53C2\u6570",
93
+ onClick: () => remove(i),
94
+ className: "rounded p-1 text-muted-foreground hover:text-destructive",
95
+ children: /* @__PURE__ */ jsx(Trash2, { className: "h-3.5 w-3.5" })
96
+ }
97
+ )
98
+ ] }),
99
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [
100
+ /* @__PURE__ */ jsx(
101
+ "select",
102
+ {
103
+ className: inputCls,
104
+ value: p.type,
105
+ onChange: (e) => update(i, { type: e.target.value }),
106
+ children: TYPES.map((t) => /* @__PURE__ */ jsx("option", { value: t, children: t }, t))
107
+ }
108
+ ),
109
+ p.type !== "boolean" && p.type !== "select" && /* @__PURE__ */ jsx(
110
+ "input",
111
+ {
112
+ className: `${inputCls} flex-1`,
113
+ value: p.default ?? "",
114
+ placeholder: "\u9ED8\u8BA4\u503C",
115
+ onChange: (e) => update(i, { default: e.target.value })
116
+ }
117
+ ),
118
+ p.type === "select" && /* @__PURE__ */ jsx(
119
+ "input",
120
+ {
121
+ className: `${inputCls} flex-1`,
122
+ value: (p.options ?? []).join(", "),
123
+ placeholder: "\u5019\u9009\u9879\uFF0C\u9017\u53F7\u5206\u9694",
124
+ onChange: (e) => update(i, {
125
+ options: e.target.value.split(",").map((s) => s.trim()).filter(Boolean)
126
+ })
127
+ }
128
+ )
129
+ ] }),
130
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-4 text-body-sm text-muted-foreground", children: [
131
+ /* @__PURE__ */ jsxs("label", { className: "flex cursor-pointer items-center gap-1.5", children: [
132
+ /* @__PURE__ */ jsx(
133
+ "input",
134
+ {
135
+ type: "checkbox",
136
+ className: "h-3.5 w-3.5 rounded border-input accent-primary",
137
+ checked: !!p.isTarget,
138
+ onChange: (e) => setTarget(i, e.target.checked)
139
+ }
140
+ ),
141
+ "\u76EE\u6807\u5B57\u6BB5\uFF08\u521B\u5EFA\u4EFB\u52A1\u65F6\u6309\u884C\u6279\u91CF\uFF09"
142
+ ] }),
143
+ (p.type === "text" || p.type === "textarea") && /* @__PURE__ */ jsxs("label", { className: "flex cursor-pointer items-center gap-1.5", children: [
144
+ /* @__PURE__ */ jsx(
145
+ "input",
146
+ {
147
+ type: "checkbox",
148
+ className: "h-3.5 w-3.5 rounded border-input accent-primary",
149
+ checked: !!p.randomable,
150
+ onChange: (e) => update(i, { randomable: e.target.checked })
151
+ }
152
+ ),
153
+ "\u53EF\u968F\u673A\u53D6\u503C"
154
+ ] })
155
+ ] })
156
+ ] }, i)),
157
+ /* @__PURE__ */ jsxs(
158
+ "button",
159
+ {
160
+ type: "button",
161
+ onClick: add,
162
+ className: "flex items-center gap-1 text-body-sm text-primary",
163
+ children: [
164
+ /* @__PURE__ */ jsx(Plus, { className: "h-3.5 w-3.5" }),
165
+ "\u6DFB\u52A0\u53C2\u6570"
166
+ ]
167
+ }
168
+ )
169
+ ] });
170
+ }
171
+ var statusIcons = {
172
+ pending: /* @__PURE__ */ jsx(Circle, { className: "w-5 h-5 text-muted-foreground" }),
173
+ running: /* @__PURE__ */ jsx(Loader2, { className: "w-5 h-5 text-primary animate-spin" }),
174
+ success: /* @__PURE__ */ jsx(CheckCircle2, { className: "w-5 h-5 text-success" }),
175
+ failed: /* @__PURE__ */ jsx(XCircle, { className: "w-5 h-5 text-destructive" })
176
+ };
177
+ function TimelineStep({ event, isLast }) {
178
+ const isPending = event.status === "pending";
179
+ const isActive = event.status === "running";
180
+ return /* @__PURE__ */ jsxs("div", { className: cn("relative flex gap-3", isPending && "opacity-40"), children: [
181
+ /* @__PURE__ */ jsxs("div", { className: "relative flex flex-col items-center", children: [
182
+ /* @__PURE__ */ jsx("div", { className: "z-10 bg-surface", children: statusIcons[event.status] }),
183
+ !isLast && /* @__PURE__ */ jsx(
184
+ "div",
185
+ {
186
+ className: cn(
187
+ "absolute top-5 w-px h-full",
188
+ event.status === "success" ? "bg-primary/30" : event.status === "failed" ? "bg-destructive/30" : "bg-border"
189
+ )
190
+ }
191
+ )
192
+ ] }),
193
+ /* @__PURE__ */ jsxs("div", { className: "pb-4 min-w-0", children: [
194
+ /* @__PURE__ */ jsx(
195
+ "p",
196
+ {
197
+ className: cn(
198
+ "text-code-md font-mono",
199
+ isActive ? "text-primary font-semibold" : isPending ? "text-muted-foreground" : "text-foreground"
200
+ ),
201
+ children: event.step_type
202
+ }
203
+ ),
204
+ event.description && /* @__PURE__ */ jsx("p", { className: "text-[11px] text-muted-foreground truncate", children: event.description }),
205
+ event.error && /* @__PURE__ */ jsx("p", { className: "text-[11px] text-destructive", children: event.error })
206
+ ] })
207
+ ] });
208
+ }
209
+ var statusLabel = {
210
+ idle: "Ready",
211
+ running: "Running",
212
+ paused: "Paused",
213
+ completed: "Completed",
214
+ failed: "Failed"
215
+ };
216
+ function StatusChip({ status }) {
217
+ const tone = status === "running" ? "bg-primary/15 text-primary" : status === "failed" ? "bg-destructive/15 text-destructive" : status === "completed" ? "bg-success/15 text-success" : status === "paused" ? "bg-warning/15 text-warning" : "bg-secondary text-muted-foreground";
218
+ return /* @__PURE__ */ jsxs(
219
+ "span",
220
+ {
221
+ className: cn(
222
+ "px-2 py-0.5 rounded text-[11px] font-mono flex items-center gap-1.5",
223
+ tone
224
+ ),
225
+ children: [
226
+ status === "running" && /* @__PURE__ */ jsxs("span", { className: "relative flex h-2 w-2", children: [
227
+ /* @__PURE__ */ jsx("span", { className: "animate-ping absolute inline-flex h-full w-full rounded-full bg-primary opacity-75" }),
228
+ /* @__PURE__ */ jsx("span", { className: "relative inline-flex rounded-full h-2 w-2 bg-primary" })
229
+ ] }),
230
+ statusLabel[status] ?? status
231
+ ]
232
+ }
233
+ );
234
+ }
235
+ var LEVEL_CHIPS = [
236
+ { level: "error", label: "E" },
237
+ { level: "warn", label: "W" },
238
+ { level: "info", label: "I" },
239
+ { level: "debug", label: "D" }
240
+ ];
241
+ var ALL_LEVELS = ["system", "error", "warn", "info", "debug"];
242
+ function RunPanel({
243
+ status,
244
+ progress,
245
+ steps,
246
+ logs,
247
+ error,
248
+ onStart,
249
+ onStop,
250
+ onPause,
251
+ onResume,
252
+ onClearLogs,
253
+ canStart,
254
+ startHint,
255
+ onResizeStart
256
+ }) {
257
+ const [tab, setTab] = useState("console");
258
+ const [query, setQuery] = useState("");
259
+ const [autoScroll, setAutoScroll] = useState(true);
260
+ const [wrap, setWrap] = useState(false);
261
+ const [hidden, setHidden] = useState(/* @__PURE__ */ new Set());
262
+ const levels = new Set(ALL_LEVELS.filter((l) => !hidden.has(l)));
263
+ const toggleLevel = (l) => setHidden((prev) => {
264
+ const next = new Set(prev);
265
+ next.has(l) ? next.delete(l) : next.add(l);
266
+ return next;
267
+ });
268
+ const completedSteps = steps.filter(
269
+ (s) => s.status === "success" || s.status === "failed"
270
+ ).length;
271
+ const idleish = status === "idle" || status === "completed" || status === "failed";
272
+ return /* @__PURE__ */ jsxs("div", { className: "flex h-full flex-col overflow-hidden bg-card", children: [
273
+ /* @__PURE__ */ jsx(
274
+ "div",
275
+ {
276
+ onPointerDown: onResizeStart,
277
+ className: "h-1.5 w-full shrink-0 cursor-row-resize bg-border/60 hover:bg-primary/50 transition-colors",
278
+ title: "\u62D6\u62FD\u8C03\u6574\u9AD8\u5EA6"
279
+ }
280
+ ),
281
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 border-b border-border px-2 py-1.5 flex-shrink-0 flex-wrap", children: [
282
+ /* @__PURE__ */ jsx(StatusChip, { status }),
283
+ idleish && /* @__PURE__ */ jsxs(
284
+ Button,
285
+ {
286
+ variant: "primary",
287
+ size: "sm",
288
+ onClick: onStart,
289
+ disabled: !canStart,
290
+ title: canStart ? void 0 : startHint,
291
+ children: [
292
+ /* @__PURE__ */ jsx(Play, { className: "w-3.5 h-3.5" }),
293
+ status === "idle" ? "Run" : "Re-run"
294
+ ]
295
+ }
296
+ ),
297
+ status === "running" && /* @__PURE__ */ jsxs(Fragment, { children: [
298
+ /* @__PURE__ */ jsxs(Button, { variant: "secondary", size: "sm", onClick: onPause, children: [
299
+ /* @__PURE__ */ jsx(Pause, { className: "w-3.5 h-3.5" }),
300
+ "Pause"
301
+ ] }),
302
+ /* @__PURE__ */ jsxs(Button, { variant: "destructive", size: "sm", onClick: onStop, children: [
303
+ /* @__PURE__ */ jsx(Square, { className: "w-3.5 h-3.5" }),
304
+ "Stop"
305
+ ] })
306
+ ] }),
307
+ status === "paused" && /* @__PURE__ */ jsxs(Fragment, { children: [
308
+ /* @__PURE__ */ jsxs(Button, { variant: "primary", size: "sm", onClick: onResume, children: [
309
+ /* @__PURE__ */ jsx(Play, { className: "w-3.5 h-3.5" }),
310
+ "Resume"
311
+ ] }),
312
+ /* @__PURE__ */ jsxs(Button, { variant: "destructive", size: "sm", onClick: onStop, children: [
313
+ /* @__PURE__ */ jsx(Square, { className: "w-3.5 h-3.5" }),
314
+ "Stop"
315
+ ] })
316
+ ] }),
317
+ /* @__PURE__ */ jsx("div", { className: "ml-2 flex items-center rounded bg-secondary p-0.5", children: ["console", "steps"].map((t) => /* @__PURE__ */ jsx(
318
+ "button",
319
+ {
320
+ onClick: () => setTab(t),
321
+ className: cn(
322
+ "rounded px-2 py-0.5 text-xs font-medium capitalize",
323
+ tab === t ? "bg-card text-foreground shadow-sm" : "text-muted-foreground hover:text-foreground"
324
+ ),
325
+ children: t === "console" ? "Console" : `Steps${steps.length ? ` ${completedSteps}/${steps.length}` : ""}`
326
+ },
327
+ t
328
+ )) }),
329
+ /* @__PURE__ */ jsx("div", { className: "ml-auto flex items-center gap-1", children: tab === "console" && /* @__PURE__ */ jsxs(Fragment, { children: [
330
+ /* @__PURE__ */ jsx("div", { className: "flex items-center gap-0.5", children: LEVEL_CHIPS.map((c) => /* @__PURE__ */ jsx(
331
+ "button",
332
+ {
333
+ onClick: () => toggleLevel(c.level),
334
+ title: c.level,
335
+ className: cn(
336
+ "w-5 h-5 rounded text-[11px] font-mono font-bold",
337
+ hidden.has(c.level) ? "text-muted-foreground/40 line-through" : c.level === "error" ? "text-destructive" : c.level === "warn" ? "text-warning" : c.level === "info" ? "text-foreground" : "text-muted-foreground"
338
+ ),
339
+ children: c.label
340
+ },
341
+ c.level
342
+ )) }),
343
+ /* @__PURE__ */ jsx(
344
+ "input",
345
+ {
346
+ value: query,
347
+ onChange: (e) => setQuery(e.target.value),
348
+ placeholder: "filter\u2026",
349
+ className: "h-6 w-28 rounded bg-secondary px-2 text-xs outline-none focus:w-40 transition-all"
350
+ }
351
+ ),
352
+ /* @__PURE__ */ jsx(
353
+ IconToggle,
354
+ {
355
+ active: autoScroll,
356
+ onClick: () => setAutoScroll((v) => !v),
357
+ title: "\u6EDA\u52A8\u5230\u5E95\u90E8",
358
+ children: /* @__PURE__ */ jsx(ArrowDownToLine, { className: "w-3.5 h-3.5" })
359
+ }
360
+ ),
361
+ /* @__PURE__ */ jsx(IconToggle, { active: wrap, onClick: () => setWrap((v) => !v), title: "\u8F6F\u6362\u884C", children: /* @__PURE__ */ jsx(WrapText, { className: "w-3.5 h-3.5" }) }),
362
+ /* @__PURE__ */ jsx(IconToggle, { active: false, onClick: onClearLogs, title: "\u6E05\u7A7A\u65E5\u5FD7", children: /* @__PURE__ */ jsx(Trash2, { className: "w-3.5 h-3.5" }) })
363
+ ] }) })
364
+ ] }),
365
+ status !== "idle" && /* @__PURE__ */ jsx("div", { className: "h-0.5 bg-secondary overflow-hidden flex-shrink-0", children: /* @__PURE__ */ jsx(
366
+ "div",
367
+ {
368
+ className: cn(
369
+ "h-full transition-all duration-500 ease-out",
370
+ status === "failed" ? "bg-destructive" : "bg-primary"
371
+ ),
372
+ style: { width: `${progress}%` }
373
+ }
374
+ ) }),
375
+ error && /* @__PURE__ */ jsx("div", { className: "mx-2 mt-1.5 rounded-md border border-destructive/30 bg-destructive/10 px-3 py-1.5 text-xs text-destructive flex-shrink-0", children: error }),
376
+ /* @__PURE__ */ jsx("div", { className: "flex-1 min-h-0", children: tab === "console" ? /* @__PURE__ */ jsx(
377
+ LogConsole,
378
+ {
379
+ logs,
380
+ levels,
381
+ query,
382
+ wrap,
383
+ autoScroll
384
+ }
385
+ ) : /* @__PURE__ */ jsx("div", { className: "h-full overflow-y-auto p-3", children: steps.length === 0 ? /* @__PURE__ */ jsx("p", { className: "text-body-sm text-muted-foreground", children: status === "idle" ? "\u9009\u62E9\u8BBE\u5907\u540E\u70B9 Run \u5F00\u59CB" : "\u6682\u65E0\u6B65\u9AA4\u4E8B\u4EF6\u3002" }) : steps.map((event, i) => /* @__PURE__ */ jsx(
386
+ TimelineStep,
387
+ {
388
+ event,
389
+ isLast: i === steps.length - 1
390
+ },
391
+ event.step_index ?? i
392
+ )) }) })
393
+ ] });
394
+ }
395
+ function IconToggle({
396
+ active,
397
+ onClick,
398
+ title,
399
+ children
400
+ }) {
401
+ return /* @__PURE__ */ jsx(
402
+ "button",
403
+ {
404
+ onClick,
405
+ title,
406
+ className: cn(
407
+ "flex h-6 w-6 items-center justify-center rounded",
408
+ active ? "bg-primary/15 text-primary" : "text-muted-foreground hover:bg-secondary hover:text-foreground"
409
+ ),
410
+ children
411
+ }
412
+ );
413
+ }
414
+ function toStudioDevice(r) {
415
+ const meta = r.meta ?? {};
416
+ let platform = r.platform ?? meta.platform ?? r.kind ?? "android";
417
+ if (platform === "desktop") {
418
+ const os = String(meta.os ?? "");
419
+ if (os === "windows" || os === "macos") platform = os;
420
+ }
421
+ return { id: r.device_id, name: r.name || r.external_id, platform, online: r.online === true };
422
+ }
423
+ function DeviceSelect({ value, onChange, onDevices, disabled }) {
424
+ const { data } = useDevices();
425
+ const devices = useMemo(() => {
426
+ const list = normalizeDevices(data ?? []).map(toStudioDevice);
427
+ return [...list].sort((a, b) => Number(b.online) - Number(a.online));
428
+ }, [data]);
429
+ useEffect(() => {
430
+ onDevices?.(devices);
431
+ }, [devices]);
432
+ const options = useMemo(
433
+ () => [
434
+ { value: "", label: "\u9009\u62E9\u8FD0\u884C\u8BBE\u5907\u2026" },
435
+ ...devices.map((d) => ({
436
+ value: d.id,
437
+ label: `${d.online ? "\u25CF" : "\u25CB"} ${d.name} (${d.platform})`
438
+ }))
439
+ ],
440
+ [devices]
441
+ );
442
+ return /* @__PURE__ */ jsx(
443
+ Select,
444
+ {
445
+ value: value ?? "",
446
+ onChange: (v) => onChange(v === "" ? null : v),
447
+ options,
448
+ className: disabled ? "opacity-50 pointer-events-none" : void 0
449
+ }
450
+ );
451
+ }
452
+ var NEW_SCRIPT_CODE = "// await device.home()\n";
453
+ var NEW_SCRIPT_NAME = "New Script";
454
+ function isPristineNewScript(name, code, variables) {
455
+ const codeBlank = code.trim() === "" || code.trim() === NEW_SCRIPT_CODE.trim();
456
+ const nameBlank = name.trim() === "" || name.trim() === NEW_SCRIPT_NAME;
457
+ return codeBlank && nameBlank && Object.keys(variables).length === 0;
458
+ }
459
+ function ScriptStudio({
460
+ scriptId,
461
+ onScriptIdChange,
462
+ renderAgentPanel,
463
+ renderInspector,
464
+ consoleHeightStorageKey = "editor.consoleHeight",
465
+ headerExtra
466
+ }) {
467
+ const client = useClient();
468
+ const { get: getScript, save: saveScript } = useScripts();
469
+ const run = useScriptRun();
470
+ const [currentScript, setCurrentScript] = useState(null);
471
+ const [loading, setLoading] = useState(false);
472
+ const [code, setCode] = useState("");
473
+ const [name, setName] = useState("");
474
+ const [variables, setVariables] = useState({});
475
+ const [leftTab, setLeftTab] = useState("code");
476
+ const [selection, setSelection] = useState("");
477
+ const [inspectorOpen, setInspectorOpen] = useState(false);
478
+ const [saved, setSaved] = useState(true);
479
+ const [selectedDeviceId, setSelectedDeviceId] = useState(null);
480
+ const [devices, setDevices] = useState([]);
481
+ const mainRef = useRef(null);
482
+ const [consoleHeight, setConsoleHeight] = useState(() => {
483
+ const s = Number(localStorage.getItem(consoleHeightStorageKey));
484
+ return s >= 120 ? s : 240;
485
+ });
486
+ const handleResizeStart = useCallback(
487
+ (e) => {
488
+ e.preventDefault();
489
+ const startY = e.clientY;
490
+ const startH = consoleHeight;
491
+ const max = (mainRef.current?.clientHeight ?? window.innerHeight) - 160;
492
+ const onMove = (ev) => {
493
+ const next = Math.min(Math.max(startH + (startY - ev.clientY), 120), Math.max(max, 160));
494
+ setConsoleHeight(next);
495
+ };
496
+ const onUp = () => {
497
+ window.removeEventListener("pointermove", onMove);
498
+ window.removeEventListener("pointerup", onUp);
499
+ document.body.style.userSelect = "";
500
+ setConsoleHeight((h) => {
501
+ localStorage.setItem(consoleHeightStorageKey, String(h));
502
+ return h;
503
+ });
504
+ };
505
+ document.body.style.userSelect = "none";
506
+ window.addEventListener("pointermove", onMove);
507
+ window.addEventListener("pointerup", onUp);
508
+ },
509
+ [consoleHeight, consoleHeightStorageKey]
510
+ );
511
+ const idRef = useRef(scriptId);
512
+ const currentRef = useRef(null);
513
+ currentRef.current = currentScript;
514
+ useEffect(() => {
515
+ let alive = true;
516
+ if (scriptId && currentRef.current?.id !== scriptId) {
517
+ setLoading(true);
518
+ void getScript(scriptId).then((s) => {
519
+ if (alive) setCurrentScript(s);
520
+ }).finally(() => {
521
+ if (alive) setLoading(false);
522
+ });
523
+ }
524
+ return () => {
525
+ alive = false;
526
+ };
527
+ }, [scriptId]);
528
+ useEffect(() => () => run.reset(), []);
529
+ useEffect(() => {
530
+ if (currentScript) {
531
+ setCode(currentScript.code ?? "");
532
+ setName(currentScript.name ?? "");
533
+ setVariables(currentScript.variables ?? {});
534
+ } else if (!scriptId) {
535
+ setCode(NEW_SCRIPT_CODE);
536
+ setName(NEW_SCRIPT_NAME);
537
+ setVariables({});
538
+ }
539
+ }, [currentScript?.id, scriptId]);
540
+ useEffect(() => {
541
+ idRef.current = currentScript?.id ?? scriptId;
542
+ }, [currentScript?.id, scriptId]);
543
+ const saveTimer = useRef(null);
544
+ const savingRef = useRef(false);
545
+ const pendingRef = useRef(false);
546
+ const doSaveRef = useRef();
547
+ const doSave = useCallback(async (auto = false) => {
548
+ if (savingRef.current) {
549
+ pendingRef.current = true;
550
+ return;
551
+ }
552
+ savingRef.current = true;
553
+ try {
554
+ const cur = currentRef.current;
555
+ const s = {
556
+ ...cur ?? { name: "", code: "" },
557
+ id: idRef.current,
558
+ name,
559
+ code,
560
+ language: cur?.language ?? "ts",
561
+ variables,
562
+ platform: cur?.platform ?? "Android"
563
+ };
564
+ const hadId = idRef.current != null;
565
+ if (auto && !hadId && isPristineNewScript(name, code, variables)) {
566
+ setSaved(true);
567
+ return;
568
+ }
569
+ const result = await saveScript(s);
570
+ const savedId = result?.id ?? s.id;
571
+ if (savedId) idRef.current = savedId;
572
+ setCurrentScript({ ...s, id: savedId });
573
+ setSaved(true);
574
+ if (!hadId && savedId) onScriptIdChange?.(savedId);
575
+ } finally {
576
+ savingRef.current = false;
577
+ if (pendingRef.current) {
578
+ pendingRef.current = false;
579
+ void doSaveRef.current?.();
580
+ }
581
+ }
582
+ }, [name, code, variables, saveScript, onScriptIdChange]);
583
+ doSaveRef.current = doSave;
584
+ const markDirty = useCallback(() => {
585
+ setSaved(false);
586
+ if (saveTimer.current) clearTimeout(saveTimer.current);
587
+ saveTimer.current = setTimeout(() => {
588
+ void doSaveRef.current?.(true);
589
+ }, 1200);
590
+ }, []);
591
+ useEffect(() => {
592
+ return () => {
593
+ if (saveTimer.current) clearTimeout(saveTimer.current);
594
+ };
595
+ }, []);
596
+ const targetId = currentScript?.id || scriptId || void 0;
597
+ const canRun = !!selectedDeviceId;
598
+ const handleRunAll = useCallback(async () => {
599
+ await doSave();
600
+ if (!selectedDeviceId) return;
601
+ await run.runCode(
602
+ code,
603
+ currentRef.current?.language ?? "ts",
604
+ variables,
605
+ [selectedDeviceId],
606
+ currentRef.current?.platform ?? "Android"
607
+ );
608
+ }, [doSave, code, variables, selectedDeviceId, run]);
609
+ const handleRunSelection = useCallback(async () => {
610
+ if (!selection.trim() || !selectedDeviceId) return;
611
+ await run.runCode(
612
+ selection,
613
+ currentRef.current?.language ?? "ts",
614
+ variables,
615
+ [selectedDeviceId],
616
+ currentRef.current?.platform ?? "Android"
617
+ );
618
+ }, [selection, variables, selectedDeviceId, run]);
619
+ const platform = devices.find((d) => d.id === selectedDeviceId)?.platform === "ios" ? "ios" : "android";
620
+ const insertSnippet = useCallback(
621
+ (snippet) => {
622
+ setCode((c) => (c.endsWith("\n") || c === "" ? c : c + "\n") + snippet);
623
+ markDirty();
624
+ },
625
+ [markDirty]
626
+ );
627
+ const slotCtx = useMemo(
628
+ () => ({
629
+ scriptId: targetId,
630
+ deviceId: selectedDeviceId,
631
+ platform,
632
+ code,
633
+ setCode: (next) => {
634
+ setCode(next);
635
+ markDirty();
636
+ },
637
+ insertSnippet,
638
+ openInspector: () => setInspectorOpen(true)
639
+ }),
640
+ [targetId, selectedDeviceId, platform, code, markDirty, insertSnippet]
641
+ );
642
+ if (loading)
643
+ return /* @__PURE__ */ jsx("div", { className: "flex-1 flex items-center justify-center text-muted-foreground text-body-sm", children: "Loading\u2026" });
644
+ return /* @__PURE__ */ jsxs("div", { className: "flex flex-col h-full overflow-hidden", children: [
645
+ /* @__PURE__ */ jsxs("header", { className: "h-14 px-4 sm:px-6 bg-card border-b border-border flex items-center justify-between gap-3 flex-shrink-0", children: [
646
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1 text-muted-foreground text-body-sm min-w-0", children: [
647
+ /* @__PURE__ */ jsx("span", { children: "Scripts" }),
648
+ /* @__PURE__ */ jsx(ChevronRight, { className: "w-4 h-4" }),
649
+ /* @__PURE__ */ jsx(
650
+ "input",
651
+ {
652
+ className: "text-foreground font-semibold bg-transparent outline-none border-b border-transparent hover:border-border focus:border-primary min-w-0 truncate px-0.5",
653
+ value: name,
654
+ placeholder: "New Script",
655
+ "aria-label": "\u811A\u672C\u540D",
656
+ onChange: (e) => {
657
+ setName(e.target.value);
658
+ markDirty();
659
+ }
660
+ }
661
+ ),
662
+ /* @__PURE__ */ jsx("span", { className: cn("ml-2 text-xs", saved ? "text-success" : "text-muted-foreground"), children: saved ? "\u25CF \u5DF2\u81EA\u52A8\u4FDD\u5B58" : "\u25CB \u672A\u4FDD\u5B58" })
663
+ ] }),
664
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 flex-shrink-0", children: [
665
+ renderInspector && /* @__PURE__ */ jsx(
666
+ "button",
667
+ {
668
+ className: "text-body-sm text-muted-foreground hover:text-foreground",
669
+ onClick: () => setInspectorOpen(true),
670
+ children: "\u8BBE\u5907/Inspector"
671
+ }
672
+ ),
673
+ /* @__PURE__ */ jsx("div", { className: "w-52", children: /* @__PURE__ */ jsx(
674
+ DeviceSelect,
675
+ {
676
+ value: selectedDeviceId,
677
+ onChange: setSelectedDeviceId,
678
+ onDevices: setDevices
679
+ }
680
+ ) }),
681
+ /* @__PURE__ */ jsxs(Button, { variant: "secondary", onClick: () => doSave(), children: [
682
+ /* @__PURE__ */ jsx(Save, { className: "w-3.5 h-3.5" }),
683
+ "Save"
684
+ ] }),
685
+ /* @__PURE__ */ jsxs(
686
+ Button,
687
+ {
688
+ variant: "primary",
689
+ onClick: handleRunAll,
690
+ disabled: !canRun,
691
+ title: canRun ? void 0 : "\u5148\u9009\u62E9\u8BBE\u5907",
692
+ children: [
693
+ /* @__PURE__ */ jsx(Play, { className: "w-3.5 h-3.5" }),
694
+ "\u8FD0\u884C\u5168\u90E8"
695
+ ]
696
+ }
697
+ ),
698
+ headerExtra
699
+ ] })
700
+ ] }),
701
+ /* @__PURE__ */ jsxs("div", { ref: mainRef, className: "flex-1 flex flex-col overflow-hidden min-h-0", children: [
702
+ /* @__PURE__ */ jsxs("div", { className: "flex-1 flex overflow-hidden min-h-0", children: [
703
+ /* @__PURE__ */ jsxs("section", { className: "flex-1 flex flex-col bg-surface border-r border-border min-w-0", children: [
704
+ /* @__PURE__ */ jsxs("div", { className: "px-2 py-2 bg-card border-b border-border flex items-center gap-1", children: [
705
+ [
706
+ ["code", "Code"],
707
+ ["params", "\u53C2\u6570"],
708
+ ["settings", "\u8BBE\u7F6E"]
709
+ ].map(([k, l]) => /* @__PURE__ */ jsx(
710
+ "button",
711
+ {
712
+ onClick: () => setLeftTab(k),
713
+ className: cn(
714
+ "rounded px-3 py-1.5 text-body-sm font-medium",
715
+ leftTab === k ? "bg-primary/10 text-primary" : "text-muted-foreground hover:text-foreground"
716
+ ),
717
+ children: l
718
+ },
719
+ k
720
+ )),
721
+ leftTab === "code" && /* @__PURE__ */ jsx(
722
+ "button",
723
+ {
724
+ className: "ml-auto text-body-sm text-success disabled:text-muted-foreground",
725
+ onClick: handleRunSelection,
726
+ disabled: !selection.trim() || !canRun,
727
+ children: "\u25B6 \u8FD0\u884C\u9009\u4E2D"
728
+ }
729
+ )
730
+ ] }),
731
+ /* @__PURE__ */ jsxs("div", { className: "flex-1 overflow-hidden", children: [
732
+ /* @__PURE__ */ jsx("div", { className: cn("h-full", leftTab !== "code" && "hidden"), children: /* @__PURE__ */ jsx(
733
+ CodeEditor,
734
+ {
735
+ value: code,
736
+ language: currentScript?.language === "js" ? "js" : "ts",
737
+ onChange: (v) => {
738
+ setCode(v);
739
+ markDirty();
740
+ },
741
+ onSelect: setSelection
742
+ }
743
+ ) }),
744
+ leftTab === "params" && /* @__PURE__ */ jsx(
745
+ ScriptParamEditor,
746
+ {
747
+ variables,
748
+ onChange: (v) => {
749
+ setVariables(v);
750
+ markDirty();
751
+ }
752
+ }
753
+ ),
754
+ leftTab === "settings" && /* @__PURE__ */ jsx(
755
+ SettingsTab,
756
+ {
757
+ script: currentScript,
758
+ onRefresh: async () => {
759
+ const id = currentScript?.id ?? scriptId;
760
+ if (id) setCurrentScript(await getScript(id));
761
+ },
762
+ generateMeta: (id) => client.scripts.generateMeta(id),
763
+ generateCover: (id) => client.scripts.generateCover(id)
764
+ }
765
+ )
766
+ ] })
767
+ ] }),
768
+ renderAgentPanel && /* @__PURE__ */ jsx("aside", { className: "w-[420px] flex flex-col bg-card flex-shrink-0", children: renderAgentPanel(slotCtx) })
769
+ ] }),
770
+ /* @__PURE__ */ jsx("div", { className: "flex-shrink-0 overflow-hidden", style: { height: consoleHeight }, children: /* @__PURE__ */ jsx(
771
+ RunPanel,
772
+ {
773
+ status: run.status,
774
+ progress: run.progress,
775
+ steps: run.steps,
776
+ logs: run.logs,
777
+ error: run.error,
778
+ onStart: handleRunAll,
779
+ onStop: run.stopRun,
780
+ onPause: run.pauseRun,
781
+ onResume: run.resumeRun,
782
+ onClearLogs: run.clearLogs,
783
+ canStart: canRun,
784
+ startHint: canRun ? void 0 : "\u5148\u9009\u62E9\u8BBE\u5907",
785
+ onResizeStart: handleResizeStart
786
+ }
787
+ ) })
788
+ ] }),
789
+ renderInspector?.({
790
+ ...slotCtx,
791
+ open: inspectorOpen,
792
+ onClose: () => setInspectorOpen(false)
793
+ })
794
+ ] });
795
+ }
796
+ function SettingsTab({ script, onRefresh, generateMeta, generateCover }) {
797
+ const id = script?.id ?? null;
798
+ const [metaBusy, setMetaBusy] = useState(false);
799
+ const [coverBusy, setCoverBusy] = useState(false);
800
+ const [error, setError] = useState(null);
801
+ const runMeta = useCallback(async () => {
802
+ if (!id) return;
803
+ setMetaBusy(true);
804
+ setError(null);
805
+ try {
806
+ await generateMeta(id);
807
+ await onRefresh();
808
+ } catch (e) {
809
+ setError(e instanceof Error ? e.message : String(e));
810
+ } finally {
811
+ setMetaBusy(false);
812
+ }
813
+ }, [id, onRefresh, generateMeta]);
814
+ const runCover = useCallback(async () => {
815
+ if (!id) return;
816
+ setCoverBusy(true);
817
+ setError(null);
818
+ try {
819
+ await generateCover(id);
820
+ await onRefresh();
821
+ } catch (e) {
822
+ setError(e instanceof Error ? e.message : String(e));
823
+ } finally {
824
+ setCoverBusy(false);
825
+ }
826
+ }, [id, onRefresh, generateCover]);
827
+ return /* @__PURE__ */ jsxs("div", { className: "p-4 space-y-3 overflow-auto h-full", children: [
828
+ /* @__PURE__ */ jsx("div", { className: "text-body-sm text-muted-foreground", children: "\u6807\u9898 / \u56FE\u6807 / \u63CF\u8FF0\uFF08\u5E02\u573A\u7528\uFF09\u3002" }),
829
+ !id && /* @__PURE__ */ jsx("div", { className: "text-body-sm text-amber-500", children: "\u5148\u4FDD\u5B58\u811A\u672C\u518D\u751F\u6210\u3002" }),
830
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3", children: [
831
+ script?.icon_url ? /* @__PURE__ */ jsx("img", { src: script.icon_url, alt: "", className: "w-12 h-12 rounded" }) : /* @__PURE__ */ jsx("div", { className: "w-12 h-12 rounded bg-muted" }),
832
+ /* @__PURE__ */ jsx(
833
+ "button",
834
+ {
835
+ className: "text-primary text-body-sm disabled:opacity-50",
836
+ disabled: !id || coverBusy,
837
+ onClick: runCover,
838
+ children: coverBusy ? "\u751F\u6210\u4E2D\u2026" : "\u751F\u6210\u5C01\u9762/\u56FE\u6807"
839
+ }
840
+ )
841
+ ] }),
842
+ /* @__PURE__ */ jsx(
843
+ "button",
844
+ {
845
+ className: "text-primary text-body-sm disabled:opacity-50",
846
+ disabled: !id || metaBusy,
847
+ onClick: runMeta,
848
+ children: metaBusy ? "\u751F\u6210\u4E2D\u2026" : "\u751F\u6210\u6807\u9898/\u63CF\u8FF0"
849
+ }
850
+ ),
851
+ script?.cover_url && /* @__PURE__ */ jsx("img", { src: script.cover_url, alt: "", className: "w-full rounded object-cover" }),
852
+ /* @__PURE__ */ jsx(
853
+ "textarea",
854
+ {
855
+ className: "w-full bg-secondary rounded px-2 py-1 text-body-sm",
856
+ rows: 4,
857
+ value: script?.description ?? "",
858
+ readOnly: true,
859
+ placeholder: "\u63CF\u8FF0\uFF08\u53EF\u7531 LLM \u751F\u6210\uFF09"
860
+ }
861
+ ),
862
+ error && /* @__PURE__ */ jsx("div", { className: "text-body-sm text-red-500", children: error })
863
+ ] });
864
+ }
865
+
866
+ export { CodeEditor, DeviceSelect, RunPanel, ScriptParamEditor, ScriptStudio, TimelineStep };