@nuxtagentic/automations 0.0.0-stage → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +9 -0
- package/README.md +173 -2
- package/client.ts +24 -0
- package/module.ts +55 -0
- package/package.json +59 -3
- package/runtime/components/AutomationCanvas.vue +488 -0
- package/runtime/components/AutomationInspector.vue +436 -0
- package/runtime/components/AutomationNodeIcon.vue +27 -0
- package/runtime/components/AutomationRunsPanel.vue +475 -0
- package/runtime/components/AutomationSidebar.vue +253 -0
- package/runtime/components/AutomationStatusIcon.vue +44 -0
- package/runtime/pages/automations.vue +1181 -0
- package/runtime/server/api/automations/[id]/endpoint.get.ts +12 -0
- package/runtime/server/api/automations/[id]/run.post.ts +30 -0
- package/runtime/server/api/automations/events.post.ts +37 -0
- package/runtime/server/api/automations/runs/[id]/resume.post.ts +23 -0
- package/runtime/server/handlers/automation-hook.post.ts +51 -0
- package/runtime/server/handlers/connect-hook.post.ts +54 -0
- package/runtime/server/handlers/integration-hook.post.ts +86 -0
- package/runtime/server/plugins/events.ts +79 -0
- package/runtime/server/plugins/maintenance.ts +20 -0
- package/runtime/server/utils/agent-runner.ts +15 -0
- package/runtime/server/utils/automation-context.ts +77 -0
- package/runtime/server/utils/automation-engine.ts +697 -0
- package/runtime/server/utils/automation-launch.ts +205 -0
- package/runtime/server/utils/automation-schedule.ts +31 -0
- package/runtime/server/utils/connection-events.ts +114 -0
- package/runtime/server/utils/maintenance.ts +255 -0
- package/runtime/server/utils/schedule-sync.ts +32 -0
- package/runtime/server/utils/transient-retry.ts +34 -0
- package/runtime/server/workflows/runner.ts +39 -0
- package/runtime/shared/agent.ts +5 -0
- package/runtime/shared/automation-run.ts +61 -0
- package/runtime/shared/automation.ts +202 -0
- package/runtime/workspace.ts +56 -0
- package/schema.ts +222 -0
- package/server.ts +2 -0
- package/workflow.ts +2 -0
|
@@ -0,0 +1,488 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed, onBeforeUnmount, shallowRef, useTemplateRef, watch } from 'vue'
|
|
3
|
+
import { usePreferredReducedMotion } from '@vueuse/core'
|
|
4
|
+
import type { AutomationNode, AutomationPositions, AutomationStep } from '../shared/automation'
|
|
5
|
+
import {
|
|
6
|
+
getStatusColor,
|
|
7
|
+
formatStepDuration,
|
|
8
|
+
getStepStatus,
|
|
9
|
+
type DisplayStatus,
|
|
10
|
+
} from '../shared/automation-run'
|
|
11
|
+
import AutomationStatusIcon from './AutomationStatusIcon.vue'
|
|
12
|
+
import AutomationNodeIcon from './AutomationNodeIcon.vue'
|
|
13
|
+
const savedPositions = defineModel<AutomationPositions>('positions', { required: true })
|
|
14
|
+
const positions = shallowRef(savedPositions.value)
|
|
15
|
+
watch(savedPositions, (value) => {
|
|
16
|
+
positions.value = value
|
|
17
|
+
})
|
|
18
|
+
const savedView = defineModel<{ x: number; y: number; z: number }>('view', { required: true })
|
|
19
|
+
const view = shallowRef(savedView.value)
|
|
20
|
+
watch(savedView, (value) => {
|
|
21
|
+
view.value = value
|
|
22
|
+
})
|
|
23
|
+
const { selected, summaries, steps } = defineProps<{
|
|
24
|
+
selected: AutomationNode | null
|
|
25
|
+
summaries: Record<AutomationNode, { main: string; sub: string }>
|
|
26
|
+
steps: AutomationStep[]
|
|
27
|
+
}>()
|
|
28
|
+
const reducedMotion = usePreferredReducedMotion()
|
|
29
|
+
const canvasElement = useTemplateRef('canvasElement')
|
|
30
|
+
const emit = defineEmits<{
|
|
31
|
+
select: [node: AutomationNode | null]
|
|
32
|
+
dragging: [active: boolean]
|
|
33
|
+
}>()
|
|
34
|
+
const nodes = [
|
|
35
|
+
{ id: 'trigger', label: 'Trigger', icon: 'i-lucide-zap', color: 'text-warning' },
|
|
36
|
+
{ id: 'agent', label: 'Agent call', icon: 'i-lucide-bot', color: 'text-info' },
|
|
37
|
+
{ id: 'success', label: 'On success', icon: 'i-lucide-check', color: 'text-success' },
|
|
38
|
+
{ id: 'error', label: 'On error', icon: 'i-lucide-triangle-alert', color: 'text-error' },
|
|
39
|
+
] as const
|
|
40
|
+
const nodeWidth = 280
|
|
41
|
+
const nodeHeight = 114
|
|
42
|
+
type Point = { x: number; y: number }
|
|
43
|
+
interface AlignmentGuide {
|
|
44
|
+
axis: 'x' | 'y'
|
|
45
|
+
value: number
|
|
46
|
+
start: number
|
|
47
|
+
end: number
|
|
48
|
+
}
|
|
49
|
+
const guides = shallowRef<AlignmentGuide[]>([])
|
|
50
|
+
function snapNode(node: AutomationNode, point: Point): Point {
|
|
51
|
+
const snapped = { ...point }
|
|
52
|
+
const tolerance = 6 / view.value.z
|
|
53
|
+
const candidates = nodes
|
|
54
|
+
.filter((entry) => entry.id !== node)
|
|
55
|
+
.map((entry) => positions.value[entry.id])
|
|
56
|
+
const matches: { axis: 'x' | 'y'; value: number }[] = []
|
|
57
|
+
for (const axis of ['x', 'y'] as const) {
|
|
58
|
+
const size = axis === 'x' ? nodeWidth : nodeHeight
|
|
59
|
+
const offsets = [size / 2, 0, size]
|
|
60
|
+
let best: { delta: number; value: number; offset: number } | undefined
|
|
61
|
+
for (const candidate of candidates) {
|
|
62
|
+
for (const targetOffset of offsets) {
|
|
63
|
+
const value = candidate[axis] + targetOffset
|
|
64
|
+
for (const offset of offsets) {
|
|
65
|
+
const delta = value - (point[axis] + offset)
|
|
66
|
+
if (Math.abs(delta) > tolerance || (best && Math.abs(delta) >= Math.abs(best.delta))) {
|
|
67
|
+
continue
|
|
68
|
+
}
|
|
69
|
+
best = { delta, value, offset }
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
if (best) {
|
|
74
|
+
snapped[axis] = best.value - best.offset
|
|
75
|
+
matches.push({ axis, value: best.value })
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
guides.value = matches.map(({ axis, value }) => {
|
|
79
|
+
const otherAxis = axis === 'x' ? 'y' : 'x'
|
|
80
|
+
const size = axis === 'x' ? nodeWidth : nodeHeight
|
|
81
|
+
const otherSize = axis === 'x' ? nodeHeight : nodeWidth
|
|
82
|
+
const aligned = candidates.filter((candidate) =>
|
|
83
|
+
[0, size / 2, size].some((offset) => Math.abs(candidate[axis] + offset - value) < 0.001),
|
|
84
|
+
)
|
|
85
|
+
const starts = [snapped[otherAxis], ...aligned.map((candidate) => candidate[otherAxis])]
|
|
86
|
+
const padding = 8 / view.value.z
|
|
87
|
+
return {
|
|
88
|
+
axis,
|
|
89
|
+
value,
|
|
90
|
+
start: Math.min(...starts) - padding,
|
|
91
|
+
end: Math.max(...starts) + otherSize + padding,
|
|
92
|
+
}
|
|
93
|
+
})
|
|
94
|
+
return snapped
|
|
95
|
+
}
|
|
96
|
+
const nodeStep = (id: AutomationNode) => steps.find((entry) => entry.node === id)
|
|
97
|
+
const nodeState = (id: AutomationNode) => getStepStatus(nodeStep(id))
|
|
98
|
+
const gridSize = computed(() => {
|
|
99
|
+
let size = 24 * view.value.z
|
|
100
|
+
while (size < 12) {
|
|
101
|
+
size *= 2
|
|
102
|
+
}
|
|
103
|
+
return size
|
|
104
|
+
})
|
|
105
|
+
const finishedLabels: Record<string, string> = { succeeded: 'Done', failed: 'Failed' }
|
|
106
|
+
function nodeLabel(id: AutomationNode) {
|
|
107
|
+
const step = nodeStep(id)
|
|
108
|
+
const state = nodeState(id)
|
|
109
|
+
if (state === 'retry') {
|
|
110
|
+
return `Retry ${(step?.attempt ?? 1) - 1}/${step?.retryLimit ?? 0}`
|
|
111
|
+
}
|
|
112
|
+
if (state === 'running') {
|
|
113
|
+
return 'Running'
|
|
114
|
+
}
|
|
115
|
+
if (state === 'skipped') {
|
|
116
|
+
return 'Skipped'
|
|
117
|
+
}
|
|
118
|
+
const label = finishedLabels[state ?? ''] ?? ''
|
|
119
|
+
const duration = formatStepDuration(step)
|
|
120
|
+
return label + (label && duration ? ` · ${duration}` : '')
|
|
121
|
+
}
|
|
122
|
+
function nodeBorderClass(isSelected: boolean, state: DisplayStatus) {
|
|
123
|
+
if (isSelected || state === 'running' || state === 'retry') {
|
|
124
|
+
return 'border-(--gray-800)'
|
|
125
|
+
}
|
|
126
|
+
if (state === 'succeeded') {
|
|
127
|
+
return 'border-[color-mix(in_oklch,var(--ui-success)_55%,var(--gray-500))]'
|
|
128
|
+
}
|
|
129
|
+
if (state === 'failed') {
|
|
130
|
+
return 'border-[color-mix(in_oklch,var(--ui-error)_60%,var(--gray-500))]'
|
|
131
|
+
}
|
|
132
|
+
return 'border-(--gray-500)'
|
|
133
|
+
}
|
|
134
|
+
function nodeShadowClass(isSelected: boolean, state: DisplayStatus) {
|
|
135
|
+
if (isSelected) {
|
|
136
|
+
return 'shadow-[0_0_0_1px_var(--gray-800),0_8px_24px_rgba(0,0,0,0.4)]'
|
|
137
|
+
}
|
|
138
|
+
if (state === 'running' || state === 'retry') {
|
|
139
|
+
return 'shadow-[0_0_0_4px_color-mix(in_oklch,var(--ui-text-highlighted)_8%,transparent),0_4px_16px_rgba(0,0,0,0.25)]'
|
|
140
|
+
}
|
|
141
|
+
return 'shadow-[0_4px_16px_rgba(0,0,0,0.25)]'
|
|
142
|
+
}
|
|
143
|
+
function nodeClasses(id: AutomationNode) {
|
|
144
|
+
const state = nodeState(id)
|
|
145
|
+
const isSelected = selected === id
|
|
146
|
+
return [
|
|
147
|
+
nodeBorderClass(isSelected, state),
|
|
148
|
+
nodeShadowClass(isSelected, state),
|
|
149
|
+
state === 'skipped' && 'opacity-45',
|
|
150
|
+
]
|
|
151
|
+
}
|
|
152
|
+
const edges = computed(() =>
|
|
153
|
+
[
|
|
154
|
+
['trigger', 'agent'],
|
|
155
|
+
['agent', 'success'],
|
|
156
|
+
['agent', 'error'],
|
|
157
|
+
].map(([from, to]) => {
|
|
158
|
+
const start = positions.value[from as AutomationNode]
|
|
159
|
+
const end = positions.value[to as AutomationNode]
|
|
160
|
+
const source = { x: start.x + nodeWidth / 2, y: start.y + nodeHeight }
|
|
161
|
+
const target = { x: end.x + nodeWidth / 2, y: end.y - 2 }
|
|
162
|
+
const middle = (source.y + target.y) / 2
|
|
163
|
+
return {
|
|
164
|
+
id: to as AutomationNode,
|
|
165
|
+
source,
|
|
166
|
+
target,
|
|
167
|
+
middle,
|
|
168
|
+
path: `M${source.x},${source.y} C${source.x},${middle} ${target.x},${middle} ${target.x},${target.y}`,
|
|
169
|
+
status: nodeState(to as AutomationNode),
|
|
170
|
+
}
|
|
171
|
+
}),
|
|
172
|
+
)
|
|
173
|
+
function edgeColor(edge: (typeof edges.value)[number]) {
|
|
174
|
+
const { id, status } = edge
|
|
175
|
+
if (!status) {
|
|
176
|
+
return 'var(--gray-700)'
|
|
177
|
+
}
|
|
178
|
+
if (status === 'skipped') {
|
|
179
|
+
return 'var(--gray-500)'
|
|
180
|
+
}
|
|
181
|
+
if (id === 'error') {
|
|
182
|
+
return 'var(--ui-error)'
|
|
183
|
+
}
|
|
184
|
+
return 'var(--ui-success)'
|
|
185
|
+
}
|
|
186
|
+
function getEdgeLabelClass({ id, status }: (typeof edges.value)[number]) {
|
|
187
|
+
if (!status || status === 'skipped') {
|
|
188
|
+
return 'border-(--gray-500) text-muted'
|
|
189
|
+
}
|
|
190
|
+
return id === 'error' ? 'border-error text-error' : 'border-success text-success'
|
|
191
|
+
}
|
|
192
|
+
const drag = shallowRef<{
|
|
193
|
+
pointer: number
|
|
194
|
+
node: AutomationNode | null
|
|
195
|
+
x: number
|
|
196
|
+
y: number
|
|
197
|
+
startX: number
|
|
198
|
+
startY: number
|
|
199
|
+
moved: boolean
|
|
200
|
+
scale: number
|
|
201
|
+
unsnapped?: Point
|
|
202
|
+
}>()
|
|
203
|
+
let frame = 0
|
|
204
|
+
let pendingMove: { x: number; y: number } | undefined
|
|
205
|
+
let pendingView: typeof view.value | undefined
|
|
206
|
+
let wheelTimer: ReturnType<typeof setTimeout> | undefined
|
|
207
|
+
function commitView() {
|
|
208
|
+
clearTimeout(wheelTimer)
|
|
209
|
+
flushFrame()
|
|
210
|
+
if (savedView.value !== view.value) {
|
|
211
|
+
savedView.value = view.value
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
function flushFrame() {
|
|
215
|
+
if (frame) {
|
|
216
|
+
cancelAnimationFrame(frame)
|
|
217
|
+
}
|
|
218
|
+
frame = 0
|
|
219
|
+
if (pendingView) {
|
|
220
|
+
view.value = pendingView
|
|
221
|
+
pendingView = undefined
|
|
222
|
+
}
|
|
223
|
+
const point = pendingMove
|
|
224
|
+
pendingMove = undefined
|
|
225
|
+
if (!drag.value || !point) {
|
|
226
|
+
return
|
|
227
|
+
}
|
|
228
|
+
// Embedded previews scale the surface; pointer deltas must use its local coordinates.
|
|
229
|
+
const dx = (point.x - drag.value.x) / drag.value.scale
|
|
230
|
+
const dy = (point.y - drag.value.y) / drag.value.scale
|
|
231
|
+
if (!dx && !dy) {
|
|
232
|
+
return
|
|
233
|
+
}
|
|
234
|
+
if (Math.abs(point.x - drag.value.startX) + Math.abs(point.y - drag.value.startY) > 2) {
|
|
235
|
+
drag.value.moved = true
|
|
236
|
+
}
|
|
237
|
+
if (drag.value.node && drag.value.unsnapped) {
|
|
238
|
+
// Keep the pointer position independent of snapping so nodes release naturally.
|
|
239
|
+
const point = {
|
|
240
|
+
x: drag.value.unsnapped.x + dx / view.value.z,
|
|
241
|
+
y: drag.value.unsnapped.y + dy / view.value.z,
|
|
242
|
+
}
|
|
243
|
+
drag.value.unsnapped = point
|
|
244
|
+
positions.value = {
|
|
245
|
+
...positions.value,
|
|
246
|
+
[drag.value.node]: drag.value.moved ? snapNode(drag.value.node, point) : point,
|
|
247
|
+
}
|
|
248
|
+
} else {
|
|
249
|
+
view.value = { ...view.value, x: view.value.x + dx, y: view.value.y + dy }
|
|
250
|
+
}
|
|
251
|
+
drag.value.x = point.x
|
|
252
|
+
drag.value.y = point.y
|
|
253
|
+
}
|
|
254
|
+
onBeforeUnmount(() => {
|
|
255
|
+
if (frame) {
|
|
256
|
+
cancelAnimationFrame(frame)
|
|
257
|
+
}
|
|
258
|
+
clearTimeout(wheelTimer)
|
|
259
|
+
})
|
|
260
|
+
function down(event: PointerEvent, node: AutomationNode | null = null) {
|
|
261
|
+
if (event.button !== 0 || (!node && (event.target as HTMLElement).closest('button'))) {
|
|
262
|
+
return
|
|
263
|
+
}
|
|
264
|
+
emit('dragging', true)
|
|
265
|
+
commitView()
|
|
266
|
+
guides.value = []
|
|
267
|
+
const element = event.currentTarget as HTMLElement
|
|
268
|
+
element.setPointerCapture(event.pointerId)
|
|
269
|
+
drag.value = {
|
|
270
|
+
pointer: event.pointerId,
|
|
271
|
+
node,
|
|
272
|
+
x: event.clientX,
|
|
273
|
+
y: event.clientY,
|
|
274
|
+
startX: event.clientX,
|
|
275
|
+
startY: event.clientY,
|
|
276
|
+
moved: false,
|
|
277
|
+
scale: getCanvasScale(),
|
|
278
|
+
unsnapped: node ? { ...positions.value[node] } : undefined,
|
|
279
|
+
}
|
|
280
|
+
if (node) {
|
|
281
|
+
emit('select', node)
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
function move(event: PointerEvent) {
|
|
285
|
+
if (!drag.value || event.pointerId !== drag.value.pointer) {
|
|
286
|
+
return
|
|
287
|
+
}
|
|
288
|
+
pendingMove = { x: event.clientX, y: event.clientY }
|
|
289
|
+
if (!frame) {
|
|
290
|
+
frame = requestAnimationFrame(flushFrame)
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
function up() {
|
|
294
|
+
flushFrame()
|
|
295
|
+
if (drag.value?.node) {
|
|
296
|
+
savedPositions.value = positions.value
|
|
297
|
+
} else if (drag.value) {
|
|
298
|
+
commitView()
|
|
299
|
+
}
|
|
300
|
+
if (drag.value && !drag.value.node && !drag.value.moved) {
|
|
301
|
+
emit('select', null)
|
|
302
|
+
}
|
|
303
|
+
drag.value = undefined
|
|
304
|
+
guides.value = []
|
|
305
|
+
emit('dragging', false)
|
|
306
|
+
}
|
|
307
|
+
function wheel(event: WheelEvent) {
|
|
308
|
+
const current = pendingView ?? view.value
|
|
309
|
+
if (event.ctrlKey || event.metaKey) {
|
|
310
|
+
pendingView = {
|
|
311
|
+
...current,
|
|
312
|
+
z: Math.max(0.35, Math.min(1.6, current.z * Math.exp(-event.deltaY * 0.005))),
|
|
313
|
+
}
|
|
314
|
+
} else {
|
|
315
|
+
const scale = getCanvasScale()
|
|
316
|
+
pendingView = {
|
|
317
|
+
...current,
|
|
318
|
+
x: current.x - event.deltaX / scale,
|
|
319
|
+
y: current.y - event.deltaY / scale,
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
if (!frame) {
|
|
323
|
+
frame = requestAnimationFrame(flushFrame)
|
|
324
|
+
}
|
|
325
|
+
clearTimeout(wheelTimer)
|
|
326
|
+
wheelTimer = setTimeout(commitView, 100)
|
|
327
|
+
}
|
|
328
|
+
function getCanvasScale() {
|
|
329
|
+
const element = canvasElement.value
|
|
330
|
+
return element?.clientWidth ? element.getBoundingClientRect().width / element.clientWidth : 1
|
|
331
|
+
}
|
|
332
|
+
function keyboard(event: KeyboardEvent, node: AutomationNode) {
|
|
333
|
+
const direction = { ArrowLeft: [-1, 0], ArrowRight: [1, 0], ArrowUp: [0, -1], ArrowDown: [0, 1] }[
|
|
334
|
+
event.key
|
|
335
|
+
]
|
|
336
|
+
if (!direction) {
|
|
337
|
+
return
|
|
338
|
+
}
|
|
339
|
+
event.preventDefault()
|
|
340
|
+
const current = positions.value[node]
|
|
341
|
+
const amount = event.shiftKey ? 50 : 10
|
|
342
|
+
positions.value = {
|
|
343
|
+
...positions.value,
|
|
344
|
+
[node]: { x: current.x + direction[0]! * amount, y: current.y + direction[1]! * amount },
|
|
345
|
+
}
|
|
346
|
+
savedPositions.value = positions.value
|
|
347
|
+
}
|
|
348
|
+
</script>
|
|
349
|
+
|
|
350
|
+
<template>
|
|
351
|
+
<div
|
|
352
|
+
ref="canvasElement"
|
|
353
|
+
class="relative min-h-0 min-w-0 flex-1 touch-none overflow-hidden bg-(--gray-200) [background-image:radial-gradient(circle,var(--gray-500)_1px,transparent_1.5px)]"
|
|
354
|
+
:style="{
|
|
355
|
+
backgroundSize: `${gridSize}px ${gridSize}px`,
|
|
356
|
+
backgroundPosition: `calc(50% + ${view.x}px) ${view.y}px`,
|
|
357
|
+
}"
|
|
358
|
+
:class="drag ? 'cursor-grabbing' : 'cursor-grab'"
|
|
359
|
+
aria-label="Automation canvas. Drag nodes to move them. Use arrow keys on a focused node."
|
|
360
|
+
@pointerdown="down($event)"
|
|
361
|
+
@pointermove="move"
|
|
362
|
+
@pointerup="up"
|
|
363
|
+
@pointercancel="up"
|
|
364
|
+
@wheel.prevent="wheel"
|
|
365
|
+
>
|
|
366
|
+
<div
|
|
367
|
+
class="absolute top-0 left-1/2 origin-top-left will-change-transform"
|
|
368
|
+
:style="{ transform: `translate3d(${view.x}px,${view.y}px,0) scale(${view.z})` }"
|
|
369
|
+
>
|
|
370
|
+
<svg
|
|
371
|
+
class="pointer-events-none absolute top-0 left-0 size-px overflow-visible"
|
|
372
|
+
aria-hidden="true"
|
|
373
|
+
>
|
|
374
|
+
<g
|
|
375
|
+
v-for="edge in edges"
|
|
376
|
+
:key="edge.id"
|
|
377
|
+
>
|
|
378
|
+
<path
|
|
379
|
+
:d="edge.path"
|
|
380
|
+
fill="none"
|
|
381
|
+
:stroke="edgeColor(edge)"
|
|
382
|
+
stroke-width="1.5"
|
|
383
|
+
:stroke-dasharray="
|
|
384
|
+
edge.status === 'running' || edge.status === 'retry' ? '5 5' : undefined
|
|
385
|
+
"
|
|
386
|
+
class="transition-colors duration-240 motion-reduce:transition-none"
|
|
387
|
+
>
|
|
388
|
+
<animate
|
|
389
|
+
v-if="
|
|
390
|
+
reducedMotion !== 'reduce' && (edge.status === 'running' || edge.status === 'retry')
|
|
391
|
+
"
|
|
392
|
+
attributeName="stroke-dashoffset"
|
|
393
|
+
from="0"
|
|
394
|
+
to="-20"
|
|
395
|
+
dur="0.6s"
|
|
396
|
+
repeatCount="indefinite"
|
|
397
|
+
/>
|
|
398
|
+
</path>
|
|
399
|
+
<path
|
|
400
|
+
:d="`M${edge.target.x - 4},${edge.target.y - 6} L${edge.target.x},${edge.target.y} L${edge.target.x + 4},${edge.target.y - 6}`"
|
|
401
|
+
fill="none"
|
|
402
|
+
:stroke="edgeColor(edge)"
|
|
403
|
+
stroke-width="1.5"
|
|
404
|
+
stroke-linecap="round"
|
|
405
|
+
stroke-linejoin="round"
|
|
406
|
+
class="transition-colors duration-240 motion-reduce:transition-none"
|
|
407
|
+
/>
|
|
408
|
+
<circle
|
|
409
|
+
:cx="edge.source.x"
|
|
410
|
+
:cy="edge.source.y"
|
|
411
|
+
r="3.5"
|
|
412
|
+
:fill="edgeColor(edge)"
|
|
413
|
+
/>
|
|
414
|
+
</g>
|
|
415
|
+
<line
|
|
416
|
+
v-for="guide in guides"
|
|
417
|
+
:key="guide.axis"
|
|
418
|
+
:x1="guide.axis === 'x' ? guide.value : guide.start"
|
|
419
|
+
:y1="guide.axis === 'x' ? guide.start : guide.value"
|
|
420
|
+
:x2="guide.axis === 'x' ? guide.value : guide.end"
|
|
421
|
+
:y2="guide.axis === 'x' ? guide.end : guide.value"
|
|
422
|
+
stroke="var(--ui-primary)"
|
|
423
|
+
stroke-width="1"
|
|
424
|
+
vector-effect="non-scaling-stroke"
|
|
425
|
+
/>
|
|
426
|
+
</svg>
|
|
427
|
+
<button
|
|
428
|
+
v-for="edge in edges.filter((entry) => entry.id !== 'agent')"
|
|
429
|
+
:key="`label-${edge.id}`"
|
|
430
|
+
class="absolute h-6 -translate-x-1/2 -translate-y-1/2 cursor-pointer rounded-full border bg-(--gray-300) px-2.5 text-xs whitespace-nowrap transition-colors duration-240 motion-reduce:transition-none"
|
|
431
|
+
:class="getEdgeLabelClass(edge)"
|
|
432
|
+
:style="{ left: `${(edge.source.x + edge.target.x) / 2}px`, top: `${edge.middle}px` }"
|
|
433
|
+
@click.stop="emit('select', edge.id)"
|
|
434
|
+
>
|
|
435
|
+
{{ edge.id === 'success' ? 'On success' : 'On error' }}
|
|
436
|
+
</button>
|
|
437
|
+
<button
|
|
438
|
+
v-for="node in nodes"
|
|
439
|
+
:key="node.id"
|
|
440
|
+
type="button"
|
|
441
|
+
class="absolute top-0 left-0 w-[280px] touch-none rounded-[14px] border bg-(--gray-300) p-1 text-left outline-offset-4 transition-[border-color,box-shadow,opacity] duration-200 will-change-transform motion-reduce:transition-none focus-visible:outline-2 focus-visible:outline-primary"
|
|
442
|
+
:class="nodeClasses(node.id)"
|
|
443
|
+
:style="{ transform: `translate3d(${positions[node.id].x}px,${positions[node.id].y}px,0)` }"
|
|
444
|
+
:aria-label="`${node.label}: ${summaries[node.id].main}`"
|
|
445
|
+
:aria-pressed="selected === node.id"
|
|
446
|
+
@pointerdown.stop="down($event, node.id)"
|
|
447
|
+
@pointermove.stop="move"
|
|
448
|
+
@pointerup.stop="up"
|
|
449
|
+
@click.stop="emit('select', node.id)"
|
|
450
|
+
@keydown="keyboard($event, node.id)"
|
|
451
|
+
>
|
|
452
|
+
<span class="flex h-9 items-center gap-2 pr-1.5 pl-1">
|
|
453
|
+
<span
|
|
454
|
+
class="grid size-6 place-items-center rounded-[7px] bg-(--gray-500)"
|
|
455
|
+
:class="node.color"
|
|
456
|
+
>
|
|
457
|
+
<AutomationNodeIcon
|
|
458
|
+
:node="node.id"
|
|
459
|
+
class="size-3.5"
|
|
460
|
+
/>
|
|
461
|
+
</span>
|
|
462
|
+
<span class="min-w-0 flex-1 truncate text-sm text-highlighted">{{ node.label }}</span>
|
|
463
|
+
<span
|
|
464
|
+
class="flex origin-right items-center gap-1.5 text-xs whitespace-nowrap transition-[color,opacity,transform] duration-200 ease-[cubic-bezier(.2,0,0,1)] motion-reduce:transition-none"
|
|
465
|
+
:class="[
|
|
466
|
+
getStatusColor(nodeState(node.id)),
|
|
467
|
+
nodeState(node.id) ? 'scale-100 opacity-100' : 'scale-80 opacity-0',
|
|
468
|
+
]"
|
|
469
|
+
>
|
|
470
|
+
<span class="tabular-nums">{{ nodeLabel(node.id) }}</span>
|
|
471
|
+
<AutomationStatusIcon
|
|
472
|
+
:status="nodeState(node.id)"
|
|
473
|
+
class="size-3.5"
|
|
474
|
+
/>
|
|
475
|
+
</span>
|
|
476
|
+
</span>
|
|
477
|
+
<span
|
|
478
|
+
class="flex h-[68px] flex-col justify-center gap-1 overflow-hidden rounded-[10px] border border-(--gray-400) bg-(--gray-100) px-3.5 py-3"
|
|
479
|
+
>
|
|
480
|
+
<span class="truncate font-mono text-[13px] leading-[18px]">
|
|
481
|
+
{{ summaries[node.id].main }}
|
|
482
|
+
</span>
|
|
483
|
+
<span class="truncate text-xs leading-4 text-muted">{{ summaries[node.id].sub }}</span>
|
|
484
|
+
</span>
|
|
485
|
+
</button>
|
|
486
|
+
</div>
|
|
487
|
+
</div>
|
|
488
|
+
</template>
|