Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
"nanoid": "^6.0.1",
"nprogress": "^0.2.0",
"pinia": "^3.0.4",
"sortablejs": "^1.15.7",
"vue": "^3.5.38",
"vue-codemirror": "^6.1.1",
"vue-demi": "^0.14.10",
Expand Down
2 changes: 1 addition & 1 deletion ui/src/assets/iconfont.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion ui/src/components/global/mk-view-layout/layout-aside.vue
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ function toggleAside(event: MouseEvent) {
:class="asideCollapsed ? 'left-0 rounded-r-full border-l-0 w-5 flex items-center pl-[2px]' : 'right-0 translate-x-1/2 rounded-full w-6 flex-center group-hover-visible'"
@click="toggleAside"
>
<MkIcon :name="asideCollapsed ? 'icon_right_outlined' : 'icon_left_outlined'" :size="12" />
<MkIcon name="icon_left_outlined" :class="asideCollapsed && 'rotate-180'" :size="12" />
</button>
</el-tooltip>
</div>
Expand Down
2 changes: 1 addition & 1 deletion ui/src/views/system/chat/user-groups/GroupsListView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ onMounted(() => loadChatUserGroups())
<MkSearchList :data="chatUserGroups" :default-active="currentGroup?.id" @click="handleGroupSelect">
<template #action-dropdown="{ row }">
<MkDropdownItem @click="handleOpenGroupDialog(row)">
<template #icon><MkIcon name="icon_edit_outlined" /></template>
<template #icon><MkIcon name="icon_rename_outlined" /></template>
<span>重命名</span>
</MkDropdownItem>
<MkDropdownItem divided @click="deleteGroup(row)">
Expand Down
2 changes: 1 addition & 1 deletion ui/src/views/system/identity/groups/GroupsListView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@ onMounted(() => {
<template #action-dropdown="{ row }">
<MkDropdownItem @click="handleOpenGroupDialog(row)">
<template #icon>
<MkIcon name="icon_edit_outlined" />
<MkIcon name="icon_rename_outlined" />
</template>
<span>重命名</span>
</MkDropdownItem>
Expand Down
2 changes: 1 addition & 1 deletion ui/src/views/system/identity/roles/RoleListView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ onMounted(() => loadRoles())
</template>
<template v-if="!role.internal" #action-dropdown>
<MkDropdownItem @click="handleOpenRoleDialog(role)">
<template #icon><MkIcon name="icon_edit_outlined" /></template>重命名
<template #icon><MkIcon name="icon_rename_outlined" /></template>重命名
</MkDropdownItem>
<MkDropdownItem divided @click="handleDeleteRole(role)">
<template #icon><MkIcon name="icon_delete-trash_outlined" /></template>删除
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,7 @@ onMounted(() => loadWorkspaceOptions())
<template #action-dropdown="{ row: workspace }">
<MkDropdownItem @click="handleOpenWorkspaceDialog(workspace)">
<template #icon>
<MkIcon name="icon_edit_outlined" />
<MkIcon name="icon_rename_outlined" />
</template>
<span>重命名</span>
</MkDropdownItem>
Expand Down
28 changes: 10 additions & 18 deletions ui/src/views/workflow/ApplicationWorkflowView.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { computed, nextTick, onBeforeUnmount, onMounted, provide, ref, useTemplateRef } from 'vue'
import { nextTick, onMounted, ref, useTemplateRef } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import type LogicFlow from '@logicflow/core'
import type { Action } from 'element-plus'
Expand All @@ -8,10 +8,10 @@ import { cloneDeep } from 'lodash'
import ApplicationApi from '@/api/admin/workspace/application/application.ts'
import type { ApplicationDetail } from '@/api/types'
import { datetimeFormat } from '@/utils/time'
import { MsgConfirm, MsgError, MsgSuccess } from '@/utils/message'
import { MsgConfirm, MsgSuccess } from '@/utils/message'
import WorkflowCanvas from '@/workflow-canvas/index.vue'
import { defaultNodes, nodeDict } from '@/workflow-canvas/config/node-mapping'
import { WorkflowMode, WorkflowNodeType, type ShapeItem } from '@/workflow-canvas/types'
import { defaultNodes } from '@/workflow-canvas/config/node-mapping'
import type { ShapeItem } from '@/workflow-canvas/types'
import WorkflowComponentMenu from './components/WorkflowComponentMenu.vue'
import Conversation from '@/components/conversation/index.vue'

Expand All @@ -30,20 +30,12 @@ const workspaceId = route.params.workspaceId as string
const workflowRef = useTemplateRef<InstanceType<typeof WorkflowCanvas>>('workflowRef')

/* 添加工作流组件 */
function toShapeItem(nodeType: WorkflowNodeType): ShapeItem | undefined {
const node = nodeDict[nodeType]
if (!node) return

return {
type: node.type,
properties: node.properties,
text: node.text,
}
function handleAddNode(shapeItem: ShapeItem) {
workflowRef.value?.addNode(shapeItem)
}

function handleAddNode(nodeType: WorkflowNodeType) {
const shapeItem = toShapeItem(nodeType)
if (shapeItem) workflowRef.value?.addNode(shapeItem)
function handleDragNode(shapeItem: ShapeItem, event: PointerEvent) {
workflowRef.value?.onmousedown(shapeItem, event)
}

/* 智能体工作流加载与保存 */
Expand Down Expand Up @@ -267,7 +259,7 @@ onMounted(() => {
</div>

<div class="flex shrink-0 items-center gap-3">
<WorkflowComponentMenu @select="handleAddNode" />
<WorkflowComponentMenu @dragstart="handleDragNode" @select="handleAddNode" />
<el-button plain :loading="saving && !publishing" :disabled="loading || saving || publishing" @click="handleSave"> 保存 </el-button>
<el-button type="primary" plain :disabled="loading || saving" @click="handleDebug"> 调试 </el-button>
<!-- <el-button
Expand Down Expand Up @@ -315,7 +307,7 @@ onMounted(() => {
<MkIcon :icon="Close" :size="16" />
</button>
</div>
<Conversation :defaultOpen='false' type="DEBUG" class="h-full" />
<Conversation :defaultOpen="false" type="DEBUG" class="h-full" />
</div>
</transition>
</main>
Expand Down
33 changes: 27 additions & 6 deletions ui/src/views/workflow/components/WorkflowComponentMenu.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,26 +3,47 @@ import { ref } from 'vue'

import { ClickOutside as vClickOutside } from 'element-plus'
import NodeMenu from '@/workflow-canvas/component/NodeMenu.vue'
import { WorkflowNodeType } from '@/workflow-canvas/types'
import type { WorkflowMenuNode } from '@/workflow-canvas/config/menu'

defineOptions({ name: 'WorkflowComponentMenu' })

const emit = defineEmits<{ select: [nodeType: WorkflowNodeType] }>()
const emit = defineEmits<{
dragstart: [workflowNode: WorkflowMenuNode, event: PointerEvent]
select: [workflowNode: WorkflowMenuNode]
}>()

const popoverVisible = ref(false)
const dragClosing = ref(false)

const handleSelect = (nodeType: WorkflowNodeType) => {
emit('select', nodeType)
const togglePopover = () => {
dragClosing.value = false
popoverVisible.value = !popoverVisible.value
}

const handleSelect = (workflowNode: WorkflowMenuNode) => {
emit('select', workflowNode)
popoverVisible.value = false
}

const handleDragStart = (workflowNode: WorkflowMenuNode, event: PointerEvent) => {
emit('dragstart', workflowNode, event)
dragClosing.value = true
popoverVisible.value = false
}
</script>

<template>
<div v-click-outside="() => (popoverVisible = false)" class="relative">
<el-button plain @click="popoverVisible = !popoverVisible"> 添加组件 </el-button>
<el-button plain @click="togglePopover"> 添加组件 </el-button>

<el-collapse-transition>
<NodeMenu v-show="popoverVisible" class="absolute right-0 top-[calc(100%+8px)] z-[2000]" @select="handleSelect" />
<NodeMenu
v-show="popoverVisible"
class="absolute right-0 top-[calc(100%+8px)] z-[2000]"
:class="{ 'pointer-events-none': dragClosing }"
@dragstart="handleDragStart"
@select="handleSelect"
/>
</el-collapse-transition>
</div>
</template>
6 changes: 3 additions & 3 deletions ui/src/workflow-canvas/WORKFLOW_README.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,9 +107,9 @@ src/workflow-canvas/

## 当前迁移范围

- 已实现并注册:基本信息、开始、AI 对话、指定回复。
- `config/node-mapping.ts` 保留基本信息和开始节点的默认数据集合
- 页面“添加组件”当前只开放 AI 对话和指定回复
- 已实现并注册:基本信息、开始、AI 对话、意图识别、文本转语音、判断器、指定回复。
- `config/node-mapping.ts` 保留节点类型映射,以及基本信息和开始节点的默认数据集合
- `NodeMenu` 根据当前工作流模式直接渲染 `config/menu.ts` 返回的菜单分组;页面“添加组件”和节点锚点菜单均支持点击创建与拖拽到画布创建
- `ApplicationWorkflowView` 已接入详情加载、默认工作流、手动与自动保存、发布以及未保存退出确认。
- 调试、发布历史、模板中心、完整国际化、枚举补充和其他尚未迁入的节点组件后续再接入。

Expand Down
16 changes: 9 additions & 7 deletions ui/src/workflow-canvas/component/NodeConditionDropdown.vue
Original file line number Diff line number Diff line change
Expand Up @@ -46,19 +46,21 @@ const condition = computed({
</script>

<template>
<MkDropdown v-if="visible" :teleported="false" trigger="click" placement="bottom-start" @visible-change="emit('visible-change', $event)">
<el-button text>条件</el-button>
<MkDropdown v-if="visible" :teleported="false" trigger="click" placement="bottom" @visible-change="emit('visible-change', $event)">
<el-button text>
<MkIcon :name="condition === 'OR' ? 'icon_any_outlined' : 'icon_all_outlined'" />
</el-button>
<template #dropdown>
<div class="w-[280px] px-4 py-3">
<h5>执行条件</h5>
<p class="mt-2 text-N600">
<div class="w-84 px-4 py-3">
<h6>执行条件</h6>
<div class="mt-2 flex items-center gap-2">
<span>前置</span>
<el-select v-model="condition" class="mx-2 w-[60px]" size="small">
<el-select v-model="condition" size="small" class="w-15!">
<el-option label="所有" value="AND" />
<el-option label="任一" value="OR" />
</el-select>
<span>连线节点执行完,执行当前节点</span>
</p>
</div>
</div>
</template>
</MkDropdown>
Expand Down
60 changes: 22 additions & 38 deletions ui/src/workflow-canvas/component/NodeMenu.vue
Original file line number Diff line number Diff line change
@@ -1,60 +1,45 @@
<script setup lang="ts">
import { computed, ref } from 'vue'
import { computed, inject, ref } from 'vue'

import aiChatIcon from '@/assets/workflow/icon_ai_chat.svg'
import replyIcon from '@/assets/workflow/icon_reply.svg'
import { WorkflowNodeType } from '@/workflow-canvas/types'
import { getMenuNodes, type WorkflowMenuNode } from '@/workflow-canvas/config/menu'
import { iconComponent } from '@/workflow-canvas/icons/utils'
import { WorkflowMode } from '@/workflow-canvas/types'

defineOptions({ name: 'NodeMenu' })

const emit = defineEmits<{
select: [nodeType: WorkflowNodeType]
dragstart: [workflowNode: WorkflowMenuNode, event: PointerEvent]
select: [workflowNode: WorkflowMenuNode]
}>()

type WorkflowComponentTab = 'basic' | 'tool' | 'application'

const activeTab = ref<WorkflowComponentTab>('basic')
const searchKeyword = ref('')
const workflowMode = inject('workflowMode', WorkflowMode.Application)

const workflowComponentTabs: Array<{ label: string; value: WorkflowComponentTab }> = [
{ label: '基础组件', value: 'basic' },
{ label: '工具', value: 'tool' },
{ label: '智能体', value: 'application' },
]

const workflowComponentGroups = [
{
label: 'AI 能力',
list: [
{ icon: aiChatIcon, iconClass: 'bg-primary-gradient', label: 'AI 对话', value: WorkflowNodeType.AiChat },
{ icon: aiChatIcon, iconClass: 'bg-primary-gradient', label: '意图识别', value: WorkflowNodeType.IntentNode },
{ icon: aiChatIcon, iconClass: 'bg-primary-gradient', label: '文本转语音', value: WorkflowNodeType.TextToSpeechNode },
{ icon: aiChatIcon, iconClass: 'bg-primary-gradient', label: '语音转文本', value: WorkflowNodeType.SpeechToTextNode },
{ icon: aiChatIcon, iconClass: 'bg-primary-gradient', label: '图片生成', value: WorkflowNodeType.ImageGenerateNode},
{ icon: aiChatIcon, iconClass: 'bg-primary-gradient', label: '图片理解', value: WorkflowNodeType.ImageUnderstandNode},
{ icon: aiChatIcon, iconClass: 'bg-primary-gradient', label: '问题优化', value: WorkflowNodeType.Question},
],
},
{
label: '业务逻辑',
list: [
{ icon: aiChatIcon, iconClass: 'bg-purple', label: '判断器', value: WorkflowNodeType.Condition },
{ icon: aiChatIcon, iconClass: 'bg-success', label: '表单收集', value: WorkflowNodeType.FormNode },
{ icon: replyIcon, iconClass: 'bg-warning', label: '指定回复', value: WorkflowNodeType.Reply },
],
},
]
const workflowComponentGroups = computed(() => getMenuNodes(workflowMode) ?? [])

const filteredComponentGroups = computed(() => {
if (activeTab.value !== 'basic') return []

const keyword = searchKeyword.value.trim().toLocaleLowerCase()
if (!keyword) return workflowComponentGroups
if (!keyword) return workflowComponentGroups.value

return workflowComponentGroups
.map((group) => ({ ...group, list: group.list.filter((option) => option.label.toLocaleLowerCase().includes(keyword)) }))
return workflowComponentGroups.value
.map((group) => ({ ...group, list: group.list.filter((node) => node.label.toLocaleLowerCase().includes(keyword)) }))
.filter((group) => group.list.length > 0)
})

const handleNodeDragStart = (event: PointerEvent, workflowNode: WorkflowMenuNode) => {
if (event.button === 0) emit('dragstart', workflowNode, event)
}
</script>

<template>
Expand Down Expand Up @@ -83,16 +68,15 @@ const filteredComponentGroups = computed(() => {
<p class="mb-3 mt-3 text-N600">{{ group.label }}</p>
<div class="grid grid-cols-2 gap-3">
<button
v-for="componentOption in group.list"
:key="componentOption.value"
v-for="workflowNode in group.list"
:key="workflowNode.type"
type="button"
class="flex h-10 items-center gap-3 rounded-md border border-N300 px-3 text-left text-N900 hover:border-primary hover:text-primary"
@click="emit('select', componentOption.value)"
class="flex h-10 cursor-grab items-center gap-3 rounded-md border border-N300 px-3 text-left text-N900 hover:border-primary hover:text-primary active:cursor-grabbing"
@click="emit('select', workflowNode)"
@pointerdown="handleNodeDragStart($event, workflowNode)"
>
<span class="flex size-6 shrink-0 items-center justify-center rounded-md" :class="componentOption.iconClass">
<img :src="componentOption.icon" alt="" class="size-4" />
</span>
<span>{{ componentOption.label }}</span>
<component :is="iconComponent(`${workflowNode.type}-icon`)" class="shrink-0" :size="24" />
<span>{{ workflowNode.label }}</span>
</button>
</div>
</template>
Expand Down
22 changes: 12 additions & 10 deletions ui/src/workflow-canvas/component/NodeOperateDropdown.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
<script setup lang="ts">
import type { BaseNodeModel } from '@logicflow/core'
import { CircleCheck } from '@element-plus/icons-vue'
import type { FormInstance } from 'element-plus'
import { computed, ref, useTemplateRef } from 'vue'
import { set } from 'lodash'
Expand Down Expand Up @@ -119,28 +118,31 @@ function deleteNode() {
<template>
<MkDropdown v-if="visible" :teleported="false" trigger="click" @visible-change="emit('visible-change', $event)">
<el-button text>
<MkIcon name="icon_more_outlined" class="text-N600" />
<MkIcon name="icon_more_outlined" />
</el-button>
<template #dropdown>
<MkDropdownMenu class="min-w-36">
<MkDropdownItem class="p-2" @click="renameNode">
<template #icon><MkIcon name="icon_edit_outlined" /></template>
<MkDropdownItem @click="renameNode">
<template #icon><MkIcon name="icon_rename_outlined" /></template>
重命名
</MkDropdownItem>
<MkDropdownItem class="p-2" @click="copyNode">
<MkDropdownItem @click="copyNode">
<template #icon><MkIcon name="icon_copy_outlined" /></template>
复制
</MkDropdownItem>
<div v-if="canChangeEnabled" class="flex-between p-2" @click.stop>
<MkIcon :icon="CircleCheck" class="text-N600" />
<span class="mr-4">启用状态</span>
<div v-if="canChangeEnabled" class="flex-between px-3 py-1" @click.stop>
<span class="flex items-center gap-2">
<MkIcon name="icon_yes_outlined" class="text-N600!" />
<span>启用状态</span>
</span>

<el-switch v-model="nodeEnabled" size="small" />
</div>
<MkDropdownItem class="border-t border-N300 p-2" @click="deleteNode">
<MkDropdownItem divided @click="deleteNode">
<template #icon><MkIcon name="icon_delete-trash_outlined" /></template>
删除
</MkDropdownItem>
<div v-if="sourceName" class="border-t border-N300 p-2" @click.stop>
<div v-if="sourceName" class="border-t mt-1 px-3 py-2" @click.stop>
<div class="text-sm text-N600">来源</div>
<div class="mt-1 break-all text-N600">{{ sourceName }}</div>
</div>
Expand Down
Loading
Loading