@andrewyang/ai-workflow-editor 0.1.0

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.
Files changed (65) hide show
  1. package/.nuxt/app.config.mjs +18 -0
  2. package/.nuxt/components.d.ts +910 -0
  3. package/.nuxt/dev/index.mjs +4103 -0
  4. package/.nuxt/dev/index.mjs.map +1 -0
  5. package/.nuxt/dist/server/client.manifest.mjs +4 -0
  6. package/.nuxt/dist/server/client.precomputed.mjs +1 -0
  7. package/.nuxt/dist/server/server.mjs +1 -0
  8. package/.nuxt/i18n-route-resources.mjs +3 -0
  9. package/.nuxt/imports.d.ts +43 -0
  10. package/.nuxt/manifest/latest.json +1 -0
  11. package/.nuxt/manifest/meta/dev.json +1 -0
  12. package/.nuxt/nitro.json +17 -0
  13. package/.nuxt/nuxt.d.ts +24 -0
  14. package/.nuxt/nuxt.json +9 -0
  15. package/.nuxt/schema/nuxt.schema.d.ts +17 -0
  16. package/.nuxt/schema/nuxt.schema.json +3 -0
  17. package/.nuxt/tsconfig.json +234 -0
  18. package/.nuxt/tsconfig.server.json +185 -0
  19. package/.nuxt/types/app-defaults.d.ts +7 -0
  20. package/.nuxt/types/app.config.d.ts +31 -0
  21. package/.nuxt/types/build.d.ts +29 -0
  22. package/.nuxt/types/builder-env.d.ts +1 -0
  23. package/.nuxt/types/components.d.ts +915 -0
  24. package/.nuxt/types/i18n-plugin.d.ts +123 -0
  25. package/.nuxt/types/imports.d.ts +993 -0
  26. package/.nuxt/types/middleware.d.ts +17 -0
  27. package/.nuxt/types/nitro-config.d.ts +14 -0
  28. package/.nuxt/types/nitro-imports.d.ts +170 -0
  29. package/.nuxt/types/nitro-layouts.d.ts +17 -0
  30. package/.nuxt/types/nitro-nuxt.d.ts +39 -0
  31. package/.nuxt/types/nitro-routes.d.ts +17 -0
  32. package/.nuxt/types/nitro.d.ts +3 -0
  33. package/.nuxt/types/plugins.d.ts +43 -0
  34. package/.nuxt/types/schema.d.ts +213 -0
  35. package/.nuxt/types/vue-shim.d.ts +0 -0
  36. package/.trae/rules/rule.md +38 -0
  37. package/.vscode/settings.json +5 -0
  38. package/nuxt.config.ts +38 -0
  39. package/package.json +42 -0
  40. package/pnpm-lock.yaml +24 -0
  41. package/src/app.vue +46 -0
  42. package/src/components/AiWorkflowEditor.vue +142 -0
  43. package/src/components/ai/AiChatPanel.vue +135 -0
  44. package/src/components/ai/AiGenerator.vue +62 -0
  45. package/src/components/editor/Canvas.vue +175 -0
  46. package/src/components/editor/FormatPanel.vue +327 -0
  47. package/src/components/editor/NodePanel.vue +240 -0
  48. package/src/components/editor/PropertyPanel.vue +348 -0
  49. package/src/components/editor/Toolbar.vue +49 -0
  50. package/src/components/editor/nodes/AiNode.vue +77 -0
  51. package/src/components/editor/nodes/NormalNode.vue +75 -0
  52. package/src/components/editor/nodes/SysmlBlockNode.vue +72 -0
  53. package/src/components/editor/nodes/SysmlRequirementNode.vue +72 -0
  54. package/src/components/editor/nodes/SysmlUseCaseNode.vue +62 -0
  55. package/src/composables/useAi.ts +82 -0
  56. package/src/i18n.config.ts +5 -0
  57. package/src/locales/en.json +106 -0
  58. package/src/locales/zh.json +106 -0
  59. package/src/plugins/aiWorkflowEditor.ts +6 -0
  60. package/src/types/ai.ts +7 -0
  61. package/src/types/workflow.ts +25 -0
  62. package/src/utils/llmAdapter.ts +46 -0
  63. package/tsconfig.json +3 -0
  64. package/uno.config.ts +15 -0
  65. package//345/211/215/347/253/257/345/256/232/345/210/266/345/274/200/345/217/221/357/274/232ai-workflow-editor/345/274/200/345/217/221/350/256/241/345/210/222.md +655 -0
