@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.
- package/.nuxt/app.config.mjs +18 -0
- package/.nuxt/components.d.ts +910 -0
- package/.nuxt/dev/index.mjs +4103 -0
- package/.nuxt/dev/index.mjs.map +1 -0
- package/.nuxt/dist/server/client.manifest.mjs +4 -0
- package/.nuxt/dist/server/client.precomputed.mjs +1 -0
- package/.nuxt/dist/server/server.mjs +1 -0
- package/.nuxt/i18n-route-resources.mjs +3 -0
- package/.nuxt/imports.d.ts +43 -0
- package/.nuxt/manifest/latest.json +1 -0
- package/.nuxt/manifest/meta/dev.json +1 -0
- package/.nuxt/nitro.json +17 -0
- package/.nuxt/nuxt.d.ts +24 -0
- package/.nuxt/nuxt.json +9 -0
- package/.nuxt/schema/nuxt.schema.d.ts +17 -0
- package/.nuxt/schema/nuxt.schema.json +3 -0
- package/.nuxt/tsconfig.json +234 -0
- package/.nuxt/tsconfig.server.json +185 -0
- package/.nuxt/types/app-defaults.d.ts +7 -0
- package/.nuxt/types/app.config.d.ts +31 -0
- package/.nuxt/types/build.d.ts +29 -0
- package/.nuxt/types/builder-env.d.ts +1 -0
- package/.nuxt/types/components.d.ts +915 -0
- package/.nuxt/types/i18n-plugin.d.ts +123 -0
- package/.nuxt/types/imports.d.ts +993 -0
- package/.nuxt/types/middleware.d.ts +17 -0
- package/.nuxt/types/nitro-config.d.ts +14 -0
- package/.nuxt/types/nitro-imports.d.ts +170 -0
- package/.nuxt/types/nitro-layouts.d.ts +17 -0
- package/.nuxt/types/nitro-nuxt.d.ts +39 -0
- package/.nuxt/types/nitro-routes.d.ts +17 -0
- package/.nuxt/types/nitro.d.ts +3 -0
- package/.nuxt/types/plugins.d.ts +43 -0
- package/.nuxt/types/schema.d.ts +213 -0
- package/.nuxt/types/vue-shim.d.ts +0 -0
- package/.trae/rules/rule.md +38 -0
- package/.vscode/settings.json +5 -0
- package/nuxt.config.ts +38 -0
- package/package.json +42 -0
- package/pnpm-lock.yaml +24 -0
- package/src/app.vue +46 -0
- package/src/components/AiWorkflowEditor.vue +142 -0
- package/src/components/ai/AiChatPanel.vue +135 -0
- package/src/components/ai/AiGenerator.vue +62 -0
- package/src/components/editor/Canvas.vue +175 -0
- package/src/components/editor/FormatPanel.vue +327 -0
- package/src/components/editor/NodePanel.vue +240 -0
- package/src/components/editor/PropertyPanel.vue +348 -0
- package/src/components/editor/Toolbar.vue +49 -0
- package/src/components/editor/nodes/AiNode.vue +77 -0
- package/src/components/editor/nodes/NormalNode.vue +75 -0
- package/src/components/editor/nodes/SysmlBlockNode.vue +72 -0
- package/src/components/editor/nodes/SysmlRequirementNode.vue +72 -0
- package/src/components/editor/nodes/SysmlUseCaseNode.vue +62 -0
- package/src/composables/useAi.ts +82 -0
- package/src/i18n.config.ts +5 -0
- package/src/locales/en.json +106 -0
- package/src/locales/zh.json +106 -0
- package/src/plugins/aiWorkflowEditor.ts +6 -0
- package/src/types/ai.ts +7 -0
- package/src/types/workflow.ts +25 -0
- package/src/utils/llmAdapter.ts +46 -0
- package/tsconfig.json +3 -0
- package/uno.config.ts +15 -0
- 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>
|