@lokvis/ui-react 0.2.1 → 0.4.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/dist/components/AssetPanel.d.ts.map +1 -1
- package/dist/components/AssetPanel.js +18 -62
- package/dist/components/AssetPanel.js.map +1 -1
- package/dist/components/Canvas.d.ts +4 -4
- package/dist/components/Canvas.d.ts.map +1 -1
- package/dist/components/Canvas.js +9 -9
- package/dist/components/Canvas.js.map +1 -1
- package/dist/components/CommandPalette.d.ts +7 -7
- package/dist/components/CommandPalette.d.ts.map +1 -1
- package/dist/components/CommandPalette.js +10 -10
- package/dist/components/CommandPalette.js.map +1 -1
- package/dist/components/CompareSlider.d.ts +9 -8
- package/dist/components/CompareSlider.d.ts.map +1 -1
- package/dist/components/CompareSlider.js +10 -9
- package/dist/components/CompareSlider.js.map +1 -1
- package/dist/components/DownloadPanel.d.ts +3 -3
- package/dist/components/DownloadPanel.d.ts.map +1 -1
- package/dist/components/DownloadPanel.js +10 -17
- package/dist/components/DownloadPanel.js.map +1 -1
- package/dist/components/ErrorBanner.d.ts +2 -2
- package/dist/components/ErrorBanner.d.ts.map +1 -1
- package/dist/components/ErrorBanner.js +3 -3
- package/dist/components/ErrorBanner.js.map +1 -1
- package/dist/components/ErrorBoundary.d.ts +40 -0
- package/dist/components/ErrorBoundary.d.ts.map +1 -0
- package/dist/components/ErrorBoundary.js +52 -0
- package/dist/components/ErrorBoundary.js.map +1 -0
- package/dist/components/ExifPanel.d.ts +1 -1
- package/dist/components/ExifPanel.d.ts.map +1 -1
- package/dist/components/ExifPanel.js +2 -2
- package/dist/components/ExifPanel.js.map +1 -1
- package/dist/components/GlobalDropzone.d.ts +5 -5
- package/dist/components/GlobalDropzone.d.ts.map +1 -1
- package/dist/components/GlobalDropzone.js +15 -8
- package/dist/components/GlobalDropzone.js.map +1 -1
- package/dist/components/HistoryPanel.d.ts +2 -2
- package/dist/components/HistoryPanel.d.ts.map +1 -1
- package/dist/components/HistoryPanel.js +23 -23
- package/dist/components/HistoryPanel.js.map +1 -1
- package/dist/components/Inspector.d.ts.map +1 -1
- package/dist/components/Inspector.js +13 -10
- package/dist/components/Inspector.js.map +1 -1
- package/dist/components/ParamForm.d.ts.map +1 -1
- package/dist/components/ParamForm.js +10 -7
- package/dist/components/ParamForm.js.map +1 -1
- package/dist/components/PipelineBar.d.ts.map +1 -1
- package/dist/components/PipelineBar.js +19 -28
- package/dist/components/PipelineBar.js.map +1 -1
- package/dist/components/ProgressBar.d.ts +4 -4
- package/dist/components/ProgressBar.d.ts.map +1 -1
- package/dist/components/ProgressBar.js +8 -8
- package/dist/components/ProgressBar.js.map +1 -1
- package/dist/components/StatusBar.d.ts +3 -3
- package/dist/components/StatusBar.d.ts.map +1 -1
- package/dist/components/StatusBar.js +12 -21
- package/dist/components/StatusBar.js.map +1 -1
- package/dist/components/StatusDot.d.ts +12 -0
- package/dist/components/StatusDot.d.ts.map +1 -0
- package/dist/components/StatusDot.js +15 -0
- package/dist/components/StatusDot.js.map +1 -0
- package/dist/components/ThemeToggle.d.ts +4 -4
- package/dist/components/ThemeToggle.d.ts.map +1 -1
- package/dist/components/ThemeToggle.js +7 -7
- package/dist/components/ThemeToggle.js.map +1 -1
- package/dist/components/ToolRunner.d.ts +53 -0
- package/dist/components/ToolRunner.d.ts.map +1 -0
- package/dist/components/ToolRunner.js +142 -0
- package/dist/components/ToolRunner.js.map +1 -0
- package/dist/components/Toolbar.d.ts.map +1 -1
- package/dist/components/Toolbar.js +3 -3
- package/dist/components/Toolbar.js.map +1 -1
- package/dist/components/WorkflowEditor.d.ts +6 -6
- package/dist/components/WorkflowEditor.d.ts.map +1 -1
- package/dist/components/WorkflowEditor.js +40 -49
- package/dist/components/WorkflowEditor.js.map +1 -1
- package/dist/components/WorkflowTemplates.d.ts +2 -2
- package/dist/components/WorkflowTemplates.d.ts.map +1 -1
- package/dist/components/WorkflowTemplates.js +17 -7
- package/dist/components/WorkflowTemplates.js.map +1 -1
- package/dist/components/Workspace.d.ts +45 -23
- package/dist/components/Workspace.d.ts.map +1 -1
- package/dist/components/Workspace.js +87 -40
- package/dist/components/Workspace.js.map +1 -1
- package/dist/hooks/useCustomPresets.d.ts +98 -0
- package/dist/hooks/useCustomPresets.d.ts.map +1 -0
- package/dist/hooks/useCustomPresets.js +210 -0
- package/dist/hooks/useCustomPresets.js.map +1 -0
- package/dist/hooks/useLokvis.d.ts +19 -1
- package/dist/hooks/useLokvis.d.ts.map +1 -1
- package/dist/hooks/useLokvis.js +30 -9
- package/dist/hooks/useLokvis.js.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -1
- package/dist/store/assets-slice.d.ts +8 -1
- package/dist/store/assets-slice.d.ts.map +1 -1
- package/dist/store/assets-slice.js +101 -54
- package/dist/store/assets-slice.js.map +1 -1
- package/dist/store/capabilities-slice.d.ts +1 -1
- package/dist/store/capabilities-slice.d.ts.map +1 -1
- package/dist/store/capabilities-slice.js +8 -1
- package/dist/store/capabilities-slice.js.map +1 -1
- package/dist/store/index.d.ts +1 -1
- package/dist/store/index.d.ts.map +1 -1
- package/dist/store/index.js +1 -1
- package/dist/store/index.js.map +1 -1
- package/dist/store/runtime-slice.d.ts.map +1 -1
- package/dist/store/runtime-slice.js +22 -16
- package/dist/store/runtime-slice.js.map +1 -1
- package/dist/store/subscribe-utils.d.ts +45 -0
- package/dist/store/subscribe-utils.d.ts.map +1 -0
- package/dist/store/subscribe-utils.js +69 -0
- package/dist/store/subscribe-utils.js.map +1 -0
- package/dist/store/types.d.ts +19 -11
- package/dist/store/types.d.ts.map +1 -1
- package/dist/store/types.js +0 -10
- package/dist/store/types.js.map +1 -1
- package/dist/store/workflow-slice.d.ts +1 -1
- package/dist/store/workflow-slice.d.ts.map +1 -1
- package/dist/store/workflow-slice.js +19 -51
- package/dist/store/workflow-slice.js.map +1 -1
- package/dist/utils.d.ts +24 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +31 -0
- package/dist/utils.js.map +1 -0
- package/package.json +7 -6
- package/src/components/AssetPanel.tsx +224 -264
- package/src/components/Canvas.tsx +178 -174
- package/src/components/CommandPalette.tsx +242 -242
- package/src/components/CompareSlider.tsx +168 -165
- package/src/components/DownloadPanel.tsx +201 -207
- package/src/components/ErrorBanner.tsx +64 -64
- package/src/components/ErrorBoundary.tsx +101 -0
- package/src/components/ExifPanel.tsx +154 -154
- package/src/components/GlobalDropzone.tsx +175 -170
- package/src/components/HistoryPanel.tsx +219 -219
- package/src/components/Inspector.tsx +145 -137
- package/src/components/ParamForm.tsx +140 -137
- package/src/components/PipelineBar.tsx +97 -104
- package/src/components/ProgressBar.tsx +78 -78
- package/src/components/StatusBar.tsx +145 -152
- package/src/components/StatusDot.tsx +27 -0
- package/src/components/ThemeToggle.tsx +98 -98
- package/src/components/ToolRunner.tsx +352 -0
- package/src/components/Toolbar.tsx +65 -65
- package/src/components/WorkflowEditor.tsx +333 -326
- package/src/components/WorkflowTemplates.tsx +101 -78
- package/src/components/Workspace.tsx +310 -223
- package/src/css-modules.d.ts +3 -0
- package/src/hooks/useCustomPresets.ts +284 -0
- package/src/hooks/useLokvis.ts +43 -10
- package/src/index.ts +7 -0
- package/src/store/assets-slice.ts +106 -52
- package/src/store/capabilities-slice.ts +10 -2
- package/src/store/index.ts +1 -1
- package/src/store/runtime-slice.ts +20 -15
- package/src/store/subscribe-utils.ts +82 -0
- package/src/store/types.ts +19 -12
- package/src/store/workflow-slice.ts +19 -53
- package/src/utils.ts +41 -0
- package/dist/store.d.ts +0 -76
- package/dist/store.d.ts.map +0 -1
- package/dist/store.js +0 -217
- package/dist/store.js.map +0 -1
|
@@ -2,14 +2,14 @@
|
|
|
2
2
|
* WorkflowEditor - 拖拽式工作流编辑器(W10.4)
|
|
3
3
|
*
|
|
4
4
|
* 增强 PipelineBar,提供节点拖拽重排能力:
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
5
|
+
* - 拖拽节点到任意位置重排线性链
|
|
6
|
+
* - 拖拽时显示插入指示器
|
|
7
|
+
* - 键盘支持:选中节点后 ← → 移动(无障碍)
|
|
8
|
+
* - 节点数上限 MAX_WORKFLOW_STEPS=5(M1 MVP 约束,UI 层常量)
|
|
9
9
|
*
|
|
10
10
|
* 与 PipelineBar 区别:
|
|
11
|
-
*
|
|
12
|
-
*
|
|
11
|
+
* - PipelineBar:只读 + 选中/删除(快速预览)
|
|
12
|
+
* - WorkflowEditor:可编辑 + 拖拽重排(编辑模式)
|
|
13
13
|
*
|
|
14
14
|
* 使用原生 HTML5 Drag and Drop API,无额外依赖。
|
|
15
15
|
* 触摸设备降级为按钮上下移动(HTML5 DnD 在移动端支持不佳)。
|
|
@@ -18,245 +18,243 @@
|
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
20
|
import * as React from 'react';
|
|
21
|
-
import { Icon } from '@lokvis/ui-core';
|
|
21
|
+
import { ConfirmDialog, Icon, Input } from '@lokvis/ui-core';
|
|
22
22
|
import type { Capability } from '@lokvis/schema';
|
|
23
23
|
import { useWorkspaceStore, MAX_WORKFLOW_STEPS } from '../store/index.js';
|
|
24
|
+
import { filterCapabilities } from '../utils.js';
|
|
25
|
+
import { StatusDot } from './StatusDot.js';
|
|
24
26
|
|
|
25
27
|
export interface WorkflowEditorProps {
|
|
26
|
-
|
|
28
|
+
className?: string;
|
|
27
29
|
}
|
|
28
30
|
|
|
29
31
|
export function WorkflowEditor({ className = '' }: WorkflowEditorProps) {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
32
|
+
const nodes = useWorkspaceStore((s) => s.nodes);
|
|
33
|
+
const selectedNodeId = useWorkspaceStore((s) => s.selectedNodeId);
|
|
34
|
+
const selectNode = useWorkspaceStore((s) => s.selectNode);
|
|
35
|
+
const removeNode = useWorkspaceStore((s) => s.removeNode);
|
|
36
|
+
const moveNode = useWorkspaceStore((s) => s.moveNode);
|
|
37
|
+
const insertNodeAt = useWorkspaceStore((s) => s.insertNodeAt);
|
|
38
|
+
const capabilities = useWorkspaceStore((s) => s.capabilities);
|
|
39
|
+
const stubCapabilities = useWorkspaceStore((s) => s.stubCapabilities);
|
|
40
|
+
const clearWorkflow = useWorkspaceStore((s) => s.clearWorkflow);
|
|
38
41
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
+
// 拖拽状态:被拖拽的节点索引 + 当前 hover 的插入位置
|
|
43
|
+
const [dragIndex, setDragIndex] = React.useState<number | null>(null);
|
|
44
|
+
const [hoverIndex, setHoverIndex] = React.useState<number | null>(null);
|
|
45
|
+
// 清空确认对话框
|
|
46
|
+
const [clearOpen, setClearOpen] = React.useState(false);
|
|
42
47
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
48
|
+
const handleDragStart = (e: React.DragEvent, index: number) => {
|
|
49
|
+
setDragIndex(index);
|
|
50
|
+
e.dataTransfer.effectAllowed = 'move';
|
|
51
|
+
// Firefox 需要设置 data 才能触发 dragstart
|
|
52
|
+
e.dataTransfer.setData('text/plain', String(index));
|
|
53
|
+
};
|
|
49
54
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
55
|
+
const handleDragOver = (e: React.DragEvent, index: number) => {
|
|
56
|
+
e.preventDefault();
|
|
57
|
+
e.dataTransfer.dropEffect = 'move';
|
|
58
|
+
if (dragIndex !== null && dragIndex !== index) {
|
|
59
|
+
setHoverIndex(index);
|
|
60
|
+
}
|
|
61
|
+
};
|
|
57
62
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
63
|
+
const handleDragLeave = () => {
|
|
64
|
+
// 不立即清空 hoverIndex,因为 dragleave 在子元素间会误触发
|
|
65
|
+
// 由 handleDrop / handleDragEnd 统一清空
|
|
66
|
+
};
|
|
62
67
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
68
|
+
const handleDrop = (e: React.DragEvent, index: number) => {
|
|
69
|
+
e.preventDefault();
|
|
70
|
+
if (dragIndex !== null && dragIndex !== index) {
|
|
71
|
+
moveNode(dragIndex, index);
|
|
72
|
+
}
|
|
73
|
+
setDragIndex(null);
|
|
74
|
+
setHoverIndex(null);
|
|
75
|
+
};
|
|
71
76
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
77
|
+
const handleDragEnd = () => {
|
|
78
|
+
setDragIndex(null);
|
|
79
|
+
setHoverIndex(null);
|
|
80
|
+
};
|
|
76
81
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
82
|
+
// 键盘支持:选中节点后用 ← → 移动。传 node.id 而非 index 避免异步操作后 stale index
|
|
83
|
+
const handleKeyDown = (e: React.KeyboardEvent, nodeId: string, index: number) => {
|
|
84
|
+
if (e.key === 'ArrowLeft' && index > 0) {
|
|
85
|
+
e.preventDefault();
|
|
86
|
+
moveNode(index, index - 1);
|
|
87
|
+
} else if (e.key === 'ArrowRight' && index < nodes.length - 1) {
|
|
88
|
+
e.preventDefault();
|
|
89
|
+
moveNode(index, index + 1);
|
|
90
|
+
} else if (e.key === 'Delete' || e.key === 'Backspace') {
|
|
91
|
+
e.preventDefault();
|
|
92
|
+
removeNode(nodeId);
|
|
93
|
+
}
|
|
94
|
+
};
|
|
90
95
|
|
|
91
|
-
|
|
96
|
+
const canAddMore = nodes.length < MAX_WORKFLOW_STEPS;
|
|
92
97
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
Clear
|
|
127
|
-
</button>
|
|
128
|
-
)}
|
|
129
|
-
</div>
|
|
98
|
+
return (
|
|
99
|
+
<div
|
|
100
|
+
className={`flex flex-col border-b border-[var(--lokvis-border)] bg-[var(--lokvis-surface)] ${className}`}
|
|
101
|
+
role="region"
|
|
102
|
+
aria-label="工作流编辑器"
|
|
103
|
+
>
|
|
104
|
+
{/* 顶部:标题 + 步数 + 清空 */}
|
|
105
|
+
<div className="flex h-9 shrink-0 items-center justify-between border-b border-[var(--lokvis-border)] px-3">
|
|
106
|
+
<div className="flex items-center gap-2">
|
|
107
|
+
<span className="text-[10px] font-semibold uppercase tracking-wider text-[var(--lokvis-fg-subtle)]">
|
|
108
|
+
Workflow Editor
|
|
109
|
+
</span>
|
|
110
|
+
<span
|
|
111
|
+
className={`rounded px-1.5 py-0.5 text-[10px] tabular-nums ${
|
|
112
|
+
canAddMore
|
|
113
|
+
? 'bg-[var(--lokvis-surface-muted)] text-[var(--lokvis-fg-muted)]'
|
|
114
|
+
: 'bg-[var(--lokvis-warning)]/15 text-[var(--lokvis-warning)]'
|
|
115
|
+
}`}
|
|
116
|
+
>
|
|
117
|
+
{nodes.length}/{MAX_WORKFLOW_STEPS}
|
|
118
|
+
</span>
|
|
119
|
+
</div>
|
|
120
|
+
{nodes.length > 0 && (
|
|
121
|
+
<button
|
|
122
|
+
type="button"
|
|
123
|
+
onClick={() => setClearOpen(true)}
|
|
124
|
+
className="text-[10px] text-[var(--lokvis-fg-subtle)] transition-colors hover:text-[var(--lokvis-danger)]"
|
|
125
|
+
aria-label="清空工作流"
|
|
126
|
+
>
|
|
127
|
+
Clear
|
|
128
|
+
</button>
|
|
129
|
+
)}
|
|
130
|
+
</div>
|
|
130
131
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
132
|
+
{/* 节点链 */}
|
|
133
|
+
<div className="flex min-h-[3rem] items-center gap-1 overflow-x-auto px-3 py-2">
|
|
134
|
+
{nodes.length === 0 ? (
|
|
135
|
+
<div className="flex flex-1 items-center justify-center py-2 text-center">
|
|
136
|
+
<p className="text-[11px] text-[var(--lokvis-fg-subtle)]">
|
|
137
|
+
空工作流 — 从右侧 <span className="font-mono">Capabilities</span> 面板点击添加节点
|
|
138
|
+
</p>
|
|
139
|
+
</div>
|
|
140
|
+
) : (
|
|
141
|
+
<>
|
|
142
|
+
{/* Source indicator */}
|
|
143
|
+
<div className="flex shrink-0 items-center gap-1 rounded-md bg-[var(--lokvis-surface-muted)] px-2 py-1 text-[10px] font-medium text-[var(--lokvis-fg-muted)]">
|
|
144
|
+
<Icon size={12}><path d="M12 4.5v15m7.5-7.5h-15" /></Icon>
|
|
145
|
+
Source
|
|
146
|
+
</div>
|
|
146
147
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
148
|
+
{/* W11.1: 节点之间可插入的连接器(+ 按钮) */}
|
|
149
|
+
<InsertConnector
|
|
150
|
+
index={0}
|
|
151
|
+
capabilities={capabilities}
|
|
152
|
+
stubCapabilities={stubCapabilities}
|
|
153
|
+
onInsert={insertNodeAt}
|
|
154
|
+
disabled={nodes.length >= MAX_WORKFLOW_STEPS}
|
|
155
|
+
/>
|
|
154
156
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
</button>
|
|
210
|
-
</button>
|
|
157
|
+
{/* 节点 */}
|
|
158
|
+
{nodes.map((node, i) => {
|
|
159
|
+
const selected = node.id === selectedNodeId;
|
|
160
|
+
const isDragging = dragIndex === i;
|
|
161
|
+
const isHoverTarget = hoverIndex === i && dragIndex !== null && dragIndex !== i;
|
|
162
|
+
return (
|
|
163
|
+
<React.Fragment key={node.id}>
|
|
164
|
+
{/* D7: remove button 作为 node button 的兄弟而非子元素(HTML 规范禁止 button 嵌套) */}
|
|
165
|
+
<div className="group relative flex shrink-0 items-center">
|
|
166
|
+
<button
|
|
167
|
+
type="button"
|
|
168
|
+
draggable
|
|
169
|
+
onDragStart={(e) => handleDragStart(e, i)}
|
|
170
|
+
onDragOver={(e) => handleDragOver(e, i)}
|
|
171
|
+
onDragLeave={handleDragLeave}
|
|
172
|
+
onDrop={(e) => handleDrop(e, i)}
|
|
173
|
+
onDragEnd={handleDragEnd}
|
|
174
|
+
onClick={() => selectNode(node.id)}
|
|
175
|
+
onKeyDown={(e) => handleKeyDown(e, node.id, i)}
|
|
176
|
+
aria-label={`节点 ${node.capability},位置 ${i + 1},拖拽或方向键重排,Delete 删除`}
|
|
177
|
+
className={`flex shrink-0 items-center gap-1.5 rounded-md px-2.5 py-1.5 text-[11px] font-medium transition-all ${
|
|
178
|
+
isDragging
|
|
179
|
+
? 'opacity-40'
|
|
180
|
+
: isHoverTarget
|
|
181
|
+
? 'ring-2 ring-[var(--lokvis-primary)] ring-offset-1'
|
|
182
|
+
: selected
|
|
183
|
+
? 'bg-[var(--lokvis-primary)]/15 text-[var(--lokvis-primary)] ring-1 ring-[var(--lokvis-primary)]/50'
|
|
184
|
+
: node.status === 'running'
|
|
185
|
+
? 'bg-[var(--lokvis-warning)]/15 text-[var(--lokvis-warning)]'
|
|
186
|
+
: node.status === 'success'
|
|
187
|
+
? 'bg-[var(--lokvis-success)]/15 text-[var(--lokvis-success)]'
|
|
188
|
+
: node.status === 'failed'
|
|
189
|
+
? 'bg-[var(--lokvis-danger)]/15 text-[var(--lokvis-danger)]'
|
|
190
|
+
: 'bg-[var(--lokvis-surface-muted)] text-[var(--lokvis-fg-muted)] hover:bg-[var(--lokvis-border)]'
|
|
191
|
+
} cursor-grab active:cursor-grabbing`}
|
|
192
|
+
>
|
|
193
|
+
{/* 步骤序号 */}
|
|
194
|
+
<span className="flex h-4 w-4 items-center justify-center rounded-full bg-[var(--lokvis-fg)]/5 text-[9px] tabular-nums">
|
|
195
|
+
{i + 1}
|
|
196
|
+
</span>
|
|
197
|
+
<span className="font-mono">{node.capability}</span>
|
|
198
|
+
<StatusDot status={node.status} />
|
|
199
|
+
</button>
|
|
200
|
+
{/* 删除按钮:兄弟 button(position absolute 浮在节点右上角),group-hover 显示 */}
|
|
201
|
+
<button
|
|
202
|
+
type="button"
|
|
203
|
+
onClick={() => removeNode(node.id)}
|
|
204
|
+
aria-label="删除节点"
|
|
205
|
+
tabIndex={-1}
|
|
206
|
+
className="absolute -right-1 -top-1 rounded p-0.5 opacity-0 transition-opacity group-hover:opacity-100 hover:bg-[var(--lokvis-danger)]/15 hover:text-[var(--lokvis-danger)]"
|
|
207
|
+
>
|
|
208
|
+
<Icon size={10} strokeWidth={3}><path d="M6 18L18 6M6 6l12 12" /></Icon>
|
|
209
|
+
</button>
|
|
210
|
+
</div>
|
|
211
211
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
212
|
+
{/* W11.1: 每个节点后的插入连接器(最后一个用于追加到 Output 前) */}
|
|
213
|
+
<InsertConnector
|
|
214
|
+
index={i + 1}
|
|
215
|
+
capabilities={capabilities}
|
|
216
|
+
stubCapabilities={stubCapabilities}
|
|
217
|
+
onInsert={insertNodeAt}
|
|
218
|
+
disabled={nodes.length >= MAX_WORKFLOW_STEPS}
|
|
219
|
+
/>
|
|
220
|
+
</React.Fragment>
|
|
221
|
+
);
|
|
222
|
+
})}
|
|
222
223
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
224
|
+
{/* Output indicator */}
|
|
225
|
+
{nodes.length > 0 && (
|
|
226
|
+
<div className="flex shrink-0 items-center gap-1 rounded-md bg-[var(--lokvis-surface-muted)] px-2 py-1 text-[10px] font-medium text-[var(--lokvis-fg-muted)]">
|
|
227
|
+
<Icon size={12}><path d="M3 16.5v2.25A2.25 2.25 0 0 0 5.25 21h13.5A2.25 2.25 0 0 0 21 18.75V16.5M16.5 12 12 16.5m0 0L7.5 12m4.5 4.5V3" /></Icon>
|
|
228
|
+
Output
|
|
229
|
+
</div>
|
|
230
|
+
)}
|
|
231
|
+
</>
|
|
232
|
+
)}
|
|
233
|
+
</div>
|
|
233
234
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
</div>
|
|
242
|
-
);
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
function StatusDot({ status }: { status: string }) {
|
|
246
|
-
const color =
|
|
247
|
-
status === 'running'
|
|
248
|
-
? 'bg-amber-500'
|
|
249
|
-
: status === 'success'
|
|
250
|
-
? 'bg-emerald-500'
|
|
251
|
-
: status === 'failed'
|
|
252
|
-
? 'bg-red-500'
|
|
253
|
-
: status === 'pending'
|
|
254
|
-
? 'bg-zinc-400 dark:bg-zinc-500'
|
|
255
|
-
: 'bg-zinc-300 dark:bg-zinc-600';
|
|
256
|
-
|
|
257
|
-
const animate = status === 'running' ? 'animate-pulse' : '';
|
|
235
|
+
{/* 提示行 */}
|
|
236
|
+
{nodes.length > 0 && (
|
|
237
|
+
<div className="flex shrink-0 items-center gap-2 border-t border-[var(--lokvis-border)] px-3 py-1 text-[10px] text-[var(--lokvis-fg-subtle)]">
|
|
238
|
+
<Icon size={10}><path d="M13 5.5a1 1 0 1 1 2 0 1-1 0 0 0-.5.86L16.5 9l1.5-.5a1 1 0 1 1 0 2l-1.5-.5-1 1.5a1 1 0 1 1-2 0l1-1.5-1-1.5a1 1 0 0 1 0-2z" /></Icon>
|
|
239
|
+
<span>拖拽重排 · 方向键移动 · Delete 删除 · hover 箭头处插入节点</span>
|
|
240
|
+
</div>
|
|
241
|
+
)}
|
|
258
242
|
|
|
259
|
-
|
|
243
|
+
{/* 清空确认对话框(替代 window.confirm) */}
|
|
244
|
+
<ConfirmDialog
|
|
245
|
+
open={clearOpen}
|
|
246
|
+
title="清空工作流"
|
|
247
|
+
message="清空当前工作流?所有节点将被移除。"
|
|
248
|
+
confirmText="清空"
|
|
249
|
+
variant="danger"
|
|
250
|
+
onConfirm={() => {
|
|
251
|
+
clearWorkflow();
|
|
252
|
+
setClearOpen(false);
|
|
253
|
+
}}
|
|
254
|
+
onClose={() => setClearOpen(false)}
|
|
255
|
+
/>
|
|
256
|
+
</div>
|
|
257
|
+
);
|
|
260
258
|
}
|
|
261
259
|
|
|
262
260
|
/**
|
|
@@ -266,117 +264,126 @@ function StatusDot({ status }: { status: string }) {
|
|
|
266
264
|
* 选择后调用 onInsert(index, capability)。
|
|
267
265
|
*/
|
|
268
266
|
function InsertConnector({
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
267
|
+
index,
|
|
268
|
+
capabilities,
|
|
269
|
+
stubCapabilities,
|
|
270
|
+
onInsert,
|
|
271
|
+
disabled,
|
|
273
272
|
}: {
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
273
|
+
index: number;
|
|
274
|
+
capabilities: Capability[];
|
|
275
|
+
stubCapabilities: Set<string>;
|
|
276
|
+
onInsert: (index: number, capability: string) => void;
|
|
277
|
+
disabled?: boolean;
|
|
278
278
|
}) {
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
279
|
+
const [open, setOpen] = React.useState(false);
|
|
280
|
+
const [filter, setFilter] = React.useState('');
|
|
281
|
+
const ref = React.useRef<HTMLDivElement>(null);
|
|
282
282
|
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
283
|
+
// 点击外部关闭菜单
|
|
284
|
+
React.useEffect(() => {
|
|
285
|
+
if (!open) return;
|
|
286
|
+
const handler = (e: MouseEvent) => {
|
|
287
|
+
// 用 instanceof guard 替代 as Node 断言:运行时安全 + 类型安全
|
|
288
|
+
if (!(e.target instanceof Node)) return;
|
|
289
|
+
if (ref.current && !ref.current.contains(e.target)) {
|
|
290
|
+
setOpen(false);
|
|
291
|
+
setFilter('');
|
|
292
|
+
}
|
|
293
|
+
};
|
|
294
|
+
document.addEventListener('mousedown', handler);
|
|
295
|
+
return () => document.removeEventListener('mousedown', handler);
|
|
296
|
+
}, [open]);
|
|
297
297
|
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
c.name.toLowerCase().includes(filter.toLowerCase()) ||
|
|
303
|
-
c.description.toLowerCase().includes(filter.toLowerCase())
|
|
304
|
-
),
|
|
305
|
-
[capabilities, filter]
|
|
306
|
-
);
|
|
298
|
+
const filtered = React.useMemo(
|
|
299
|
+
() => filterCapabilities(capabilities, filter),
|
|
300
|
+
[capabilities, filter]
|
|
301
|
+
);
|
|
307
302
|
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
303
|
+
const handleSelect = (capName: string) => {
|
|
304
|
+
onInsert(index, capName);
|
|
305
|
+
setOpen(false);
|
|
306
|
+
setFilter('');
|
|
307
|
+
};
|
|
313
308
|
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
309
|
+
if (disabled) {
|
|
310
|
+
// 5 步上限时只显示箭头,不显示 +
|
|
311
|
+
return (
|
|
312
|
+
<Icon size={12} className="shrink-0 text-[var(--lokvis-fg-subtle)]">
|
|
313
|
+
<path d="m9 5 7 7-7 7" />
|
|
314
|
+
</Icon>
|
|
315
|
+
);
|
|
316
|
+
}
|
|
322
317
|
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
318
|
+
return (
|
|
319
|
+
<div ref={ref} className="relative flex shrink-0 items-center">
|
|
320
|
+
{/* 箭头 / + 按钮切换 */}
|
|
321
|
+
<button
|
|
322
|
+
type="button"
|
|
323
|
+
onClick={() => setOpen(!open)}
|
|
324
|
+
aria-label={`在位置 ${index + 1} 插入节点`}
|
|
325
|
+
title="插入节点"
|
|
326
|
+
className="group flex h-5 w-5 items-center justify-center rounded text-[var(--lokvis-fg-subtle)] transition-colors hover:bg-[var(--lokvis-primary)]/15 hover:text-[var(--lokvis-primary)]"
|
|
327
|
+
>
|
|
328
|
+
<Icon size={12} className="group-hover:hidden">
|
|
329
|
+
<path d="m9 5 7 7-7 7" />
|
|
330
|
+
</Icon>
|
|
331
|
+
<Icon size={12} className="hidden group-hover:block" strokeWidth={2.5}>
|
|
332
|
+
<path d="M12 5v14M5 12h14" />
|
|
333
|
+
</Icon>
|
|
334
|
+
</button>
|
|
340
335
|
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
336
|
+
{/* 弹出菜单 */}
|
|
337
|
+
{open && (
|
|
338
|
+
<div className="absolute top-full left-1/2 z-50 mt-1 w-56 -translate-x-1/2 rounded-lg border border-[var(--lokvis-border)] bg-[var(--lokvis-surface)] shadow-[var(--lokvis-elevation-2)]">
|
|
339
|
+
{/* 搜索 */}
|
|
340
|
+
<div className="border-b border-[var(--lokvis-border)] p-1.5">
|
|
341
|
+
<Input
|
|
342
|
+
type="text"
|
|
343
|
+
size="sm"
|
|
344
|
+
autoFocus
|
|
345
|
+
placeholder="搜索 capability..."
|
|
346
|
+
value={filter}
|
|
347
|
+
onChange={(e) => setFilter(e.target.value)}
|
|
348
|
+
/>
|
|
349
|
+
</div>
|
|
350
|
+
{/* 列表 */}
|
|
351
|
+
<div className="max-h-48 overflow-y-auto p-1">
|
|
352
|
+
{filtered.length === 0 ? (
|
|
353
|
+
<p className="px-2 py-2 text-center text-[10px] text-[var(--lokvis-fg-subtle)]">无匹配 capability</p>
|
|
354
|
+
) : (
|
|
355
|
+
filtered.map((cap) => {
|
|
356
|
+
// A7: stub-only 能力显示 "Coming Soon" 标记
|
|
357
|
+
const isStubOnly = stubCapabilities.has(cap.name);
|
|
358
|
+
return (
|
|
359
|
+
<button
|
|
360
|
+
key={cap.name}
|
|
361
|
+
type="button"
|
|
362
|
+
onClick={() => handleSelect(cap.name)}
|
|
363
|
+
title={isStubOnly ? 'Coming soon — no engine installed yet' : undefined}
|
|
364
|
+
className="block w-full rounded px-2 py-1 text-left transition-colors hover:bg-[var(--lokvis-primary)]/10"
|
|
365
|
+
>
|
|
366
|
+
<span className="flex items-center justify-between gap-1.5">
|
|
367
|
+
<span className="block font-mono text-[10px] font-medium text-[var(--lokvis-fg-muted)]">
|
|
368
|
+
{cap.name}
|
|
369
|
+
</span>
|
|
370
|
+
{isStubOnly && (
|
|
371
|
+
<span className="shrink-0 rounded px-1 py-0.5 text-[8px] font-medium uppercase bg-[var(--lokvis-warning)]/15 text-[var(--lokvis-warning)]">
|
|
372
|
+
Soon
|
|
373
|
+
</span>
|
|
374
|
+
)}
|
|
375
|
+
</span>
|
|
376
|
+
<span className="block truncate text-[9px] text-[var(--lokvis-fg-muted)]">
|
|
377
|
+
{cap.description}
|
|
378
|
+
</span>
|
|
379
|
+
</button>
|
|
380
|
+
);
|
|
381
|
+
})
|
|
382
|
+
)}
|
|
383
|
+
</div>
|
|
384
|
+
</div>
|
|
385
|
+
)}
|
|
386
|
+
</div>
|
|
387
|
+
);
|
|
381
388
|
}
|
|
382
389
|
|