@@ -0,0 +1,327 @@
1
+ <template>
2
+ <div class="flex h-full flex-col bg-white/80 backdrop-blur-xl border-l border-gray-200 w-80 shadow-[-4px_0_24px_rgba(0,0,0,0.08)] z-20 relative">
3
+ <!-- Header -->
4
+ <div class="flex items-center justify-between border-b border-gray-100 p-4">
5
+ <div class="font-bold text-gray-800">{{ $t('propertyPanel.title') }}</div>
6
+ <el-button link class="!text-gray-400 hover:!text-gray-600 !p-1.5" @click="$emit('close')">
7
+ <div class="i-lucide-x w-4 h-4" />
8
+ </el-button>
9
+ </div>
10
+
11
+ <!-- Tabs -->
12
+ <div class="flex-1 flex flex-col overflow-hidden">
13
+ <el-tabs v-model="activeTab" class="h-full flex flex-col custom-tabs" stretch>
14
+ <!-- Style Tab -->
15
+ <el-tab-pane :label="$t('propertyPanel.tabs.style')" name="style" class="h-full overflow-y-auto custom-scrollbar">
16
+ <div class="p-4 space-y-6">
17
+ <!-- Fill -->
18
+ <div class="space-y-3">
19
+ <div class="flex items-center justify-between">
20
+ <el-checkbox v-model="styleForm.fill" :label="$t('propertyPanel.style.fill')" />
21
+ <div class="flex items-center gap-2" v-if="styleForm.fill">
22
+ <el-color-picker v-model="styleForm.fillColor" size="small" show-alpha />
23
+ </div>
24
+ </div>
25
+ </div>
26
+
27
+ <!-- Stroke -->
28
+ <div class="space-y-3">
29
+ <div class="flex items-center justify-between">
30
+ <el-checkbox v-model="styleForm.stroke" :label="$t('propertyPanel.style.stroke')" />
31
+ <div class="flex items-center gap-2" v-if="styleForm.stroke">
32
+ <el-color-picker v-model="styleForm.strokeColor" size="small" show-alpha />
33
+ </div>
34
+ </div>
35
+ <div v-if="styleForm.stroke" class="pl-6 space-y-2">
36
+ <div class="flex items-center gap-2">
37
+ <el-select v-model="styleForm.strokeType" size="small" class="flex-1">
38
+ <el-option value="solid" label="Solid" />
39
+ <el-option value="dashed" label="Dashed" />
40
+ <el-option value="dotted" label="Dotted" />
41
+ </el-select>
42
+ <el-input-number v-model="styleForm.strokeWidth" size="small" :min="0" :max="20" controls-position="right" class="!w-20">
43
+ <template #suffix>pt</template>
44
+ </el-input-number>
45
+ </div>
46
+ </div>
47
+ </div>
48
+
49
+ <!-- Opacity -->
50
+ <div class="space-y-2">
51
+ <div class="flex items-center justify-between text-xs text-gray-600">
52
+ <span>{{ $t('propertyPanel.style.opacity') }}</span>
53
+ <span>{{ styleForm.opacity }}%</span>
54
+ </div>
55
+ <el-slider v-model="styleForm.opacity" :min="0" :max="100" size="small" />
56
+ </div>
57
+
58
+ <!-- Effects -->
59
+ <div class="grid grid-cols-2 gap-3">
60
+ <el-checkbox v-model="styleForm.rounded" :label="$t('propertyPanel.style.rounded')" />
61
+ <el-checkbox v-model="styleForm.shadow" :label="$t('propertyPanel.style.shadow')" />
62
+ </div>
63
+ </div>
64
+ </el-tab-pane>
65
+
66
+ <!-- Text Tab -->
67
+ <el-tab-pane :label="$t('propertyPanel.tabs.text')" name="text" class="h-full overflow-y-auto custom-scrollbar">
68
+ <div class="p-4 space-y-6">
69
+ <!-- Content Editing (Double click fallback) -->
70
+ <div class="space-y-2">
71
+ <label class="text-xs font-medium text-gray-500">{{ $t('propertyPanel.text.content') }}</label>
72
+ <el-input v-model="textForm.content" type="textarea" :rows="2" resize="none" />
73
+ </div>
74
+
75
+ <!-- Font -->
76
+ <div class="space-y-3">
77
+ <div class="flex items-center gap-2">
78
+ <el-select v-model="textForm.fontFamily" size="small" class="flex-1">
79
+ <el-option value="Helvetica" label="Helvetica" />
80
+ <el-option value="Arial" label="Arial" />
81
+ <el-option value="Times New Roman" label="Times New Roman" />
82
+ </el-select>
83
+ </div>
84
+ <div class="flex items-center gap-2">
85
+ <div class="flex border border-gray-200 rounded overflow-hidden">
86
+ <button
87
+ @click="textForm.bold = !textForm.bold"
88
+ :class="['p-1.5 hover:bg-gray-50 transition-colors', textForm.bold ? 'bg-gray-100 text-purple-600' : 'text-gray-500']"
89
+ >
90
+ <div class="i-lucide-bold w-4 h-4" />
91
+ </button>
92
+ <div class="w-px bg-gray-200" />
93
+ <button
94
+ @click="textForm.italic = !textForm.italic"
95
+ :class="['p-1.5 hover:bg-gray-50 transition-colors', textForm.italic ? 'bg-gray-100 text-purple-600' : 'text-gray-500']"
96
+ >
97
+ <div class="i-lucide-italic w-4 h-4" />
98
+ </button>
99
+ <div class="w-px bg-gray-200" />
100
+ <button
101
+ @click="textForm.underline = !textForm.underline"
102
+ :class="['p-1.5 hover:bg-gray-50 transition-colors', textForm.underline ? 'bg-gray-100 text-purple-600' : 'text-gray-500']"
103
+ >
104
+ <div class="i-lucide-underline w-4 h-4" />
105
+ </button>
106
+ </div>
107
+ <el-input-number v-model="textForm.fontSize" size="small" :min="8" :max="72" controls-position="right" class="flex-1">
108
+ <template #suffix>px</template>
109
+ </el-input-number>
110
+ </div>
111
+ </div>
112
+
113
+ <!-- Alignment -->
114
+ <div class="flex items-center justify-between border border-gray-200 rounded p-1">
115
+ <button
116
+ v-for="align in ['left', 'center', 'right']"
117
+ :key="align"
118
+ @click="textForm.align = align"
119
+ :class="['p-1 rounded hover:bg-gray-100 transition-colors', textForm.align === align ? 'bg-gray-100 text-purple-600' : 'text-gray-400']"
120
+ >
121
+ <div :class="`i-lucide-align-${align} w-4 h-4`" />
122
+ </button>
123
+ </div>
124
+
125
+ <!-- Color -->
126
+ <div class="flex items-center justify-between">
127
+ <el-checkbox v-model="textForm.hasColor" :label="$t('propertyPanel.text.color')" />
128
+ <el-color-picker v-if="textForm.hasColor" v-model="textForm.color" size="small" />
129
+ </div>
130
+ </div>
131
+ </el-tab-pane>
132
+
133
+ <!-- Arrange Tab -->
134
+ <el-tab-pane :label="$t('propertyPanel.tabs.arrange')" name="arrange" class="h-full overflow-y-auto custom-scrollbar">
135
+ <div class="p-4 space-y-6">
136
+ <!-- Z-Order -->
137
+ <div class="grid grid-cols-2 gap-2">
138
+ <el-button size="small" @click="$emit('z-index', 'front')">{{ $t('propertyPanel.arrange.front') }}</el-button>
139
+ <el-button size="small" @click="$emit('z-index', 'back')">{{ $t('propertyPanel.arrange.back') }}</el-button>
140
+ </div>
141
+
142
+ <!-- Size -->
143
+ <div class="space-y-3">
144
+ <div class="text-xs font-bold text-gray-900">{{ $t('propertyPanel.arrange.size') }}</div>
145
+ <div class="grid grid-cols-2 gap-3">
146
+ <div class="space-y-1">
147
+ <div class="text-[10px] text-gray-400">W</div>
148
+ <el-input-number v-model="arrangeForm.width" size="small" controls-position="right" class="!w-full" />
149
+ </div>
150
+ <div class="space-y-1">
151
+ <div class="text-[10px] text-gray-400">H</div>
152
+ <el-input-number v-model="arrangeForm.height" size="small" controls-position="right" class="!w-full" />
153
+ </div>
154
+ </div>
155
+ </div>
156
+
157
+ <!-- Position -->
158
+ <div class="space-y-3">
159
+ <div class="text-xs font-bold text-gray-900">{{ $t('propertyPanel.arrange.position') }}</div>
160
+ <div class="grid grid-cols-2 gap-3">
161
+ <div class="space-y-1">
162
+ <div class="text-[10px] text-gray-400">X</div>
163
+ <el-input-number v-model="arrangeForm.x" size="small" controls-position="right" class="!w-full" />
164
+ </div>
165
+ <div class="space-y-1">
166
+ <div class="text-[10px] text-gray-400">Y</div>
167
+ <el-input-number v-model="arrangeForm.y" size="small" controls-position="right" class="!w-full" />
168
+ </div>
169
+ </div>
170
+ </div>
171
+ </div>
172
+ </el-tab-pane>
173
+ </el-tabs>
174
+ </div>
175
+ </div>
176
+ </template>
177
+
178
+ <script setup lang="ts">
179
+ import { ref, watch, reactive } from 'vue';
180
+
181
+ const props = defineProps<{
182
+ node: any;
183
+ }>();
184
+
185
+ const emit = defineEmits<{
186
+ (e: 'close'): void;
187
+ (e: 'update', data: any): void;
188
+ (e: 'z-index', action: string): void;
189
+ }>();
190
+
191
+ const activeTab = ref('style');
192
+
193
+ // Internal state initialized from node data
194
+ const styleForm = reactive({
195
+ fill: true,
196
+ fillColor: '#ffffff',
197
+ stroke: true,
198
+ strokeColor: '#000000',
199
+ strokeType: 'solid',
200
+ strokeWidth: 1,
201
+ opacity: 100,
202
+ rounded: true,
203
+ shadow: true,
204
+ });
205
+
206
+ const textForm = reactive({
207
+ content: '',
208
+ fontFamily: 'Helvetica',
209
+ bold: false,
210
+ italic: false,
211
+ underline: false,
212
+ fontSize: 14,
213
+ align: 'center',
214
+ hasColor: true,
215
+ color: '#000000',
216
+ });
217
+
218
+ const arrangeForm = reactive({
219
+ width: 160,
220
+ height: 80,
221
+ x: 0,
222
+ y: 0,
223
+ });
224
+
225
+ // Watch node changes to update form
226
+ watch(() => props.node, (newNode) => {
227
+ if (!newNode) return;
228
+
229
+ // Style
230
+ const style = newNode.data?.style || {};
231
+ styleForm.fill = style.fill !== false;
232
+ styleForm.fillColor = style.backgroundColor || '#ffffff';
233
+ styleForm.stroke = style.stroke !== false;
234
+ styleForm.strokeColor = style.borderColor || '#e5e7eb'; // default border
235
+ styleForm.strokeWidth = style.borderWidth !== undefined ? parseInt(style.borderWidth) : 1;
236
+ styleForm.rounded = style.borderRadius !== '0px';
237
+ styleForm.shadow = !!style.boxShadow;
238
+ styleForm.opacity = (style.opacity !== undefined ? style.opacity : 1) * 100;
239
+
240
+ // Text
241
+ textForm.content = newNode.data?.label || '';
242
+ const textStyle = newNode.data?.textStyle || {};
243
+ textForm.fontSize = parseInt(textStyle.fontSize) || 14;
244
+ textForm.bold = textStyle.fontWeight === 'bold';
245
+ textForm.italic = textStyle.fontStyle === 'italic';
246
+ textForm.color = textStyle.color || '#1f2937';
247
+ textForm.align = textStyle.textAlign || 'center';
248
+
249
+ // Arrange
250
+ arrangeForm.width = newNode.style?.width ? parseInt(newNode.style.width) : 160;
251
+ arrangeForm.height = newNode.style?.height ? parseInt(newNode.style.height) : 80;
252
+ arrangeForm.x = Math.round(newNode.position?.x || 0);
253
+ arrangeForm.y = Math.round(newNode.position?.y || 0);
254
+ }, { immediate: true, deep: true });
255
+
256
+ // Watch form changes to emit updates
257
+ watch(styleForm, () => {
258
+ emit('update', {
259
+ type: 'style',
260
+ data: {
261
+ backgroundColor: styleForm.fill ? styleForm.fillColor : 'transparent',
262
+ borderColor: styleForm.stroke ? styleForm.strokeColor : 'transparent',
263
+ borderWidth: `${styleForm.strokeWidth}px`,
264
+ borderStyle: styleForm.strokeType,
265
+ borderRadius: styleForm.rounded ? '8px' : '0px',
266
+ boxShadow: styleForm.shadow ? '0 4px 6px -1px rgb(0 0 0 / 0.1)' : 'none',
267
+ opacity: styleForm.opacity / 100,
268
+ }
269
+ });
270
+ });
271
+
272
+ watch(textForm, () => {
273
+ emit('update', {
274
+ type: 'text',
275
+ data: {
276
+ label: textForm.content,
277
+ textStyle: {
278
+ fontSize: `${textForm.fontSize}px`,
279
+ fontWeight: textForm.bold ? 'bold' : 'normal',
280
+ fontStyle: textForm.italic ? 'italic' : 'normal',
281
+ textDecoration: textForm.underline ? 'underline' : 'none',
282
+ color: textForm.hasColor ? textForm.color : 'inherit',
283
+ textAlign: textForm.align,
284
+ }
285
+ }
286
+ });
287
+ });
288
+
289
+ watch(arrangeForm, () => {
290
+ emit('update', {
291
+ type: 'arrange',
292
+ data: {
293
+ width: `${arrangeForm.width}px`,
294
+ height: `${arrangeForm.height}px`,
295
+ x: arrangeForm.x,
296
+ y: arrangeForm.y,
297
+ }
298
+ });
299
+ });
300
+
301
+ </script>
302
+
303
+ <style scoped>
304
+ .custom-tabs :deep(.el-tabs__header) {
305
+ margin: 0;
306
+ background-color: transparent;
307
+ border-bottom: 1px solid #f3f4f6;
308
+ }
309
+ .custom-tabs :deep(.el-tabs__nav-wrap::after) {
310
+ display: none;
311
+ }
312
+ .custom-tabs :deep(.el-tabs__item) {
313
+ font-size: 12px;
314
+ font-weight: 600;
315
+ color: #6b7280;
316
+ }
317
+ .custom-tabs :deep(.el-tabs__item.is-active) {
318
+ color: #7c3aed; /* purple-600 */
319
+ }
320
+ .custom-tabs :deep(.el-tabs__active-bar) {
321
+ background-color: #7c3aed;
322
+ }
323
+ .custom-tabs :deep(.el-tabs__content) {
324
+ flex: 1;
325
+ overflow: hidden;
326
+ }
327
+ </style>
@@ -0,0 +1,240 @@
1
+ <template>
2
+ <div class="flex h-full flex-col bg-azure/80 backdrop-blur-xl border-r border-gray-200 w-72 shadow-[4px_0_24px_rgba(0,0,0,0.08)] z-20 relative" style="background-color: azure;">
3
+ <!-- Header -->
4
+ <div class="p-4 border-b border-gray-100">
5
+ <div class="relative group">
6
+ <el-input
7
+ v-model="searchText"
8
+ :placeholder="$t('nodePanel.search')"
9
+ class="w-full !rounded-xl transition-all duration-300"
10
+ >
11
+ <template #prefix>
12
+ <div class="i-lucide-search text-gray-400 group-hover:text-purple-500 transition-colors" />
13
+ </template>
14
+ </el-input>
15
+ </div>
16
+ </div>
17
+
18
+ <!-- Scrollable Content -->
19
+ <div class="flex-1 overflow-y-auto p-4 space-y-6 custom-scrollbar">
20
+ <el-collapse v-model="activeNames" class="custom-collapse border-none">
21
+ <!-- Section: Common -->
22
+ <el-collapse-item :title="$t('nodePanel.general')" name="general">
23
+ <div class="grid grid-cols-1 gap-3 px-1">
24
+ <div
25
+ v-for="node in filteredGeneralNodes"
26
+ :key="node.label"
27
+ class="group relative flex items-center p-2.5 rounded-xl bg-gradient-to-br from-white to-gray-50/50 border border-gray-100 hover:border-purple-200/60 shadow-sm hover:shadow-[0_8px_20px_rgba(168,85,247,0.08)] cursor-grab active:cursor-grabbing transition-all duration-300 hover:-translate-y-0.5"
28
+ :draggable="node.draggable"
29
+ @dragstart="node.draggable && onDragStart($event, node.type)"
30
+ :title="node.fullLabel || node.label"
31
+ >
32
+ <!-- Left: Icon -->
33
+ <div class="w-10 h-10 flex items-center justify-center rounded-xl bg-purple-50/50 group-hover:bg-purple-100/60 group-hover:scale-110 transition-all duration-300">
34
+ <div :class="[node.icon, 'text-lg text-purple-500/80 group-hover:text-purple-600 transition-colors duration-300']" />
35
+ </div>
36
+ <!-- Right: Label -->
37
+ <div class="flex-1 pl-3">
38
+ <span class="text-sm font-medium text-gray-600 group-hover:text-gray-900 transition-colors">{{ node.label }}</span>
39
+ </div>
40
+
41
+ <!-- Drag Indicator -->
42
+ <div class="absolute right-3 opacity-0 group-hover:opacity-100 transition-all duration-300 translate-x-2 group-hover:translate-x-0 text-gray-300">
43
+ <div class="i-lucide-grip-vertical w-4 h-4" />
44
+ </div>
45
+ </div>
46
+ </div>
47
+ </el-collapse-item>
48
+
49
+ <!-- Section: SysML -->
50
+ <el-collapse-item :title="$t('nodePanel.sysml')" name="sysml">
51
+ <div class="grid grid-cols-1 gap-3 px-1">
52
+ <div
53
+ v-for="node in filteredSysmlNodes"
54
+ :key="node.label"
55
+ class="group relative flex items-center p-2.5 rounded-xl bg-gradient-to-br from-white to-gray-50/50 border border-gray-100 hover:border-blue-200/60 shadow-sm hover:shadow-[0_8px_20px_rgba(59,130,246,0.08)] cursor-grab active:cursor-grabbing transition-all duration-300 hover:-translate-y-0.5"
56
+ :draggable="node.draggable"
57
+ @dragstart="node.draggable && onDragStart($event, node.type)"
58
+ :title="node.fullLabel || node.label"
59
+ >
60
+ <!-- Left: Icon -->
61
+ <div class="w-10 h-10 flex items-center justify-center rounded-xl bg-blue-50/50 group-hover:bg-blue-100/60 group-hover:scale-110 transition-all duration-300">
62
+ <div :class="[node.icon, 'text-lg text-blue-500/80 group-hover:text-blue-600 transition-colors duration-300']" />
63
+ </div>
64
+ <!-- Right: Label -->
65
+ <div class="flex-1 pl-3">
66
+ <span class="text-sm font-medium text-gray-600 group-hover:text-gray-900 transition-colors">{{ node.label }}</span>
67
+ </div>
68
+ <!-- Drag Indicator -->
69
+ <div class="absolute right-3 opacity-0 group-hover:opacity-100 transition-all duration-300 translate-x-2 group-hover:translate-x-0 text-gray-300">
70
+ <div class="i-lucide-grip-vertical w-4 h-4" />
71
+ </div>
72
+ </div>
73
+ </div>
74
+ </el-collapse-item>
75
+
76
+ <!-- Section: Advanced -->
77
+ <el-collapse-item :title="$t('nodePanel.advanced')" name="advanced">
78
+ <div class="px-1">
79
+ <div class="flex flex-col items-center justify-center py-8 border border-dashed border-gray-200 rounded-xl bg-gray-50/30">
80
+ <div class="i-lucide-package-open text-xl text-gray-300 mb-2" />
81
+ <span class="text-[10px] text-gray-400 font-medium">{{ $t('nodePanel.noAdvanced') }}</span>
82
+ </div>
83
+ </div>
84
+ </el-collapse-item>
85
+ </el-collapse>
86
+ </div>
87
+
88
+ <!-- Footer -->
89
+ <div class="p-4 border-t border-gray-100 bg-white/40 backdrop-blur-md">
90
+ <el-button class="w-full !rounded-xl !h-10 !border-gray-200/60 !bg-white/80 hover:!bg-white hover:!border-purple-200 hover:!text-purple-600 !shadow-sm hover:!shadow-lg hover:!shadow-purple-500/10 transition-all duration-300 group">
91
+ <div class="i-lucide-plus mr-2 group-hover:scale-110 transition-transform duration-300" />
92
+ <span class="text-xs font-medium">{{ $t('nodePanel.moreShapes') }}</span>
93
+ </el-button>
94
+ </div>
95
+ </div>
96
+ </template>
97
+
98
+ <style scoped>
99
+ .custom-scrollbar::-webkit-scrollbar {
100
+ width: 4px;
101
+ }
102
+ .custom-scrollbar::-webkit-scrollbar-track {
103
+ background: transparent;
104
+ }
105
+ .custom-scrollbar::-webkit-scrollbar-thumb {
106
+ background: #e5e7eb;
107
+ border-radius: 4px;
108
+ }
109
+ .custom-scrollbar::-webkit-scrollbar-thumb:hover {
110
+ background: #d1d5db;
111
+ }
112
+
113
+ /* Custom Collapse Styles */
114
+ .custom-collapse :deep(.el-collapse-item__header) {
115
+ background-color: transparent;
116
+ border-bottom: none;
117
+ font-size: 0.75rem; /* text-xs */
118
+ font-weight: 700; /* font-bold */
119
+ color: #9ca3af; /* text-gray-400 */
120
+ text-transform: uppercase;
121
+ letter-spacing: 0.05em; /* tracking-wider */
122
+ height: 32px;
123
+ line-height: 32px;
124
+ margin-bottom: 0.5rem;
125
+ }
126
+ .custom-collapse :deep(.el-collapse-item__header:hover) {
127
+ color: #374151; /* text-gray-700 */
128
+ }
129
+ .custom-collapse :deep(.el-collapse-item__wrap) {
130
+ background-color: transparent;
131
+ border-bottom: none;
132
+ }
133
+ .custom-collapse :deep(.el-collapse-item__content) {
134
+ padding-bottom: 0.5rem;
135
+ }
136
+ .custom-collapse :deep(.el-collapse-item__arrow) {
137
+ margin: 0 0.5rem 0 0;
138
+ color: inherit;
139
+ transform: rotate(-90deg);
140
+ transition: transform 0.3s;
141
+ }
142
+ .custom-collapse :deep(.el-collapse-item.is-active .el-collapse-item__arrow) {
143
+ transform: rotate(0deg);
144
+ }
145
+ </style>
146
+
147
+ <script setup lang="ts">
148
+ import { ref, computed } from 'vue';
149
+ import { useI18n } from 'vue-i18n';
150
+
151
+ const { t } = useI18n();
152
+ const searchText = ref('');
153
+ const activeNames = ref(['general', 'sysml']);
154
+
155
+ const generalNodes = computed(() => [
156
+ {
157
+ type: 'custom-ai',
158
+ label: t('nodePanel.nodes.ai'),
159
+ fullLabel: t('nodePanel.nodes.aiFull'),
160
+ icon: 'i-lucide-brain-circuit',
161
+ wrapperClass: 'hover:bg-purple-50 hover:border-purple-200',
162
+ iconClass: 'text-purple-600',
163
+ textClass: 'group-hover:text-purple-700',
164
+ draggable: true,
165
+ showLabel: true
166
+ },
167
+ {
168
+ type: 'custom-normal',
169
+ label: t('nodePanel.nodes.action'),
170
+ fullLabel: t('nodePanel.nodes.actionFull'),
171
+ icon: 'i-lucide-square-activity',
172
+ wrapperClass: 'hover:bg-blue-50 hover:border-blue-200',
173
+ iconClass: 'text-blue-600',
174
+ textClass: 'group-hover:text-blue-700',
175
+ draggable: true,
176
+ showLabel: true
177
+ },
178
+ // { label: t('nodePanel.nodes.square'), icon: 'i-lucide-square', iconClass: 'text-gray-400', wrapperClass: 'hover:bg-gray-100', showLabel: false, type: '', draggable: false },
179
+ // { label: t('nodePanel.nodes.circle'), icon: 'i-lucide-circle', iconClass: 'text-gray-400', wrapperClass: 'hover:bg-gray-100', showLabel: false, type: '', draggable: false },
180
+ // { label: t('nodePanel.nodes.diamond'), icon: 'i-lucide-diamond', iconClass: 'text-gray-400', wrapperClass: 'hover:bg-gray-100', showLabel: false, type: '', draggable: false },
181
+ // { label: t('nodePanel.nodes.triangle'), icon: 'i-lucide-triangle', iconClass: 'text-gray-400', wrapperClass: 'hover:bg-gray-100', showLabel: false, type: '', draggable: false },
182
+ // { label: t('nodePanel.nodes.cylinder'), icon: 'i-lucide-cylinder', iconClass: 'text-gray-400', wrapperClass: 'hover:bg-gray-100', showLabel: false, type: '', draggable: false },
183
+ // { label: t('nodePanel.nodes.file'), icon: 'i-lucide-file', iconClass: 'text-gray-400', wrapperClass: 'hover:bg-gray-100', showLabel: false, type: '', draggable: false },
184
+ ]);
185
+
186
+ const sysmlNodes = computed(() => [
187
+ {
188
+ type: 'sysml-block',
189
+ label: t('nodePanel.nodes.sysmlBlock'),
190
+ fullLabel: t('nodePanel.nodes.sysmlBlockFull'),
191
+ icon: 'i-lucide-box',
192
+ wrapperClass: 'hover:bg-gray-100 hover:border-gray-400',
193
+ iconClass: 'text-gray-700',
194
+ textClass: 'group-hover:text-gray-900',
195
+ draggable: true,
196
+ showLabel: true
197
+ },
198
+ {
199
+ type: 'sysml-req',
200
+ label: t('nodePanel.nodes.sysmlReq'),
201
+ fullLabel: t('nodePanel.nodes.sysmlReqFull'),
202
+ icon: 'i-lucide-file-text',
203
+ wrapperClass: 'hover:bg-yellow-50 hover:border-yellow-200',
204
+ iconClass: 'text-yellow-600',
205
+ textClass: 'group-hover:text-yellow-700',
206
+ draggable: true,
207
+ showLabel: true
208
+ },
209
+ {
210
+ type: 'sysml-usecase',
211
+ label: t('nodePanel.nodes.sysmlUseCase'),
212
+ fullLabel: t('nodePanel.nodes.sysmlUseCaseFull'),
213
+ icon: 'i-lucide-circle',
214
+ wrapperClass: 'hover:bg-green-50 hover:border-green-200',
215
+ iconClass: 'text-green-600',
216
+ textClass: 'group-hover:text-green-700',
217
+ draggable: true,
218
+ showLabel: true
219
+ },
220
+ ]);
221
+
222
+ const filterNodes = (nodes: any[], query: string) => {
223
+ if (!query) return nodes;
224
+ return nodes.filter(node =>
225
+ node.label.toLowerCase().includes(query) ||
226
+ (node.fullLabel && node.fullLabel.toLowerCase().includes(query))
227
+ );
228
+ };
229
+
230
+ const filteredGeneralNodes = computed(() => filterNodes(generalNodes.value, searchText.value.toLowerCase()));
231
+ const filteredSysmlNodes = computed(() => filterNodes(sysmlNodes.value, searchText.value.toLowerCase()));
232
+
233
+
234
+ const onDragStart = (event: DragEvent, nodeType: string) => {
235
+ if (event.dataTransfer) {
236
+ event.dataTransfer.setData('application/vueflow', nodeType);
237
+ event.dataTransfer.effectAllowed = 'move';
238
+ }
239
+ };
240
+ </script>