@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
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "@andrewyang/ai-workflow-editor",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "private": false,
6
+ "scripts": {
7
+ "build": "nuxt build",
8
+ "dev": "nuxt dev",
9
+ "generate": "nuxt generate",
10
+ "preview": "nuxt preview",
11
+ "postinstall": "nuxt prepare"
12
+ },
13
+ "dependencies": {
14
+ "@element-plus/nuxt": "^1.1.5",
15
+ "@langchain/community": "^0.3.28",
16
+ "@langchain/core": "^0.3.37",
17
+ "@langchain/openai": "^0.4.2",
18
+ "@nuxtjs/i18n": "^10.2.3",
19
+ "@vue-flow/background": "^1.3.0",
20
+ "@vue-flow/controls": "^1.1.2",
21
+ "@vue-flow/core": "^1.41.0",
22
+ "@vue-flow/node-resizer": "^1.5.1",
23
+ "clsx": "^2.1.0",
24
+ "element-plus": "^2.13.2",
25
+ "lucide-vue-next": "^0.344.0",
26
+ "nuxt": "^3.0.0",
27
+ "tailwind-merge": "^2.2.1",
28
+ "vue": "^3.4.0",
29
+ "zod": "^3.22.4"
30
+ },
31
+ "devDependencies": {
32
+ "@unocss/nuxt": "^0.58.5",
33
+ "typescript": "^5.3.3"
34
+ },
35
+ "peerDependencies": {
36
+ "nuxt": "^3.0.0",
37
+ "vue": "^3.4.0"
38
+ },
39
+ "overrides": {
40
+ "glob": "^10.3.10"
41
+ }
42
+ }
package/pnpm-lock.yaml ADDED
@@ -0,0 +1,24 @@
1
+ lockfileVersion: '9.0'
2
+
3
+ settings:
4
+ autoInstallPeers: true
5
+ excludeLinksFromLockfile: false
6
+
7
+ importers:
8
+
9
+ .:
10
+ dependencies:
11
+ pnpm:
12
+ specifier: ^10.29.2
13
+ version: 10.29.2
14
+
15
+ packages:
16
+
17
+ pnpm@10.29.2:
18
+ resolution: {integrity: sha512-vvQ/p1nZH9LaSzGaWg0T73pFu5haPXNCBYRw+dIFGjuoZ05ilnJlRobvlEOtE6gtor657rPgIhyHuBVP/510uA==}
19
+ engines: {node: '>=18.12'}
20
+ hasBin: true
21
+
22
+ snapshots:
23
+
24
+ pnpm@10.29.2: {}
package/src/app.vue ADDED
@@ -0,0 +1,46 @@
1
+ <template>
2
+ <el-config-provider :locale="zhCn">
3
+ <div class="h-screen w-screen bg-white">
4
+ <ClientOnly>
5
+ <AiWorkflowEditor
6
+ :default-llm-config="llmConfig"
7
+ @save="onSave"
8
+ />
9
+ <template #fallback>
10
+ <div class="flex h-full w-full items-center justify-center bg-gray-50">
11
+ <div class="flex flex-col items-center gap-3">
12
+ <div class="i-lucide-loader-2 animate-spin text-3xl text-purple-600" />
13
+ <span class="text-sm font-medium text-gray-500">{{ $t('common.loading') }}</span>
14
+ </div>
15
+ </div>
16
+ </template>
17
+ </ClientOnly>
18
+ </div>
19
+ </el-config-provider>
20
+ </template>
21
+
22
+ <script setup lang="ts">
23
+ import { ref } from 'vue';
24
+ import type { LlmConfig } from '@/types/ai';
25
+ import zhCn from 'element-plus/es/locale/lang/zh-cn';
26
+
27
+ const llmConfig = ref<LlmConfig>({
28
+ provider: 'ollama',
29
+ model: 'qwen3:8b',
30
+ temperature: 0.7,
31
+ // baseUrl: 'http://192.168.1.10:11434',
32
+ baseUrl: 'http://localhost:11434/',
33
+ });
34
+
35
+ const onSave = (workflow: any) => {
36
+ console.log('Saved workflow:', workflow);
37
+ };
38
+ </script>
39
+
40
+ <style>
41
+ body {
42
+ margin: 0;
43
+ overflow: hidden;
44
+ background-color: #ffffff;
45
+ }
46
+ </style>
@@ -0,0 +1,142 @@
1
+ <template>
2
+ <div class="ai-workflow-editor flex h-screen w-full flex-col bg-gray-50 font-sans text-gray-900">
3
+ <!-- Top Bar -->
4
+ <Toolbar
5
+ @save="handleSave"
6
+ @import="handleImport"
7
+ @zoom-in="canvasRef?.zoomIn()"
8
+ @zoom-out="canvasRef?.zoomOut()"
9
+ @fit-view="canvasRef?.fitView()"
10
+ />
11
+
12
+ <!-- Main Content Area -->
13
+ <div class="flex flex-1 overflow-hidden">
14
+ <!-- Left Sidebar: Shapes -->
15
+ <NodePanel />
16
+
17
+ <!-- Center: Canvas -->
18
+ <div class="flex-1 relative bg-white">
19
+ <Canvas
20
+ ref="canvasRef"
21
+ @node-click="handleNodeClick"
22
+ @edge-click="handleEdgeClick"
23
+ @pane-click="handlePaneClick"
24
+ @node-double-click="handleNodeDoubleClick"
25
+ />
26
+
27
+ <!-- Loading Overlay -->
28
+ <div v-if="aiLoading" class="absolute inset-0 z-50 flex items-center justify-center bg-white/50 backdrop-blur-sm">
29
+ <div class="flex flex-col items-center gap-3">
30
+ <div class="i-lucide-loader-2 text-4xl text-purple-600 animate-spin" />
31
+ <span class="text-sm font-medium text-purple-600">{{ $t('common.generating') }}</span>
32
+ </div>
33
+ </div>
34
+ </div>
35
+
36
+ <!-- Right Sidebar: Format Panel or AI Assistant -->
37
+ <FormatPanel
38
+ v-if="selectedNode"
39
+ :node="selectedNode"
40
+ @close="selectedNode = null"
41
+ @update="handleNodeUpdate"
42
+ @z-index="handleZIndex"
43
+ />
44
+ <AiChatPanel
45
+ v-else
46
+ :loading="aiLoading"
47
+ @generate="handleAiGenerate"
48
+ />
49
+ </div>
50
+ </div>
51
+ </template>
52
+
53
+ <script setup lang="ts">
54
+ import { ref } from 'vue';
55
+ import Canvas from './editor/Canvas.vue';
56
+ import Toolbar from './editor/Toolbar.vue';
57
+ import NodePanel from './editor/NodePanel.vue';
58
+ import AiChatPanel from './ai/AiChatPanel.vue';
59
+ import FormatPanel from './editor/FormatPanel.vue';
60
+ import { useAi } from '@/composables/useAi';
61
+ import type { LlmConfig } from '@/types/ai';
62
+
63
+ const props = defineProps<{
64
+ initialWorkflow?: { nodes: any[]; edges: any[] };
65
+ defaultLlmConfig?: LlmConfig;
66
+ }>();
67
+
68
+ const emit = defineEmits<{
69
+ (e: 'save', workflow: any): void;
70
+ (e: 'node-click', node: any): void;
71
+ (e: 'edge-click', edge: any): void;
72
+ (e: 'ai-generate-success', workflow: any): void;
73
+ }>();
74
+
75
+ const canvasRef = ref<InstanceType<typeof Canvas>>();
76
+ const { generateWorkflow, loading: aiLoading, error: aiError } = useAi();
77
+ const selectedNode = ref<any>(null);
78
+
79
+ const handleSave = () => {
80
+ const workflow = canvasRef.value?.exportWorkflow();
81
+ if (workflow) emit('save', workflow);
82
+ };
83
+
84
+ const handleImport = () => {
85
+ console.log('Import triggered');
86
+ // Implementation for file import can be added here
87
+ };
88
+
89
+ const handleAiGenerate = async (prompt: string) => {
90
+ if (!props.defaultLlmConfig) {
91
+ console.error('LLM Config not provided');
92
+ // In a real app, prompt user to configure LLM
93
+ return;
94
+ }
95
+
96
+ try {
97
+ const workflow = await generateWorkflow({
98
+ prompt,
99
+ llmConfig: props.defaultLlmConfig,
100
+ });
101
+ canvasRef.value?.importWorkflow(workflow);
102
+ emit('ai-generate-success', workflow);
103
+ } catch (e) {
104
+ console.error(`AI Generation failed: ${e}`);
105
+ // Ideally show a toast notification here
106
+ }
107
+ };
108
+
109
+ const handleNodeClick = (node: any) => {
110
+ selectedNode.value = node;
111
+ emit('node-click', node);
112
+ };
113
+
114
+ const handlePaneClick = () => {
115
+ selectedNode.value = null;
116
+ };
117
+
118
+ const handleNodeDoubleClick = (node: any) => {
119
+ selectedNode.value = node;
120
+ if (node.data) {
121
+ node.data.isEditing = true;
122
+ }
123
+ };
124
+
125
+ const handleNodeUpdate = (update: { type: string; data: any }) => {
126
+ if (!selectedNode.value) return;
127
+ canvasRef.value?.updateNode(selectedNode.value.id, update);
128
+ };
129
+
130
+ const handleZIndex = (action: string) => {
131
+ if (!selectedNode.value) return;
132
+ // Implementation for Z-Index change would go here or in Canvas
133
+ console.log('Z-Index action:', action);
134
+ };
135
+
136
+ const handleEdgeClick = (edge: any) => emit('edge-click', edge);
137
+
138
+ defineExpose({
139
+ exportWorkflow: () => canvasRef.value?.exportWorkflow(),
140
+ importWorkflow: (data: any) => canvasRef.value?.importWorkflow(data),
141
+ });
142
+ </script>
@@ -0,0 +1,135 @@
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="flex items-center gap-2 font-semibold text-gray-800">
6
+ <div class="i-lucide-sparkles text-purple-600" />
7
+ <span>{{ $t('aiChat.title') }}</span>
8
+ </div>
9
+ <div class="flex gap-2 text-gray-400">
10
+ <el-button link class="!text-gray-400 hover:!text-gray-600 !p-1.5">
11
+ <div class="i-lucide-settings w-4 h-4" />
12
+ </el-button>
13
+ <el-button link class="!text-gray-400 hover:!text-gray-600 !p-1.5">
14
+ <div class="i-lucide-panel-right-close w-4 h-4" />
15
+ </el-button>
16
+ </div>
17
+ </div>
18
+
19
+ <!-- Content -->
20
+ <div class="flex-1 overflow-y-auto p-4 space-y-6">
21
+ <!-- Promo Card -->
22
+ <div class="rounded-xl bg-purple-50 p-4 border border-purple-100">
23
+ <div class="flex items-center gap-2 mb-2">
24
+ <span class="bg-purple-600 text-white text-[10px] px-1.5 py-0.5 rounded font-medium">{{ $t('aiChat.new') }}</span>
25
+ <span class="text-xs font-semibold text-purple-900">{{ $t('aiChat.v2') }}</span>
26
+ </div>
27
+ <p class="text-xs text-purple-700/80 leading-relaxed">
28
+ {{ $t('aiChat.desc') }}
29
+ </p>
30
+ </div>
31
+
32
+ <!-- Welcome Text -->
33
+ <div class="text-center py-4">
34
+ <h3 class="text-lg font-bold text-gray-900 mb-2">{{ $t('aiChat.createTitle') }}</h3>
35
+ <p class="text-sm text-gray-500">{{ $t('aiChat.createDesc') }}</p>
36
+ </div>
37
+
38
+ <!-- Quick Actions -->
39
+ <div class="space-y-3">
40
+ <div class="text-xs font-semibold text-gray-400 uppercase tracking-wider">{{ $t('aiChat.quickStarts') }}</div>
41
+
42
+ <button @click="setInput($t('aiChat.quickInputs.doc'))" class="w-full text-left group flex items-start gap-3 p-3 rounded-xl border border-gray-200 hover:border-purple-300 hover:shadow-md hover:shadow-purple-500/5 transition-all bg-white">
43
+ <div class="flex-shrink-0 w-8 h-8 rounded-full bg-gray-50 flex items-center justify-center text-gray-500 group-hover:text-purple-600 group-hover:bg-purple-50 transition-colors">
44
+ <div class="i-lucide-file-text w-4 h-4" />
45
+ </div>
46
+ <div>
47
+ <div class="text-sm font-medium text-gray-900">{{ $t('aiChat.docProcess.title') }}</div>
48
+ <div class="text-xs text-gray-500 mt-0.5">{{ $t('aiChat.docProcess.desc') }}</div>
49
+ </div>
50
+ </button>
51
+
52
+ <button @click="setInput($t('aiChat.quickInputs.auth'))" class="w-full text-left group flex items-start gap-3 p-3 rounded-xl border border-gray-200 hover:border-purple-300 hover:shadow-md hover:shadow-purple-500/5 transition-all bg-white">
53
+ <div class="flex-shrink-0 w-8 h-8 rounded-full bg-gray-50 flex items-center justify-center text-gray-500 group-hover:text-purple-600 group-hover:bg-purple-50 transition-colors">
54
+ <div class="i-lucide-user-plus w-4 h-4" />
55
+ </div>
56
+ <div>
57
+ <div class="text-sm font-medium text-gray-900">{{ $t('aiChat.userAuth.title') }}</div>
58
+ <div class="text-xs text-gray-500 mt-0.5">{{ $t('aiChat.userAuth.desc') }}</div>
59
+ </div>
60
+ </button>
61
+
62
+ <button @click="setInput($t('aiChat.quickInputs.etl'))" class="w-full text-left group flex items-start gap-3 p-3 rounded-xl border border-gray-200 hover:border-purple-300 hover:shadow-md hover:shadow-purple-500/5 transition-all bg-white">
63
+ <div class="flex-shrink-0 w-8 h-8 rounded-full bg-gray-50 flex items-center justify-center text-gray-500 group-hover:text-purple-600 group-hover:bg-purple-50 transition-colors">
64
+ <div class="i-lucide-database w-4 h-4" />
65
+ </div>
66
+ <div>
67
+ <div class="text-sm font-medium text-gray-900">{{ $t('aiChat.etl.title') }}</div>
68
+ <div class="text-xs text-gray-500 mt-0.5">{{ $t('aiChat.etl.desc') }}</div>
69
+ </div>
70
+ </button>
71
+ </div>
72
+ </div>
73
+
74
+ <!-- Input Area -->
75
+ <div class="p-4 border-t border-gray-100 bg-gray-50/50">
76
+ <div class="relative">
77
+ <el-input
78
+ v-model="prompt"
79
+ type="textarea"
80
+ :rows="3"
81
+ :placeholder="$t('aiChat.inputPlaceholder')"
82
+ @keydown.enter.prevent="handleGenerate"
83
+ resize="none"
84
+ />
85
+
86
+ <div class="flex items-center justify-between mt-2">
87
+ <div class="flex gap-2">
88
+ <el-button link class="!p-1.5 !text-gray-400 hover:!text-gray-600 hover:!bg-gray-100 !rounded-lg">
89
+ <div class="i-lucide-history w-4 h-4" />
90
+ </el-button>
91
+ <el-button link class="!p-1.5 !text-gray-400 hover:!text-gray-600 hover:!bg-gray-100 !rounded-lg">
92
+ <div class="i-lucide-paperclip w-4 h-4" />
93
+ </el-button>
94
+ </div>
95
+
96
+ <el-button
97
+ @click="handleGenerate"
98
+ :loading="loading"
99
+ :disabled="!prompt.trim()"
100
+ color="#111827"
101
+ class="!flex !items-center !gap-2 !px-4 !py-1.5 !rounded-lg !text-sm !font-medium !text-white"
102
+ >
103
+ <span v-if="!loading">{{ $t('aiChat.generate') }}</span>
104
+ <div v-if="!loading" class="i-lucide-send w-3.5 h-3.5" />
105
+ </el-button>
106
+ </div>
107
+ </div>
108
+ </div>
109
+ </div>
110
+ </template>
111
+
112
+ <script setup lang="ts">
113
+ import { ref } from 'vue';
114
+
115
+ const props = defineProps<{
116
+ loading?: boolean;
117
+ }>();
118
+
119
+ const emit = defineEmits<{
120
+ (e: 'generate', prompt: string): void;
121
+ }>();
122
+
123
+ const prompt = ref('');
124
+
125
+ const setInput = (text: string) => {
126
+ prompt.value = text;
127
+ };
128
+
129
+ const handleGenerate = () => {
130
+ if (!prompt.value.trim() || props.loading) return;
131
+ emit('generate', prompt.value);
132
+ // Optional: clear prompt or keep it for refinement?
133
+ // prompt.value = '';
134
+ };
135
+ </script>
@@ -0,0 +1,62 @@
1
+ <template>
2
+ <div v-if="visible" class="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm">
3
+ <div class="w-full max-w-lg rounded-xl border border-gray-700 bg-gray-900 p-6 shadow-2xl">
4
+ <h3 class="mb-4 text-lg font-semibold text-white flex items-center gap-2">
5
+ <div class="i-lucide-sparkles text-purple-500" />
6
+ {{ $t('aiGenerator.title') }}
7
+ </h3>
8
+
9
+ <el-input
10
+ v-model="prompt"
11
+ type="textarea"
12
+ :rows="4"
13
+ class="custom-dark-input"
14
+ :placeholder="$t('aiGenerator.placeholder')"
15
+ />
16
+
17
+ <div class="mt-6 flex justify-end gap-3">
18
+ <el-button @click="close" text class="!text-gray-400 hover:!text-white hover:!bg-gray-800">
19
+ {{ $t('aiGenerator.cancel') }}
20
+ </el-button>
21
+ <el-button @click="confirm" color="#9333ea" class="!text-white !shadow-lg !shadow-purple-500/20">
22
+ {{ $t('aiGenerator.generate') }}
23
+ </el-button>
24
+ </div>
25
+ </div>
26
+ </div>
27
+ </template>
28
+
29
+ <style scoped>
30
+ :deep(.custom-dark-input .el-textarea__inner) {
31
+ background-color: #030712;
32
+ border-color: #374151;
33
+ color: #e5e7eb;
34
+ }
35
+ :deep(.custom-dark-input .el-textarea__inner:focus) {
36
+ border-color: #a855f7;
37
+ box-shadow: 0 0 0 1px #a855f7;
38
+ }
39
+ </style>
40
+
41
+ <script setup lang="ts">
42
+ import { ref, watch } from 'vue';
43
+
44
+ const props = defineProps<{ visible: boolean }>();
45
+ const emit = defineEmits(['update:visible', 'confirm']);
46
+
47
+ const prompt = ref('');
48
+
49
+ const close = () => {
50
+ emit('update:visible', false);
51
+ };
52
+
53
+ const confirm = () => {
54
+ if (!prompt.value.trim()) return;
55
+ emit('confirm', prompt.value);
56
+ close();
57
+ };
58
+
59
+ watch(() => props.visible, (newVal) => {
60
+ if (newVal) prompt.value = '';
61
+ });
62
+ </script>
@@ -0,0 +1,175 @@
1
+ <template>
2
+ <div class="h-full w-full bg-gray-50/50">
3
+ <VueFlow
4
+ v-model:nodes="nodes"
5
+ v-model:edges="edges"
6
+ :default-viewport="{ zoom: 1 }"
7
+ :min-zoom="0.2"
8
+ :max-zoom="4"
9
+ fit-view-on-init
10
+ @node-click="handleNodeClick"
11
+ @edge-click="handleEdgeClick"
12
+ @connect="onConnect"
13
+ @pane-ready="onPaneReady"
14
+ @dragover="onDragOver"
15
+ @drop="onDrop"
16
+ @pane-click="$emit('pane-click')"
17
+ @node-double-click="$emit('node-double-click', $event.node)"
18
+ >
19
+ <Background pattern-color="#e5e7eb" :gap="20" :size="1.5" variant="lines" />
20
+
21
+ <!-- Custom Nodes -->
22
+ <template #node-custom-ai="props">
23
+ <AiNode :data="props.data" :selected="props.selected" />
24
+ </template>
25
+
26
+ <template #node-custom-normal="props">
27
+ <NormalNode :data="props.data" :selected="props.selected" />
28
+ </template>
29
+
30
+ <!-- SysML Nodes -->
31
+ <template #node-sysml-block="props">
32
+ <SysmlBlockNode :data="props.data" :selected="props.selected" />
33
+ </template>
34
+ <template #node-sysml-req="props">
35
+ <SysmlRequirementNode :data="props.data" :selected="props.selected" />
36
+ </template>
37
+ <template #node-sysml-usecase="props">
38
+ <SysmlUseCaseNode :data="props.data" :selected="props.selected" />
39
+ </template>
40
+ </VueFlow>
41
+ </div>
42
+ </template>
43
+
44
+ <script setup lang="ts">
45
+ import { ref } from 'vue';
46
+ import { VueFlow, type Node, type Edge, addEdge } from '@vue-flow/core';
47
+ import { Background } from '@vue-flow/background';
48
+ import AiNode from './nodes/AiNode.vue';
49
+ import NormalNode from './nodes/NormalNode.vue';
50
+ import SysmlBlockNode from './nodes/SysmlBlockNode.vue';
51
+ import SysmlRequirementNode from './nodes/SysmlRequirementNode.vue';
52
+ import SysmlUseCaseNode from './nodes/SysmlUseCaseNode.vue';
53
+ import '@vue-flow/core/dist/style.css';
54
+ import '@vue-flow/core/dist/theme-default.css';
55
+
56
+ const nodes = ref<Node[]>([]);
57
+ const edges = ref<Edge[]>([]);
58
+
59
+ const emit = defineEmits<{
60
+ (e: 'node-click', node: Node): void;
61
+ (e: 'edge-click', edge: Edge): void;
62
+ (e: 'pane-click'): void;
63
+ (e: 'node-double-click', node: Node): void;
64
+ }>();
65
+
66
+ const updateNode = (id: string, update: { type: string; data: any }) => {
67
+ const node = nodes.value.find((n) => n.id === id);
68
+ if (!node) return;
69
+
70
+ if (update.type === 'style') {
71
+ // Merge styles into data.style for persistence/reactivity in custom node
72
+ node.data = {
73
+ ...node.data,
74
+ style: {
75
+ ...(node.data.style || {}),
76
+ ...update.data,
77
+ },
78
+ };
79
+ } else if (update.type === 'text') {
80
+ node.data = {
81
+ ...node.data,
82
+ label: update.data.label,
83
+ textStyle: {
84
+ ...(node.data.textStyle || {}),
85
+ ...update.data.textStyle,
86
+ },
87
+ };
88
+ } else if (update.type === 'arrange') {
89
+ // Update position and dimensions
90
+ if (update.data.x !== undefined) node.position.x = update.data.x;
91
+ if (update.data.y !== undefined) node.position.y = update.data.y;
92
+
93
+ // Update dimensions via style (standard way in Vue Flow for resizable nodes)
94
+ node.style = {
95
+ ...(node.style || {}),
96
+ width: update.data.width,
97
+ height: update.data.height,
98
+ };
99
+ }
100
+ };
101
+
102
+ const handleNodeClick = (event: any) => {
103
+ emit('node-click', event.node);
104
+ };
105
+
106
+ const handleEdgeClick = (event: any) => {
107
+ emit('edge-click', event.edge);
108
+ };
109
+
110
+ const onConnect = (params: any) => {
111
+ edges.value = addEdge(params, edges.value);
112
+ };
113
+
114
+ let vueFlowInstance: any = null;
115
+
116
+ const onPaneReady = (instance: any) => {
117
+ vueFlowInstance = instance;
118
+ };
119
+
120
+ const onDragOver = (event: DragEvent) => {
121
+ event.preventDefault();
122
+ if (event.dataTransfer) {
123
+ event.dataTransfer.dropEffect = 'move';
124
+ }
125
+ };
126
+
127
+ const onDrop = (event: DragEvent) => {
128
+ const type = event.dataTransfer?.getData('application/vueflow');
129
+ if (!type || !vueFlowInstance) return;
130
+
131
+ const position = vueFlowInstance.project({
132
+ x: event.clientX,
133
+ y: event.clientY,
134
+ });
135
+
136
+ const newNode = {
137
+ id: `node-${Date.now()}`,
138
+ type,
139
+ position,
140
+ data: {
141
+ label: type === 'custom-ai' ? 'AI Process' :
142
+ type === 'custom-normal' ? 'Action Step' :
143
+ type === 'sysml-block' ? 'New Block' :
144
+ type === 'sysml-req' ? 'New Requirement' :
145
+ type === 'sysml-usecase' ? 'New Use Case' : 'Node'
146
+ },
147
+ };
148
+
149
+ nodes.value.push(newNode);
150
+ };
151
+
152
+ defineExpose({
153
+ addNode: (node: Node) => nodes.value.push(node),
154
+ exportWorkflow: () => ({ nodes: nodes.value, edges: edges.value }),
155
+ importWorkflow: (data: { nodes: Node[]; edges: Edge[] }) => {
156
+ nodes.value = data.nodes;
157
+ edges.value = data.edges;
158
+ },
159
+ updateNode,
160
+ zoomIn: () => vueFlowInstance?.zoomIn(),
161
+ zoomOut: () => vueFlowInstance?.zoomOut(),
162
+ fitView: () => vueFlowInstance?.fitView(),
163
+ });
164
+ </script>
165
+
166
+ <style>
167
+ /* Custom edge styles for light theme */
168
+ .vue-flow__edge-path {
169
+ stroke: #94a3b8; /* slate-400 */
170
+ stroke-width: 2;
171
+ }
172
+ .vue-flow__edge.selected .vue-flow__edge-path {
173
+ stroke: #7c3aed; /* purple-600 */
174
+ }
175
+ </style>