diff --git a/ui/src/api/user/resource-authorization.ts b/ui/src/api/user/resource-authorization.ts index 3d0d3d2ac..2a1daea04 100644 --- a/ui/src/api/user/resource-authorization.ts +++ b/ui/src/api/user/resource-authorization.ts @@ -4,6 +4,15 @@ import type { pageRequest } from '@/api/type/common' import type { Ref } from 'vue' const prefix = '/workspace' + +/** + * 获取成员列表 + * @query 参数 + */ +const getUserList: (workspace_id: String) => Promise> = (workspace_id) => { + return get(`${prefix}/${workspace_id}/user_list`) +} + /** * 获取资源权限 * @query 参数 @@ -40,4 +49,5 @@ const putResourceAuthorization: (workspace_id: String, body: any) => Promise该功能需要使用麦克风,浏览器禁止不安全页面录音,解决方案如下:
+1、可开启 https 解决;
+2、若无 https 配置则需要修改浏览器安全配置,Chrome 设置如下:
+(1) 地址栏输入chrome://flags/#unsafely-treat-insecure-origin-as-secure;
+(2) 将 http 站点配置在文本框中,例如: http://127.0.0.1:8080。

`, + recorderError: '录音失败', + confirm: '我知道了', + requiredMessage: '请填写所有必填字段', + inputParamMessage1: '请在URL中填写参数', + inputParamMessage2: '的值', + prologueMessage: '抱歉,当前正在维护,无法提供服务,请稍后再试!' + }, + inputPlaceholder: { + speaking: '说话中', + recorderLoading: '转文字中', + default: '请输入问题' + }, + uploadFile: { + label: '上传文件', + most: '最多', + limit: '个,每个文件限制', + fileType: '文件类型', + tipMessage: '请在文件上传配置中选择文件类型', + limitMessage1: '最多上传', + limitMessage2: '个文件', + sizeLimit: '单个文件大小不能超过', + imageMessage: '请解析图片内容', + errorMessage: '上传失败' + }, + executionDetails: { + title: '执行详情', + paramOutputTooltip: '每个文档仅支持预览500字', + audioFile: '语音文件', + searchContent: '检索内容', + searchResult: '检索结果', + conditionResult: '判断结果', + currentChat: '本次对话', + answer: 'AI 回答', + replyContent: '回复内容', + textContent: '文本内容', + input: '输入', + output: '输出', + rerankerContent: '重排内容', + rerankerResult: '重排结果', + paragraph: '分段', + noSubmit: '用户未提交', + errMessage: '错误日志' + }, + KnowledgeSource: { + title: '知识来源', + referenceParagraph: '引用分段', + consume: '消耗tokens', + consumeTime: '耗时' + }, + paragraphSource: { + title: '知识库引用', + question: '用户问题', + optimizationQuestion: '优化后问题' + }, + editTitle: '编辑标题' +} diff --git a/ui/src/locales/lang/zh-CN/components.ts b/ui/src/locales/lang/zh-CN/components.ts new file mode 100644 index 000000000..5b871daf1 --- /dev/null +++ b/ui/src/locales/lang/zh-CN/components.ts @@ -0,0 +1,12 @@ +export default { + quickCreatePlaceholder: '快速创建空白文档', + quickCreateName: '文档名称', + noData: '无匹配数据', + loading: '加载中', + noMore: '到底啦!', + selectParagraph: { + title: '选择分段', + error: '仅执行未成功分段', + all: '全部分段' + } +} diff --git a/ui/src/locales/lang/zh-CN/index.ts b/ui/src/locales/lang/zh-CN/index.ts index 919882e57..73d9f06ba 100644 --- a/ui/src/locales/lang/zh-CN/index.ts +++ b/ui/src/locales/lang/zh-CN/index.ts @@ -1,11 +1,11 @@ import zhCn from 'element-plus/es/locale/lang/zh-cn' -// import components from './components' +import components from './components' import views from './views' import theme from './theme' import layout from './layout' import dynamicsForm from './dynamics-form' import common from './common' -// import chat from './ai-chat' +import chat from './ai-chat' export default { lang: '简体中文', zhCn, @@ -14,4 +14,6 @@ export default { layout, dynamicsForm, common, + chat, + components, } diff --git a/ui/src/locales/lang/zh-CN/views/application-overview.ts b/ui/src/locales/lang/zh-CN/views/application-overview.ts new file mode 100644 index 000000000..1f15d2c08 --- /dev/null +++ b/ui/src/locales/lang/zh-CN/views/application-overview.ts @@ -0,0 +1,113 @@ +export default { + title: '概览', + appInfo: { + header: '应用信息', + publicAccessLink: '公开访问链接', + openText: '开', + closeText: '关', + copyLinkText: '复制链接', + refreshLinkText: '刷新链接', + demo: '演示', + embedInWebsite: '嵌入第三方', + accessControl: '访问限制', + displaySetting: '显示设置', + apiAccessCredentials: 'API 访问凭据', + apiKey: 'API Key', + refreshToken: { + msgConfirm1: '是否重新生成公开访问链接?', + msgConfirm2: + '重新生成公开访问链接会影响嵌入第三方脚本变更,需要将新脚本重新嵌入第三方,请谨慎操作!', + refreshSuccess: '刷新成功' + }, + + APIKeyDialog: { + saveSettings: '保存设置', + msgConfirm1: '是否删除API Key', + msgConfirm2: '删除API Key后将无法恢复,请确认是否删除?', + enabledSuccess: '已启用', + disabledSuccess: '已禁用' + }, + EditAvatarDialog: { + title: '应用头像', + customizeUpload: '自定义上传', + upload: '上传', + default: '默认logo', + custom: '自定义', + sizeTip: '建议尺寸 32*32,支持 JPG、PNG、GIF,大小不超过 10 MB', + fileSizeExceeded: '文件大小超过 10 MB', + uploadImagePrompt: '请上传一张图片' + }, + EmbedDialog: { + fullscreenModeTitle: '全屏模式', + copyInstructions: '复制以下代码进行嵌入', + floatingModeTitle: '浮窗模式', + mobileModeTitle: '移动端模式' + }, + LimitDialog: { + showSourceLabel: '显示知识来源', + clientQueryLimitLabel: '每个客户端提问限制', + timesDays: '次/天', + authentication: '身份验证', + authenticationValue: '验证密码', + whitelistLabel: '白名单', + whitelistPlaceholder: + '请输入允许嵌入第三方的源地址,一行一个,如:\nhttp://127.0.0.1:5678\nhttps://dataease.io' + }, + SettingAPIKeyDialog: { + dialogTitle: '设置', + allowCrossDomainLabel: '允许跨域地址', + crossDomainPlaceholder: + '请输入允许的跨域地址,开启后不输入跨域地址则不限制。\n跨域地址一行一个,如:\nhttp://127.0.0.1:5678 \nhttps://dataease.io' + }, + SettingDisplayDialog: { + dialogTitle: '显示设置', + languageLabel: '语言', + showSourceLabel: '显示知识来源', + showExecutionDetail: '显示执行详情', + restoreDefault: '恢复默认', + customThemeColor: '自定义主题色', + headerTitleFontColor: '头部标题字体颜色', + default: '默认', + askUserAvatar: '提问用户头像', + replace: '替换', + imageMessage: '建议尺寸 32*32,支持 JPG、PNG、GIF,大小不超过 10 MB', + AIAvatar: 'AI 回复头像', + display: '显示', + floatIcon: '浮窗入口图标', + iconDefaultPosition: '图标默认位置', + iconPosition: { + left: '左', + right: '右', + bottom: '下', + top: '上' + }, + draggablePosition: '可拖拽位置', + showHistory: '显示历史记录', + displayGuide: '显示引导图(浮窗模式)', + disclaimer: '免责声明', + disclaimerValue: '「以上内容均由 AI 生成,仅供参考和借鉴」' + } + }, + monitor: { + monitoringStatistics: '监控统计', + customRange: '自定义范围', + startDatePlaceholder: '开始时间', + endDatePlaceholder: '结束时间', + pastDayOptions: { + past7Days: '过去7天', + past30Days: '过去30天', + past90Days: '过去90天', + past183Days: '过去半年', + other: '自定义' + }, + charts: { + customerTotal: '用户总数', + customerNew: '用户新增数', + queryCount: '提问次数', + tokensTotal: 'Tokens 总数', + userSatisfaction: '用户满意度', + approval: '赞同', + disapproval: '反对' + } + } +} diff --git a/ui/src/locales/lang/zh-CN/views/application-workflow.ts b/ui/src/locales/lang/zh-CN/views/application-workflow.ts new file mode 100644 index 000000000..1495316b8 --- /dev/null +++ b/ui/src/locales/lang/zh-CN/views/application-workflow.ts @@ -0,0 +1,302 @@ +export default { + node: '节点', + nodeName: '节点名称', + baseComponent: '基础组件', + nodeSetting: '节点设置', + workflow: '工作流', + searchBar: { + placeholder: '按名称搜索' + }, + info: { + previewVersion: '预览版本:', + saveTime: '保存时间:' + }, + setting: { + restoreVersion: '恢复版本', + restoreCurrentVersion: '恢复此版本', + addComponent: '添加组件', + public: '发布', + releaseHistory: '发布历史', + autoSave: '自动保存', + latestRelease: '最近发布', + copyParam: '复制参数', + debug: '调试', + exit: '直接退出', + exitSave: '保存并退出' + }, + tip: { + publicSuccess: '发布成功', + noData: '没有找到相关结果', + nameMessage: '名字不能为空!', + onlyRight: '只允许从右边的锚点连出', + notRecyclable: '不可循环连线', + onlyLeft: '只允许连接左边的锚点', + applicationNodeError: '该应用不可用', + functionNodeError: '该函数不可用', + repeatedNodeError: '节点名称已存在!', + cannotCopy: '不能被复制', + copyError: '已复制节点', + paramErrorMessage: '参数已存在: ', + saveMessage: '当前的更改尚未保存,是否保存后退出?' + }, + delete: { + confirmTitle: '确定删除该节点?', + deleteMessage: '节点不允许删除' + }, + control: { + zoomOut: '缩小', + zoomIn: '放大', + fitView: '适应', + retract: '收起全部节点', + extend: '展开全部节点', + beautify: '一键美化' + }, + variable: { + label: '变量', + global: '全局变量', + Referencing: '引用变量', + ReferencingRequired: '引用变量必填', + ReferencingError: '引用变量错误', + NoReferencing: '不存在的引用变量', + placeholder: '请选择变量' + }, + condition: { + title: '执行条件', + front: '前置', + AND: '所有', + OR: '任一', + text: '连线节点执行完,执行当前节点' + }, + validate: { + startNodeRequired: '开始节点必填', + startNodeOnly: '开始节点只能有一个', + baseNodeRequired: '基本信息节点必填', + baseNodeOnly: '基本信息节点只能有一个', + notInWorkFlowNode: '未在流程中的节点', + noNextNode: '不存在的下一个节点', + nodeUnavailable: '节点不可用', + needConnect1: '节点的', + needConnect2: '分支需要连接', + cannotEndNode: '节点不能当做结束节点' + }, + nodes: { + startNode: { + label: '开始', + question: '用户问题', + currentTime: '当前时间' + }, + baseNode: { + label: '基本信息', + appName: { + label: '应用名称' + }, + appDescription: { + label: '应用描述' + }, + fileUpload: { + label: '文件上传', + tooltip: '开启后,问答页面会显示上传文件的按钮。' + }, + FileUploadSetting: { + title: '文件上传设置', + maxFiles: '单次上传最多文件数', + fileLimit: '每个文件最大(MB)', + fileUploadType: { + label: '上传的文件类型', + documentText: '需要使用“文档内容提取”节点解析文档内容', + imageText: '需要使用“视觉模型”节点解析图片内容', + audioText: '需要使用“语音转文本”节点解析音频内容', + otherText: '需要自行解析该类型文件' + }, + + } + }, + aiChatNode: { + label: 'AI 对话', + text: '与 AI 大模型进行对话', + answer: 'AI 回答内容', + returnContent: { + label: '返回内容', + tooltip: `关闭后该节点的内容则不输出给用户。 + 如果你想让用户看到该节点的输出内容,请打开开关。` + }, + defaultPrompt: '已知信息', + think: '思考过程' + }, + searchDatasetNode: { + label: '知识库检索', + text: '关联知识库,查找与问题相关的分段', + paragraph_list: '检索结果的分段列表', + is_hit_handling_method_list: '满足直接回答的分段列表', + result: '检索结果', + directly_return: '满足直接回答的分段内容', + searchParam: '检索参数', + searchQuestion: { + label: '检索问题', + placeholder: '请选择检索问题', + requiredMessage: '请选择检索问题' + } + }, + questionNode: { + label: '问题优化', + text: '根据历史聊天记录优化完善当前问题,更利于匹配知识库分段', + result: '问题优化结果', + defaultPrompt1: `根据上下文优化和完善用户问题`, + defaultPrompt2: `请输出一个优化后的问题。`, + systemDefault: '你是一个问题优化大师' + }, + conditionNode: { + label: '判断器', + text: '根据不同条件执行不同的节点', + branch_name: '分支名称', + conditions: { + label: '条件', + info: '符合以下', + requiredMessage: '请选择条件' + }, + valueMessage: '请输入值', + addCondition: '添加条件', + addBranch: '添加分支' + }, + replyNode: { + label: '指定回复', + text: '指定回复内容,引用变量会转换为字符串进行输出', + content: '内容', + replyContent: { + label: '回复内容', + custom: '自定义', + reference: '引用变量' + } + }, + rerankerNode: { + label: '多路召回', + text: '使用重排模型对多个知识库的检索结果进行二次召回', + result_list: '重排结果列表', + result: '重排结果', + rerankerContent: { + label: '重排内容', + requiredMessage: '请选择重排内容' + }, + higher: '高于', + ScoreTooltip: 'Score越高相关性越强。', + max_paragraph_char_number: '最大引用字符数', + reranker_model: { + label: '重排模型', + placeholder: '请选择重排模型' + } + }, + formNode: { + label: '表单收集', + text: '在问答过程中用于收集用户信息,可以根据收集到表单数据执行后续流程', + form_content_format1: '你好,请先填写下面表单内容:', + form_content_format2: '填写后请点击【提交】按钮进行提交。', + form_data: '表单全部内容', + formContent: { + label: '表单输出内容', + requiredMessage: '请表单输出内容', + tooltip: '设置执行该节点输出的内容,{ form } 为表单的占位符。' + }, + formAllContent: '表单全部内容', + formSetting: '表单配置' + }, + documentExtractNode: { + label: '文档内容提取', + text: '提取文档中的内容', + content: '文档内容' + }, + imageUnderstandNode: { + label: '图片理解', + text: '识别出图片中的对象、场景等信息回答用户问题', + answer: 'AI 回答内容', + model: { + label: '视觉模型', + requiredMessage: '请选择视觉模型' + }, + image: { + label: '选择图片', + requiredMessage: '请选择图片' + } + }, + variableAssignNode: { + label: '变量赋值', + text: '更新全局变量的值', + assign: '赋值' + }, + mcpNode: { + label: 'MCP 调用', + text: '通过SSE/Streamable HTTP方式执行MCP服务中的工具', + getToolsSuccess: '获取工具成功', + getTool: '获取工具', + tool: '工具', + toolParam: '工具参数', + mcpServerTip: '请输入JSON格式的MCP服务器配置', + mcpToolTip: '请选择工具', + configLabel: 'MCP Server Config (仅支持SSE/Streamable HTTP调用方式)' + }, + imageGenerateNode: { + label: '图片生成', + text: '根据提供的文本内容生成图片', + answer: 'AI 回答内容', + model: { + label: '图片生成模型', + requiredMessage: '请选择图片生成模型' + }, + prompt: { + label: '提示词(正向)', + tooltip: '正向提示词,用来描述生成图像中期望包含的元素和视觉特点' + }, + negative_prompt: { + label: '提示词(负向)', + tooltip: '反向提示词,用来描述不希望在画面中看到的内容,可以对画面进行限制。', + placeholder: '请描述不想生成的图片内容,比如:颜色、血腥内容' + } + }, + speechToTextNode: { + label: '语音转文本', + text: '将音频通过语音识别模型转换为文本', + stt_model: { + label: '语音识别模型' + }, + audio: { + label: '选择语音文件', + placeholder: '请选择语音文件' + } + }, + textToSpeechNode: { + label: '文本转语音', + text: '将文本通过语音合成模型转换为音频', + tts_model: { + label: '语音识别模型' + }, + content: { + label: '选择文本内容' + } + }, + functionNode: { + label: '自定义函数', + text: '通过执行自定义脚本,实现数据处理' + }, + applicationNode: { + label: '应用节点' + } + }, + compare: { + is_null: '为空', + is_not_null: '不为空', + contain: '包含', + not_contain: '不包含', + eq: '等于', + ge: '大于等于', + gt: '大于', + le: '小于等于', + lt: '小于', + len_eq: '长度等于', + len_ge: '长度大于等于', + len_gt: '长度大于', + len_le: '长度小于等于', + len_lt: '长度小于', + is_true: '为真', + is_not_true: '不为真' + }, + FileUploadSetting: {} +} diff --git a/ui/src/locales/lang/zh-CN/views/index.ts b/ui/src/locales/lang/zh-CN/views/index.ts index 53e7964f7..ac04f79ff 100644 --- a/ui/src/locales/lang/zh-CN/views/index.ts +++ b/ui/src/locales/lang/zh-CN/views/index.ts @@ -8,17 +8,13 @@ import userManage from './user-manage' import resourceAuthorization from './resource-authorization' import application from './application' import problem from './problem' +import applicationOverview from './application-overview' +import applicationWorkflow from './application-workflow' // import notFound from './404' -// import applicationOverview from './application-overview' - -// import user from './user' -// import team from './team' - // import paragraph from './paragraph' // import log from './log' -// import applicationWorkflow from './application-workflow' // import operateLog from './operate-log' export default { @@ -32,16 +28,12 @@ export default { resourceAuthorization, application, problem, + applicationOverview, + applicationWorkflow, // notFound, - - // applicationOverview, - - // user, - // team, // paragraph, // log, - // applicationWorkflow, // operateLog } diff --git a/ui/src/router/modules/1application.ts b/ui/src/router/modules/1application.ts new file mode 100644 index 000000000..f3527086b --- /dev/null +++ b/ui/src/router/modules/1application.ts @@ -0,0 +1,94 @@ +// import Layout from '@/layout/layout-template/DetailLayout.vue' +import { ComplexPermission } from '@/utils/permission/type' + +const applicationRouter = { + path: '/application', + name: 'application', + meta: { title: 'views.application.title', permission: 'APPLICATION:READ' }, + redirect: '/application', + component: () => import('@/layout/layout-template/SimpleLayout.vue'), + children: [ + { + path: '/application', + name: 'application-index', + meta: { title: '应用主页', activeMenu: '/application' }, + component: () => import('@/views/application/index.vue') + }, + // { + // path: '/application/:id/:type', + // name: 'ApplicationDetail', + // meta: { title: '应用详情', activeMenu: '/application' }, + // component: Layout, + // hidden: true, + // children: [ + // { + // path: 'overview', + // name: 'AppOverview', + // meta: { + // icon: 'app-all-menu', + // iconActive: 'app-all-menu-active', + // title: 'views.applicationOverview.title', + // active: 'overview', + // parentPath: '/application/:id/:type', + // parentName: 'ApplicationDetail' + // }, + // component: () => import('@/views/application-overview/index.vue') + // }, + // { + // path: 'setting', + // name: 'AppSetting', + // meta: { + // icon: 'app-setting', + // iconActive: 'app-setting-active', + // title: 'common.setting', + // active: 'setting', + // parentPath: '/application/:id/:type', + // parentName: 'ApplicationDetail' + // }, + // component: () => import('@/views/application/ApplicationSetting.vue') + // }, + // { + // path: 'access', + // name: 'AppAccess', + // meta: { + // icon: 'app-access', + // iconActive: 'app-access-active', + // title: 'views.application.applicationAccess.title', + // active: 'access', + // parentPath: '/application/:id/:type', + // parentName: 'ApplicationDetail', + // permission: new ComplexPermission([], ['x-pack'], 'OR') + // }, + // component: () => import('@/views/application/ApplicationAccess.vue') + // }, + // { + // path: 'hit-test', + // name: 'AppHitTest', + // meta: { + // icon: 'app-hit-test', + // title: 'views.application.hitTest.title', + // active: 'hit-test', + // parentPath: '/application/:id/:type', + // parentName: 'ApplicationDetail' + // }, + // component: () => import('@/views/hit-test/index.vue') + // }, + // // { + // // path: 'log', + // // name: 'Log', + // // meta: { + // // icon: 'app-document', + // // iconActive: 'app-document-active', + // // title: 'views.log.title', + // // active: 'log', + // // parentPath: '/application/:id/:type', + // // parentName: 'ApplicationDetail' + // // }, + // // component: () => import('@/views/log/index.vue') + // // } + // ] + // } + ] +} + +export default applicationRouter diff --git a/ui/src/router/routes.ts b/ui/src/router/routes.ts index e395f2886..908a31bde 100644 --- a/ui/src/router/routes.ts +++ b/ui/src/router/routes.ts @@ -10,6 +10,20 @@ export const routes: Array = [ children: [...rolesRoutes], }, + // 高级编排 + { + path: '/application/:id/workflow', + name: 'ApplicationWorkflow', + meta: { activeMenu: '/application' }, + component: () => import('@/views/application-workflow/index.vue'), + }, + // 对话 + { + path: '/chat/:accessToken', + name: 'Chat', + component: () => import('@/views/chat/index.vue'), + }, + { path: '/login', name: 'login', diff --git a/ui/src/styles/app.scss b/ui/src/styles/app.scss index 012aaca6f..858365832 100644 --- a/ui/src/styles/app.scss +++ b/ui/src/styles/app.scss @@ -409,3 +409,11 @@ h5 { background: var(--el-color-primary); } } + +/* + 内容部分 自适应高度 +*/ +.main-calc-height { + height: var(--app-main-height); + box-sizing: border-box; +} diff --git a/ui/src/views/application-overview/component/APIKeyDialog.vue b/ui/src/views/application-overview/component/APIKeyDialog.vue new file mode 100644 index 000000000..dc8c9aa37 --- /dev/null +++ b/ui/src/views/application-overview/component/APIKeyDialog.vue @@ -0,0 +1,153 @@ + + + diff --git a/ui/src/views/application-overview/component/DisplaySettingDialog.vue b/ui/src/views/application-overview/component/DisplaySettingDialog.vue new file mode 100644 index 000000000..21878dda9 --- /dev/null +++ b/ui/src/views/application-overview/component/DisplaySettingDialog.vue @@ -0,0 +1,103 @@ + + + diff --git a/ui/src/views/application-overview/component/EditAvatarDialog.vue b/ui/src/views/application-overview/component/EditAvatarDialog.vue new file mode 100644 index 000000000..b9946d687 --- /dev/null +++ b/ui/src/views/application-overview/component/EditAvatarDialog.vue @@ -0,0 +1,137 @@ + + + diff --git a/ui/src/views/application-overview/component/EmbedDialog.vue b/ui/src/views/application-overview/component/EmbedDialog.vue new file mode 100644 index 000000000..379339aba --- /dev/null +++ b/ui/src/views/application-overview/component/EmbedDialog.vue @@ -0,0 +1,163 @@ + + + diff --git a/ui/src/views/application-overview/component/LimitDialog.vue b/ui/src/views/application-overview/component/LimitDialog.vue new file mode 100644 index 000000000..3f7f5ca55 --- /dev/null +++ b/ui/src/views/application-overview/component/LimitDialog.vue @@ -0,0 +1,196 @@ + + + diff --git a/ui/src/views/application-overview/component/SettingAPIKeyDialog.vue b/ui/src/views/application-overview/component/SettingAPIKeyDialog.vue new file mode 100644 index 000000000..a5f3770b3 --- /dev/null +++ b/ui/src/views/application-overview/component/SettingAPIKeyDialog.vue @@ -0,0 +1,113 @@ + + + diff --git a/ui/src/views/application-overview/component/StatisticsCharts.vue b/ui/src/views/application-overview/component/StatisticsCharts.vue new file mode 100644 index 000000000..dbcc2e8d4 --- /dev/null +++ b/ui/src/views/application-overview/component/StatisticsCharts.vue @@ -0,0 +1,163 @@ + + + diff --git a/ui/src/views/application-overview/component/XPackDisplaySettingDialog.vue b/ui/src/views/application-overview/component/XPackDisplaySettingDialog.vue new file mode 100644 index 000000000..076ff7118 --- /dev/null +++ b/ui/src/views/application-overview/component/XPackDisplaySettingDialog.vue @@ -0,0 +1,639 @@ + + + diff --git a/ui/src/views/application-overview/index.vue b/ui/src/views/application-overview/index.vue new file mode 100644 index 000000000..6119a7c66 --- /dev/null +++ b/ui/src/views/application-overview/index.vue @@ -0,0 +1,434 @@ + + + diff --git a/ui/src/views/application-workflow/component/DropdownMenu.vue b/ui/src/views/application-workflow/component/DropdownMenu.vue new file mode 100644 index 000000000..322bdde06 --- /dev/null +++ b/ui/src/views/application-workflow/component/DropdownMenu.vue @@ -0,0 +1,302 @@ + + + diff --git a/ui/src/views/application-workflow/component/PublishHistory.vue b/ui/src/views/application-workflow/component/PublishHistory.vue new file mode 100644 index 000000000..5fcaaef3d --- /dev/null +++ b/ui/src/views/application-workflow/component/PublishHistory.vue @@ -0,0 +1,147 @@ + + + diff --git a/ui/src/views/application-workflow/index.vue b/ui/src/views/application-workflow/index.vue new file mode 100644 index 000000000..4f1c1eb49 --- /dev/null +++ b/ui/src/views/application-workflow/index.vue @@ -0,0 +1,487 @@ + + + diff --git a/ui/src/views/application/ApplicationAccess.vue b/ui/src/views/application/ApplicationAccess.vue new file mode 100644 index 000000000..ce2fe6aab --- /dev/null +++ b/ui/src/views/application/ApplicationAccess.vue @@ -0,0 +1,185 @@ + + + + + diff --git a/ui/src/views/application/ApplicationSetting.vue b/ui/src/views/application/ApplicationSetting.vue new file mode 100644 index 000000000..2bde0856c --- /dev/null +++ b/ui/src/views/application/ApplicationSetting.vue @@ -0,0 +1,816 @@ + + + diff --git a/ui/src/views/application/component/AIModeParamSettingDialog.vue b/ui/src/views/application/component/AIModeParamSettingDialog.vue new file mode 100644 index 000000000..a14519c79 --- /dev/null +++ b/ui/src/views/application/component/AIModeParamSettingDialog.vue @@ -0,0 +1,91 @@ + + + + + diff --git a/ui/src/views/application/component/AccessSettingDrawer.vue b/ui/src/views/application/component/AccessSettingDrawer.vue new file mode 100644 index 000000000..19edbd578 --- /dev/null +++ b/ui/src/views/application/component/AccessSettingDrawer.vue @@ -0,0 +1,400 @@ + + + diff --git a/ui/src/views/application/component/AddDatasetDialog.vue b/ui/src/views/application/component/AddDatasetDialog.vue new file mode 100644 index 000000000..f3c93e0f3 --- /dev/null +++ b/ui/src/views/application/component/AddDatasetDialog.vue @@ -0,0 +1,168 @@ + + + diff --git a/ui/src/views/application/component/CopyApplicationDialog.vue b/ui/src/views/application/component/CopyApplicationDialog.vue new file mode 100644 index 000000000..6fb8fe530 --- /dev/null +++ b/ui/src/views/application/component/CopyApplicationDialog.vue @@ -0,0 +1,178 @@ + + + diff --git a/ui/src/views/application/component/CreateApplicationDialog.vue b/ui/src/views/application/component/CreateApplicationDialog.vue new file mode 100644 index 000000000..7415753c1 --- /dev/null +++ b/ui/src/views/application/component/CreateApplicationDialog.vue @@ -0,0 +1,271 @@ + + + diff --git a/ui/src/views/application/component/McpServersDialog.vue b/ui/src/views/application/component/McpServersDialog.vue new file mode 100644 index 000000000..597cdc95c --- /dev/null +++ b/ui/src/views/application/component/McpServersDialog.vue @@ -0,0 +1,91 @@ + + + diff --git a/ui/src/views/application/component/ParamSettingDialog.vue b/ui/src/views/application/component/ParamSettingDialog.vue new file mode 100644 index 000000000..bd0cb5545 --- /dev/null +++ b/ui/src/views/application/component/ParamSettingDialog.vue @@ -0,0 +1,327 @@ + + + diff --git a/ui/src/views/application/component/ReasoningParamSettingDialog.vue b/ui/src/views/application/component/ReasoningParamSettingDialog.vue new file mode 100644 index 000000000..2ebc87b62 --- /dev/null +++ b/ui/src/views/application/component/ReasoningParamSettingDialog.vue @@ -0,0 +1,87 @@ + + + diff --git a/ui/src/views/application/component/TTSModeParamSettingDialog.vue b/ui/src/views/application/component/TTSModeParamSettingDialog.vue new file mode 100644 index 000000000..8015347d8 --- /dev/null +++ b/ui/src/views/application/component/TTSModeParamSettingDialog.vue @@ -0,0 +1,153 @@ + + + + + diff --git a/ui/src/views/application/index.vue b/ui/src/views/application/index.vue new file mode 100644 index 000000000..a943b6a5d --- /dev/null +++ b/ui/src/views/application/index.vue @@ -0,0 +1,444 @@ + + + diff --git a/ui/src/views/chat/auth/component/password.vue b/ui/src/views/chat/auth/component/password.vue new file mode 100644 index 000000000..05b79fd84 --- /dev/null +++ b/ui/src/views/chat/auth/component/password.vue @@ -0,0 +1,84 @@ + + + diff --git a/ui/src/views/chat/auth/index.vue b/ui/src/views/chat/auth/index.vue new file mode 100644 index 000000000..535ab0d94 --- /dev/null +++ b/ui/src/views/chat/auth/index.vue @@ -0,0 +1,66 @@ + + + diff --git a/ui/src/views/chat/base/index.vue b/ui/src/views/chat/base/index.vue new file mode 100644 index 000000000..bccd00a7d --- /dev/null +++ b/ui/src/views/chat/base/index.vue @@ -0,0 +1,106 @@ + + + diff --git a/ui/src/views/chat/embed/index.vue b/ui/src/views/chat/embed/index.vue new file mode 100644 index 000000000..df9fde29a --- /dev/null +++ b/ui/src/views/chat/embed/index.vue @@ -0,0 +1,404 @@ + + + + diff --git a/ui/src/views/chat/index.vue b/ui/src/views/chat/index.vue new file mode 100644 index 000000000..7f58d1df3 --- /dev/null +++ b/ui/src/views/chat/index.vue @@ -0,0 +1,103 @@ + + + diff --git a/ui/src/views/chat/mobile/index.vue b/ui/src/views/chat/mobile/index.vue new file mode 100644 index 000000000..808570079 --- /dev/null +++ b/ui/src/views/chat/mobile/index.vue @@ -0,0 +1,401 @@ + + + + diff --git a/ui/src/views/chat/pc/EditTitleDialog.vue b/ui/src/views/chat/pc/EditTitleDialog.vue new file mode 100644 index 000000000..e8a8d08e7 --- /dev/null +++ b/ui/src/views/chat/pc/EditTitleDialog.vue @@ -0,0 +1,87 @@ + + + diff --git a/ui/src/views/chat/pc/index.vue b/ui/src/views/chat/pc/index.vue new file mode 100644 index 000000000..b1b20e711 --- /dev/null +++ b/ui/src/views/chat/pc/index.vue @@ -0,0 +1,527 @@ + + + + diff --git a/ui/src/views/knowledge/component/CreateDatasetDialog.vue b/ui/src/views/knowledge/component/CreateDatasetDialog.vue index 2d9eaa067..1a799b837 100644 --- a/ui/src/views/knowledge/component/CreateDatasetDialog.vue +++ b/ui/src/views/knowledge/component/CreateDatasetDialog.vue @@ -54,7 +54,7 @@
- +

@@ -106,7 +106,7 @@ - + diff --git a/ui/src/views/paragraph/component/SelectDocumentDialog.vue b/ui/src/views/paragraph/component/SelectDocumentDialog.vue index 36cf57e0c..11d078ebe 100644 --- a/ui/src/views/paragraph/component/SelectDocumentDialog.vue +++ b/ui/src/views/paragraph/component/SelectDocumentDialog.vue @@ -30,7 +30,7 @@ shape="square" :size="24" > - + - + - + {{ item.name }} diff --git a/ui/src/views/resource-authorization/component/PermissionSetting.vue b/ui/src/views/resource-authorization/component/PermissionSetting.vue index 34886ea6e..6712c9bcf 100644 --- a/ui/src/views/resource-authorization/component/PermissionSetting.vue +++ b/ui/src/views/resource-authorization/component/PermissionSetting.vue @@ -3,22 +3,15 @@ v-model="filterText" :placeholder="$t('common.search')" prefix-icon="Search" - class="p-24 pt-0 pb-0 mb-16 mt-4" + class="mb-16 mt-4" clearable /> -

+
- + @@ -111,92 +104,96 @@ diff --git a/ui/src/workflow/common/AddFormCollect.vue b/ui/src/workflow/common/AddFormCollect.vue new file mode 100644 index 000000000..db15703a6 --- /dev/null +++ b/ui/src/workflow/common/AddFormCollect.vue @@ -0,0 +1,53 @@ + + + diff --git a/ui/src/workflow/common/CustomLine.vue b/ui/src/workflow/common/CustomLine.vue new file mode 100644 index 000000000..48146e5a9 --- /dev/null +++ b/ui/src/workflow/common/CustomLine.vue @@ -0,0 +1,37 @@ + + + + diff --git a/ui/src/workflow/common/EditFormCollect.vue b/ui/src/workflow/common/EditFormCollect.vue new file mode 100644 index 000000000..6ebbaf95e --- /dev/null +++ b/ui/src/workflow/common/EditFormCollect.vue @@ -0,0 +1,58 @@ + + + diff --git a/ui/src/workflow/common/NodeCascader.vue b/ui/src/workflow/common/NodeCascader.vue new file mode 100644 index 000000000..66a039cfe --- /dev/null +++ b/ui/src/workflow/common/NodeCascader.vue @@ -0,0 +1,87 @@ + + + + diff --git a/ui/src/workflow/common/NodeContainer.vue b/ui/src/workflow/common/NodeContainer.vue new file mode 100644 index 000000000..b61fe25b6 --- /dev/null +++ b/ui/src/workflow/common/NodeContainer.vue @@ -0,0 +1,367 @@ + + + diff --git a/ui/src/workflow/common/NodeControl.vue b/ui/src/workflow/common/NodeControl.vue new file mode 100644 index 000000000..8e822b097 --- /dev/null +++ b/ui/src/workflow/common/NodeControl.vue @@ -0,0 +1,110 @@ + + + + diff --git a/ui/src/workflow/common/app-node.ts b/ui/src/workflow/common/app-node.ts new file mode 100644 index 000000000..3665cb262 --- /dev/null +++ b/ui/src/workflow/common/app-node.ts @@ -0,0 +1,407 @@ +import Components from '@/components' +import ElementPlus from 'element-plus' +import * as ElementPlusIcons from '@element-plus/icons-vue' +import zhCn from 'element-plus/dist/locale/zh-cn.mjs' +import { HtmlResize } from '@logicflow/extension' +import { h as lh } from '@logicflow/core' +import { createApp, h } from 'vue' +import directives from '@/directives' +import i18n from '@/locales' +import { WorkflowType } from '@/enums/workflow' +import { nodeDict } from '@/workflow/common/data' +import { isActive, connect, disconnect } from './teleport' +import { t } from '@/locales' +import { type Dict } from '@/api/type/common' +class AppNode extends HtmlResize.view { + isMounted + r?: any + component: any + app: any + root?: any + VueNode: any + up_node_field_dict?: Dict> + constructor(props: any, VueNode: any) { + super(props) + this.component = VueNode + this.isMounted = false + props.model.clear_next_node_field = this.clear_next_node_field.bind(this) + props.model.get_up_node_field_dict = this.get_up_node_field_dict.bind(this) + props.model.get_node_field_list = this.get_node_field_list.bind(this) + props.model.get_up_node_field_list = this.get_up_node_field_list.bind(this) + + if (props.model.properties.noRender) { + delete props.model.properties.noRender + } else { + const filterNodes = props.graphModel.nodes.filter((v: any) => v.type === props.model.type) + const filterNameSameNodes = filterNodes.filter( + (v: any) => v.properties.stepName === props.model.properties.stepName + ) + if (filterNameSameNodes.length - 1 > 0) { + getNodesName(filterNameSameNodes.length - 1) + } + } + function getNodesName(num: number) { + const number = num + const name = props.model.properties.stepName + number + if (!props.graphModel.nodes?.some((node: any) => node.properties.stepName === name.trim())) { + props.model.properties.stepName = name + } else { + getNodesName(number + 1) + } + } + props.model.properties.config = nodeDict[props.model.type].properties.config + if (props.model.properties.height) { + props.model.height = props.model.properties.height + } + } + get_node_field_list() { + const result = [] + if (this.props.model.type === 'start-node') { + result.push({ + value: 'global', + label: t('views.applicationWorkflow.variable.global'), + type: 'global', + children: this.props.model.properties?.config?.globalFields || [] + }) + } + result.push({ + value: this.props.model.id, + label: this.props.model.properties.stepName, + type: this.props.model.type, + children: this.props.model.properties?.config?.fields || [] + }) + return result + } + get_up_node_field_dict(contain_self: boolean, use_cache: boolean) { + if (!this.up_node_field_dict || !use_cache) { + const up_node_list = this.props.graphModel.getNodeIncomingNode(this.props.model.id) + this.up_node_field_dict = up_node_list + .filter((node) => node.id != 'start-node') + .map((node) => node.get_up_node_field_dict(true, use_cache)) + .reduce((pre, next) => ({ ...pre, ...next }), {}) + } + if (contain_self) { + return { + ...this.up_node_field_dict, + [this.props.model.id]: this.get_node_field_list() + } + } + return this.up_node_field_dict ? this.up_node_field_dict : {} + } + + get_up_node_field_list(contain_self: boolean, use_cache: boolean) { + const result = Object.values(this.get_up_node_field_dict(contain_self, use_cache)).reduce( + (pre, next) => [...pre, ...next], + [] + ) + const start_node_field_list = this.props.graphModel + .getNodeModelById('start-node') + .get_node_field_list() + return [...start_node_field_list, ...result] + } + + clear_next_node_field(contain_self: boolean) { + const next_node_list = this.props.graphModel.getNodeOutgoingNode(this.props.model.id) + next_node_list.forEach((node) => { + node.clear_next_node_field(true) + }) + if (contain_self) { + this.up_node_field_dict = undefined + } + } + getAnchorShape(anchorData: any) { + const { x, y, type } = anchorData + let isConnect = false + + if (type == 'left') { + isConnect = this.props.graphModel.edges.some((edge) => edge.targetAnchorId == anchorData.id) + } else { + isConnect = this.props.graphModel.edges.some((edge) => edge.sourceAnchorId == anchorData.id) + } + + return lh( + 'foreignObject', + { + ...anchorData, + x: x - 10, + y: y - 12, + width: 30, + height: 30 + }, + [ + lh('div', { + style: { zindex: 0 }, + onClick: () => { + if (type == 'right') { + this.props.model.openNodeMenu(anchorData) + } + }, + dangerouslySetInnerHTML: { + __html: isConnect + ? ` + + + + + + + + + + + + + + + + + + ` + : ` + + + + + + + + + + + + + + + + + ` + } + }) + ] + ) + } + + setHtml(rootEl: HTMLElement) { + if (!this.isMounted) { + this.isMounted = true + const node = document.createElement('div') + rootEl.appendChild(node) + this.renderVueComponent(node) + } else { + if (this.r && this.r.component) { + this.r.component.props.properties = this.props.model.getProperties() + } + } + } + componentWillUnmount() { + super.componentWillUnmount() + this.unmount() + } + getComponentContainer() { + return this.root + } + protected targetId() { + return `${this.props.graphModel.flowId}:${this.props.model.id}` + } + protected renderVueComponent(root: any) { + this.unmountVueComponent() + this.root = root + const { model, graphModel } = this.props + + if (root) { + if (isActive()) { + connect(this.targetId(), this.component, root, model, graphModel) + } else { + this.r = h(this.component, { + properties: this.props.model.properties, + nodeModel: this.props.model + }) + this.app = createApp({ + render() { + return this.r + }, + provide() { + return { + getNode: () => model, + getGraph: () => graphModel + } + } + }) + + this.app.use(ElementPlus, { + locale: zhCn + }) + this.app.use(Components) + this.app.use(directives) + this.app.use(i18n) + for (const [key, component] of Object.entries(ElementPlusIcons)) { + this.app.component(key, component) + } + this.app?.mount(root) + } + } + } + + protected unmountVueComponent() { + if (this.app) { + this.app.unmount() + this.app = null + } + if (this.root) { + this.root.innerHTML = '' + } + return this.root + } + + unmount() { + if (isActive()) { + disconnect(this.targetId()) + } + this.unmountVueComponent() + } +} + +class AppNodeModel extends HtmlResize.model { + refreshDeges() { + // 更新节点连接边的path + this.incoming.edges.forEach((edge: any) => { + // 调用自定义的更新方案 + edge.updatePathByAnchor() + }) + this.outgoing.edges.forEach((edge: any) => { + edge.updatePathByAnchor() + }) + } + set_position(position: { x?: number; y?: number }) { + const { x, y } = position + if (x) { + this.x = x + } + if (y) { + this.y = y + } + this.refreshDeges() + } + getResizeOutlineStyle() { + const style = super.getResizeOutlineStyle() + style.stroke = 'none' + return style + } + getControlPointStyle() { + const style = super.getControlPointStyle() + style.stroke = 'none' + style.fill = 'none' + return style + } + getNodeStyle() { + return { + overflow: 'visible' + } + } + getOutlineStyle() { + const style = super.getOutlineStyle() + style.stroke = 'none' + if (style.hover) { + style.hover.stroke = 'none' + } + return style + } + // 如果不用修改锚地形状,可以重写颜色相关样式 + getAnchorStyle(anchorInfo: any) { + const style = super.getAnchorStyle(anchorInfo) + if (anchorInfo.type === 'left') { + style.fill = 'red' + style.hover.fill = 'transparent' + style.hover.stroke = 'transpanrent' + style.className = 'lf-hide-default' + } else { + style.fill = 'green' + } + return style + } + + setHeight(height: number) { + const sourceHeight = this.height + const targetHeight = height + 100 + this.height = targetHeight + this.properties['height'] = targetHeight + this.move(0, (targetHeight - sourceHeight) / 2) + this.outgoing.edges.forEach((edge: any) => { + // 调用自定义的更新方案 + edge.updatePathByAnchor() + }) + this.incoming.edges.forEach((edge: any) => { + // 调用自定义的更新方案 + edge.updatePathByAnchor() + }) + } + get_width() { + return this.properties?.width || 340 + } + + setAttributes() { + const { t } = i18n.global + this.width = this.get_width() + const isLoop = (node_id: string, target_node_id: string) => { + const up_node_list = this.graphModel.getNodeIncomingNode(node_id) + for (const index in up_node_list) { + const item = up_node_list[index] + if (item.id === target_node_id) { + return true + } else { + const result = isLoop(item.id, target_node_id) + if (result) { + return true + } + } + } + return false + } + const circleOnlyAsTarget = { + message: t('views.applicationWorkflow.tip.onlyRight'), + validate: (sourceNode: any, targetNode: any, sourceAnchor: any) => { + return sourceAnchor.type === 'right' + } + } + this.sourceRules.push({ + message: t('views.applicationWorkflow.tip.notRecyclable'), + validate: (sourceNode: any, targetNode: any, sourceAnchor: any, targetAnchor: any) => { + return !isLoop(sourceNode.id, targetNode.id) + } + }) + + this.sourceRules.push(circleOnlyAsTarget) + this.targetRules.push({ + message: t('views.applicationWorkflow.tip.onlyLeft'), + validate: (sourceNode: any, targetNode: any, sourceAnchor: any, targetAnchor: any) => { + return targetAnchor.type === 'left' + } + }) + } + getDefaultAnchor() { + const { id, x, y, width } = this + const showNode = this.properties.showNode === undefined ? true : this.properties.showNode + const anchors: any = [] + + if (this.type !== WorkflowType.Base) { + if (this.type !== WorkflowType.Start) { + anchors.push({ + x: x - width / 2 + 10, + y: showNode ? y : y - 15, + id: `${id}_left`, + edgeAddable: false, + type: 'left' + }) + } + anchors.push({ + x: x + width / 2 - 10, + y: showNode ? y : y - 15, + id: `${id}_right`, + type: 'right' + }) + } + + return anchors + } +} +export { AppNodeModel, AppNode } diff --git a/ui/src/workflow/common/data.ts b/ui/src/workflow/common/data.ts new file mode 100644 index 000000000..6ce89c243 --- /dev/null +++ b/ui/src/workflow/common/data.ts @@ -0,0 +1,465 @@ +import { WorkflowType } from '@/enums/workflow' +import { t } from '@/locales' + +export const startNode = { + id: WorkflowType.Start, + type: WorkflowType.Start, + x: 480, + y: 3340, + properties: { + height: 364, + stepName: t('views.applicationWorkflow.nodes.startNode.label'), + config: { + fields: [ + { + label: t('views.applicationWorkflow.nodes.startNode.question'), + value: 'question' + } + ], + globalFields: [ + { label: t('views.applicationWorkflow.nodes.startNode.currentTime'), value: 'time' }, + { + label: t('views.application.applicationForm.form.historyRecord.label'), + value: 'history_context' + }, + { + label: t('chat.chatId'), + value: 'chat_id' + } + ] + }, + fields: [{ label: t('views.applicationWorkflow.nodes.startNode.question'), value: 'question' }], + globalFields: [ + { label: t('views.applicationWorkflow.nodes.startNode.currentTime'), value: 'time' } + ], + showNode: true + } +} +export const baseNode = { + id: WorkflowType.Base, + type: WorkflowType.Base, + x: 360, + y: 2761.3875, + text: '', + properties: { + height: 728.375, + stepName: t('views.applicationWorkflow.nodes.baseNode.label'), + input_field_list: [], + node_data: { + name: '', + desc: '', + // @ts-ignore + prologue: t('views.application.applicationForm.form.defaultPrologue'), + tts_type: 'BROWSER' + }, + config: {}, + showNode: true, + user_input_config: { title: t('chat.userInput') }, + user_input_field_list: [] + } +} +/** + * 说明 + * type 与 nodes 文件对应 + */ +export const baseNodes = [baseNode, startNode] +/** + * ai对话节点配置数据 + */ +export const aiChatNode = { + type: WorkflowType.AiChat, + text: t('views.applicationWorkflow.nodes.aiChatNode.text'), + label: t('views.applicationWorkflow.nodes.aiChatNode.label'), + height: 340, + properties: { + stepName: t('views.applicationWorkflow.nodes.aiChatNode.label'), + config: { + fields: [ + { + label: t('views.applicationWorkflow.nodes.aiChatNode.answer'), + value: 'answer' + }, + { + label: t('views.applicationWorkflow.nodes.aiChatNode.think'), + value: 'reasoning_content' + } + ] + } + } +} +/** + * 知识库检索配置数据 + */ +export const searchDatasetNode = { + type: WorkflowType.SearchDataset, + text: t('views.applicationWorkflow.nodes.searchDatasetNode.text'), + label: t('views.applicationWorkflow.nodes.searchDatasetNode.label'), + height: 355, + properties: { + stepName: t('views.applicationWorkflow.nodes.searchDatasetNode.label'), + config: { + fields: [ + { + label: t('views.applicationWorkflow.nodes.searchDatasetNode.paragraph_list'), + value: 'paragraph_list' + }, + { + label: t('views.applicationWorkflow.nodes.searchDatasetNode.is_hit_handling_method_list'), + value: 'is_hit_handling_method_list' + }, + { + label: t('views.applicationWorkflow.nodes.searchDatasetNode.result'), + value: 'data' + }, + { + label: t('views.applicationWorkflow.nodes.searchDatasetNode.directly_return'), + value: 'directly_return' + } + ] + } + } +} +export const questionNode = { + type: WorkflowType.Question, + text: t('views.applicationWorkflow.nodes.questionNode.text'), + label: t('views.applicationWorkflow.nodes.questionNode.label'), + height: 345, + properties: { + stepName: t('views.applicationWorkflow.nodes.questionNode.label'), + config: { + fields: [ + { + label: t('views.applicationWorkflow.nodes.questionNode.result'), + value: 'answer' + } + ] + } + } +} +export const conditionNode = { + type: WorkflowType.Condition, + text: t('views.applicationWorkflow.nodes.conditionNode.text'), + label: t('views.applicationWorkflow.nodes.conditionNode.label'), + height: 175, + properties: { + width: 600, + stepName: t('views.applicationWorkflow.nodes.conditionNode.label'), + config: { + fields: [ + { + label: t('views.applicationWorkflow.nodes.conditionNode.branch_name'), + value: 'branch_name' + } + ] + } + } +} +export const replyNode = { + type: WorkflowType.Reply, + text: t('views.applicationWorkflow.nodes.replyNode.text'), + label: t('views.applicationWorkflow.nodes.replyNode.label'), + height: 210, + properties: { + stepName: t('views.applicationWorkflow.nodes.replyNode.label'), + config: { + fields: [ + { + label: t('views.applicationWorkflow.nodes.replyNode.content'), + value: 'answer' + } + ] + } + } +} +export const rerankerNode = { + type: WorkflowType.RrerankerNode, + text: t('views.applicationWorkflow.nodes.rerankerNode.text'), + label: t('views.applicationWorkflow.nodes.rerankerNode.label'), + height: 252, + properties: { + stepName: t('views.applicationWorkflow.nodes.rerankerNode.label'), + config: { + fields: [ + { + label: t('views.applicationWorkflow.nodes.rerankerNode.result_list'), + value: 'result_list' + }, + { + label: t('views.applicationWorkflow.nodes.rerankerNode.result'), + value: 'result' + } + ] + } + } +} +export const formNode = { + type: WorkflowType.FormNode, + text: t('views.applicationWorkflow.nodes.formNode.text'), + label: t('views.applicationWorkflow.nodes.formNode.label'), + height: 252, + properties: { + width: 600, + stepName: t('views.applicationWorkflow.nodes.formNode.label'), + node_data: { + is_result: true, + form_field_list: [], + form_content_format: `${t('views.applicationWorkflow.nodes.formNode.form_content_format1')} +{{form}} +${t('views.applicationWorkflow.nodes.formNode.form_content_format2')}` + }, + config: { + fields: [ + { + label: t('views.applicationWorkflow.nodes.formNode.form_data'), + value: 'form_data' + } + ] + } + } +} +export const documentExtractNode = { + type: WorkflowType.DocumentExtractNode, + text: t('views.applicationWorkflow.nodes.documentExtractNode.text'), + label: t('views.applicationWorkflow.nodes.documentExtractNode.label'), + height: 252, + properties: { + stepName: t('views.applicationWorkflow.nodes.documentExtractNode.label'), + config: { + fields: [ + { + label: t('views.applicationWorkflow.nodes.documentExtractNode.content'), + value: 'content' + } + ] + } + } +} +export const imageUnderstandNode = { + type: WorkflowType.ImageUnderstandNode, + text: t('views.applicationWorkflow.nodes.imageUnderstandNode.text'), + label: t('views.applicationWorkflow.nodes.imageUnderstandNode.label'), + height: 252, + properties: { + stepName: t('views.applicationWorkflow.nodes.imageUnderstandNode.label'), + config: { + fields: [ + { + label: t('views.applicationWorkflow.nodes.imageUnderstandNode.answer'), + value: 'answer' + } + ] + } + } +} + +export const variableAssignNode = { + type: WorkflowType.VariableAssignNode, + text: t('views.applicationWorkflow.nodes.variableAssignNode.text'), + label: t('views.applicationWorkflow.nodes.variableAssignNode.label'), + height: 252, + properties: { + stepName: t('views.applicationWorkflow.nodes.variableAssignNode.label'), + config: {} + } +} + +export const mcpNode = { + type: WorkflowType.McpNode, + text: t('views.applicationWorkflow.nodes.mcpNode.text'), + label: t('views.applicationWorkflow.nodes.mcpNode.label'), + height: 252, + properties: { + stepName: t('views.applicationWorkflow.nodes.mcpNode.label'), + config: { + fields: [ + { + label: t('common.result'), + value: 'result' + } + ] + } + } +} + +export const imageGenerateNode = { + type: WorkflowType.ImageGenerateNode, + text: t('views.applicationWorkflow.nodes.imageGenerateNode.text'), + label: t('views.applicationWorkflow.nodes.imageGenerateNode.label'), + height: 252, + properties: { + stepName: t('views.applicationWorkflow.nodes.imageGenerateNode.label'), + config: { + fields: [ + { + label: t('views.applicationWorkflow.nodes.imageGenerateNode.answer'), + value: 'answer' + }, + { + label: t('common.fileUpload.image'), + value: 'image' + } + ] + } + } +} + +export const speechToTextNode = { + type: WorkflowType.SpeechToTextNode, + text: t('views.applicationWorkflow.nodes.speechToTextNode.text'), + label: t('views.applicationWorkflow.nodes.speechToTextNode.label'), + height: 252, + properties: { + stepName: t('views.applicationWorkflow.nodes.speechToTextNode.label'), + config: { + fields: [ + { + label: t('common.result'), + value: 'result' + } + ] + } + } +} +export const textToSpeechNode = { + type: WorkflowType.TextToSpeechNode, + text: t('views.applicationWorkflow.nodes.textToSpeechNode.text'), + label: t('views.applicationWorkflow.nodes.textToSpeechNode.label'), + height: 252, + properties: { + stepName: t('views.applicationWorkflow.nodes.textToSpeechNode.label'), + config: { + fields: [ + { + label: t('common.result'), + value: 'result' + } + ] + } + } +} +export const menuNodes = [ + aiChatNode, + imageUnderstandNode, + imageGenerateNode, + searchDatasetNode, + rerankerNode, + conditionNode, + replyNode, + formNode, + questionNode, + documentExtractNode, + speechToTextNode, + textToSpeechNode, + variableAssignNode, + mcpNode +] + +/** + * 自定义函数配置数据 + */ +export const functionNode = { + type: WorkflowType.FunctionLibCustom, + text: t('views.applicationWorkflow.nodes.functionNode.text'), + label: t('views.applicationWorkflow.nodes.functionNode.label'), + height: 260, + properties: { + stepName: t('views.applicationWorkflow.nodes.functionNode.label'), + config: { + fields: [ + { + label: t('common.result'), + value: 'result' + } + ] + } + } +} +export const functionLibNode = { + type: WorkflowType.FunctionLib, + text: t('views.applicationWorkflow.nodes.functionNode.text'), + label: t('views.applicationWorkflow.nodes.functionNode.label'), + height: 170, + properties: { + stepName: t('views.applicationWorkflow.nodes.functionNode.label'), + config: { + fields: [ + { + label: t('common.result'), + value: 'result' + } + ] + } + } +} + +export const applicationNode = { + type: WorkflowType.Application, + text: t('views.applicationWorkflow.nodes.applicationNode.label'), + label: t('views.applicationWorkflow.nodes.applicationNode.label'), + height: 260, + properties: { + stepName: t('views.applicationWorkflow.nodes.applicationNode.label'), + config: { + fields: [ + { + label: t('common.result'), + value: 'result' + } + ] + } + } +} + +export const compareList = [ + { value: 'is_null', label: t('views.applicationWorkflow.compare.is_null') }, + { value: 'is_not_null', label: t('views.applicationWorkflow.compare.is_not_null') }, + { value: 'contain', label: t('views.applicationWorkflow.compare.contain') }, + { value: 'not_contain', label: t('views.applicationWorkflow.compare.not_contain') }, + { value: 'eq', label: t('views.applicationWorkflow.compare.eq') }, + { value: 'ge', label: t('views.applicationWorkflow.compare.ge') }, + { value: 'gt', label: t('views.applicationWorkflow.compare.gt') }, + { value: 'le', label: t('views.applicationWorkflow.compare.le') }, + { value: 'lt', label: t('views.applicationWorkflow.compare.lt') }, + { value: 'len_eq', label: t('views.applicationWorkflow.compare.len_eq') }, + { value: 'len_ge', label: t('views.applicationWorkflow.compare.len_ge') }, + { value: 'len_gt', label: t('views.applicationWorkflow.compare.len_gt') }, + { value: 'len_le', label: t('views.applicationWorkflow.compare.len_le') }, + { value: 'len_lt', label: t('views.applicationWorkflow.compare.len_lt') }, + { value: 'is_true', label: t('views.applicationWorkflow.compare.is_true') }, + { value: 'is_not_true', label: t('views.applicationWorkflow.compare.is_not_true') } +] + +export const nodeDict: any = { + [WorkflowType.AiChat]: aiChatNode, + [WorkflowType.SearchDataset]: searchDatasetNode, + [WorkflowType.Question]: questionNode, + [WorkflowType.Condition]: conditionNode, + [WorkflowType.Base]: baseNode, + [WorkflowType.Start]: startNode, + [WorkflowType.Reply]: replyNode, + [WorkflowType.FunctionLib]: functionLibNode, + [WorkflowType.FunctionLibCustom]: functionNode, + [WorkflowType.RrerankerNode]: rerankerNode, + [WorkflowType.FormNode]: formNode, + [WorkflowType.Application]: applicationNode, + [WorkflowType.DocumentExtractNode]: documentExtractNode, + [WorkflowType.ImageUnderstandNode]: imageUnderstandNode, + [WorkflowType.TextToSpeechNode]: textToSpeechNode, + [WorkflowType.SpeechToTextNode]: speechToTextNode, + [WorkflowType.ImageGenerateNode]: imageGenerateNode, + [WorkflowType.VariableAssignNode]: variableAssignNode, + [WorkflowType.McpNode]: mcpNode +} +export function isWorkFlow(type: string | undefined) { + return type === 'WORK_FLOW' +} + +export function isLastNode(nodeModel: any) { + const incoming = nodeModel.graphModel.getNodeIncomingNode(nodeModel.id) + const outcomming = nodeModel.graphModel.getNodeOutgoingNode(nodeModel.id) + if (incoming.length > 0 && outcomming.length === 0) { + return true + } else { + return false + } +} diff --git a/ui/src/workflow/common/edge.ts b/ui/src/workflow/common/edge.ts new file mode 100644 index 000000000..461b09650 --- /dev/null +++ b/ui/src/workflow/common/edge.ts @@ -0,0 +1,243 @@ +import { BezierEdge, BezierEdgeModel, h } from '@logicflow/core' +import { createApp, h as vh } from 'vue' +import { isActive, connect, disconnect } from './teleport' +import CustomLine from './CustomLine.vue' +function isMouseInElement(element: any, e: any) { + const rect = element.getBoundingClientRect() + return ( + e.clientX >= rect.left && + e.clientX <= rect.right && + e.clientY >= rect.top && + e.clientY <= rect.bottom + ) +} +const DEFAULT_WIDTH = 32 +const DEFAULT_HEIGHT = 32 +class CustomEdge2 extends BezierEdge { + isMounted + customLineApp?: any + root?: any + constructor() { + super() + this.isMounted = false + this.handleMouseUp = (e: any) => { + this.props.graphModel.clearSelectElements() + this.props.model.isSelected = true + const element = e.target.parentNode.parentNode.querySelector('.lf-custom-edge-wrapper') + if (isMouseInElement(element, e)) { + this.props.model.graphModel.deleteEdgeById(this.props.model.id) + } + } + } + /** + * 渲染vue组件 + * @param root + */ + protected renderVueComponent(root: any) { + this.unmountVueComponent() + this.root = root + const { graphModel } = this.props + if (root) { + if (isActive()) { + connect( + this.targetId(), + CustomLine, + root, + this.props.model, + graphModel, + (node: any, graph: any) => { + return { model: node, graph } + } + ) + } else { + this.customLineApp = createApp({ + render: () => vh(CustomLine, { model: this.props.model }) + }) + this.customLineApp?.mount(root) + } + } + } + protected targetId() { + return `${this.props.graphModel.flowId}:${this.props.model.id}` + } + /** + * 组件即将卸载勾子 + */ + componentWillUnmount() { + if (super.componentWillUnmount) { + super.componentWillUnmount() + } + if (isActive()) { + disconnect(this.targetId()) + } + this.unmountVueComponent() + } + /** + * 卸载vue + * @returns + */ + protected unmountVueComponent() { + if (this.customLineApp) { + this.customLineApp.unmount() + this.customLineApp = null + } + if (this.root) { + this.root.innerHTML = '' + } + return this.root + } + + getEdge() { + const { model } = this.props + const id = model.id + const { customWidth = DEFAULT_WIDTH, customHeight = DEFAULT_HEIGHT } = model.getProperties() + const { startPoint, endPoint, path, isAnimation, arrowConfig } = model + const animationStyle = model.getEdgeAnimationStyle() + const { + strokeDasharray, + stroke, + strokeDashoffset, + animationName, + animationDuration, + animationIterationCount, + animationTimingFunction, + animationDirection + } = animationStyle + const positionData = { + x: (startPoint.x + endPoint.x - customWidth) / 2, + y: (startPoint.y + endPoint.y - customHeight) / 2, + width: customWidth, + height: customHeight + } + const style = model.getEdgeStyle() + const wrapperStyle = { + width: customWidth, + height: customHeight + } + + setTimeout(() => { + const s = document.getElementById(id) + if (s && !this.isMounted) { + this.isMounted = true + this.renderVueComponent(s) + } + }, 0) + + delete style.stroke + + return h('g', {}, [ + h( + 'style' as any, + { type: 'text/css' }, + '.lf-edge{stroke:#afafaf}.lf-edge:hover{stroke: #3370FF;}' + ), + h('path', { + d: path, + ...style, + ...arrowConfig, + ...(isAnimation + ? { + strokeDasharray, + stroke, + style: { + strokeDashoffset, + animationName, + animationDuration, + animationIterationCount, + animationTimingFunction, + animationDirection + } + } + : {}) + }), + h( + 'foreignObject', + { + ...positionData, + y: positionData.y + 5, + x: positionData.x + 5, + style: {} + }, + [ + h('div', { + id, + style: { ...wrapperStyle }, + className: 'lf-custom-edge-wrapper' + }) + ] + ) + ]) + } +} + +class CustomEdgeModel2 extends BezierEdgeModel { + getArrowStyle() { + const arrowStyle = super.getArrowStyle() + arrowStyle.offset = 1 + arrowStyle.verticalLength = 0 + return arrowStyle + } + + getEdgeStyle() { + const style = super.getEdgeStyle() + // svg属性 + style.strokeWidth = 2 + style.stroke = '#BBBFC4' + style.offset = 0 + return style + } + /** + * 重写此方法,使保存数据是能带上锚点数据。 + */ + getData() { + const data: any = super.getData() + if (data) { + data.sourceAnchorId = this.sourceAnchorId + data.targetAnchorId = this.targetAnchorId + } + return data + } + /** + * 给边自定义方案,使其支持基于锚点的位置更新边的路径 + */ + updatePathByAnchor() { + // TODO + const sourceNodeModel = this.graphModel.getNodeModelById(this.sourceNodeId) + const sourceAnchor = sourceNodeModel + .getDefaultAnchor() + .find((anchor: any) => anchor.id === this.sourceAnchorId) + + const targetNodeModel = this.graphModel.getNodeModelById(this.targetNodeId) + const targetAnchor = targetNodeModel + .getDefaultAnchor() + .find((anchor: any) => anchor.id === this.targetAnchorId) + if (sourceAnchor && targetAnchor) { + const startPoint = { + x: sourceAnchor.x, + y: sourceAnchor.y + } + this.updateStartPoint(startPoint) + const endPoint = { + x: targetAnchor.x, + y: targetAnchor.y + } + + this.updateEndPoint(endPoint) + } + + // 这里需要将原有的pointsList设置为空,才能触发bezier的自动计算control点。 + this.pointsList = [] + this.initPoints() + } + setAttributes(): void { + super.setAttributes() + this.isHitable = true + this.zIndex = 0 + } +} + +export default { + type: 'app-edge', + view: CustomEdge2, + model: CustomEdgeModel2 +} diff --git a/ui/src/workflow/common/shortcut.ts b/ui/src/workflow/common/shortcut.ts new file mode 100644 index 000000000..2b3235b6b --- /dev/null +++ b/ui/src/workflow/common/shortcut.ts @@ -0,0 +1,136 @@ +import type LogicFlow from '@logicflow/core' +import { type GraphModel } from '@logicflow/core' +import { MsgSuccess, MsgError, MsgConfirm } from '@/utils/message' +import { WorkflowType } from '@/enums/workflow' +import { t } from '@/locales' +let selected: any | null = null + +function translationNodeData(nodeData: any, distance: any) { + nodeData.x += distance + nodeData.y += distance + if (nodeData.text) { + nodeData.text.x += distance + nodeData.text.y += distance + } + return nodeData +} + +function translationEdgeData(edgeData: any, distance: any) { + if (edgeData.startPoint) { + edgeData.startPoint.x += distance + edgeData.startPoint.y += distance + } + if (edgeData.endPoint) { + edgeData.endPoint.x += distance + edgeData.endPoint.y += distance + } + if (edgeData.pointsList && edgeData.pointsList.length > 0) { + edgeData.pointsList.forEach((point: any) => { + point.x += distance + point.y += distance + }) + } + if (edgeData.text) { + edgeData.text.x += distance + edgeData.text.y += distance + } + return edgeData +} + +const TRANSLATION_DISTANCE = 40 +let CHILDREN_TRANSLATION_DISTANCE = 40 + +export function initDefaultShortcut(lf: LogicFlow, graph: GraphModel) { + const { keyboard } = lf + const { + options: { keyboard: keyboardOptions } + } = keyboard + const copy_node = () => { + CHILDREN_TRANSLATION_DISTANCE = TRANSLATION_DISTANCE + if (!keyboardOptions?.enabled) return true + if (graph.textEditElement) return true + const { guards } = lf.options + const elements = graph.getSelectElements(false) + const enabledClone = guards && guards.beforeClone ? guards.beforeClone(elements) : true + if (!enabledClone || (elements.nodes.length === 0 && elements.edges.length === 0)) { + selected = null + return true + } + const base_nodes = elements.nodes.filter( + (node: any) => node.type === WorkflowType.Start || node.type === WorkflowType.Base + ) + if (base_nodes.length > 0) { + MsgError(base_nodes[0]?.properties?.stepName + t('views.applicationWorkflow.tip.cannotCopy')) + return + } + selected = elements + selected.nodes.forEach((node: any) => translationNodeData(node, TRANSLATION_DISTANCE)) + selected.edges.forEach((edge: any) => translationEdgeData(edge, TRANSLATION_DISTANCE)) + MsgSuccess(t('views.applicationWorkflow.tip.copyError')) + return false + } + const paste_node = () => { + if (!keyboardOptions?.enabled) return true + if (graph.textEditElement) return true + if (selected && (selected.nodes || selected.edges)) { + lf.clearSelectElements() + const addElements = lf.addElements(selected, CHILDREN_TRANSLATION_DISTANCE) + if (!addElements) return true + addElements.nodes.forEach((node) => lf.selectElementById(node.id, true)) + addElements.edges.forEach((edge) => lf.selectElementById(edge.id, true)) + selected.nodes.forEach((node: any) => translationNodeData(node, TRANSLATION_DISTANCE)) + selected.edges.forEach((edge: any) => translationEdgeData(edge, TRANSLATION_DISTANCE)) + CHILDREN_TRANSLATION_DISTANCE = CHILDREN_TRANSLATION_DISTANCE + TRANSLATION_DISTANCE + } + return false + } + const delete_node = () => { + const elements = graph.getSelectElements(true) + lf.clearSelectElements() + if (elements.nodes.length == 0 && elements.edges.length == 0) { + return + } + if (elements.edges.length > 0 && elements.nodes.length == 0) { + elements.edges.forEach((edge: any) => lf.deleteEdge(edge.id)) + return + } + const nodes = elements.nodes.filter((node) => ['start-node', 'base-node'].includes(node.type)) + if (nodes.length > 0) { + MsgError(`${nodes[0].properties?.stepName}${t('views.applicationWorkflow.delete.deleteMessage')}`) + return + } + MsgConfirm(t('common.tip'), t('views.applicationWorkflow.delete.confirmTitle'), { + confirmButtonText: t('common.confirm'), + confirmButtonClass: 'danger' + }).then(() => { + if (!keyboardOptions?.enabled) return true + if (graph.textEditElement) return true + + elements.edges.forEach((edge: any) => lf.deleteEdge(edge.id)) + elements.nodes.forEach((node: any) => lf.deleteNode(node.id)) + }) + + return false + } + graph.eventCenter.on('copy_node', copy_node) + // 复制 + keyboard.on(['cmd + c', 'ctrl + c'], copy_node) + // 粘贴 + keyboard.on(['cmd + v', 'ctrl + v'], paste_node) + // undo + keyboard.on(['cmd + z', 'ctrl + z'], () => { + // if (!keyboardOptions?.enabled) return true + // if (graph.textEditElement) return true + // lf.undo() + // return false + }) + // redo + keyboard.on(['cmd + y', 'ctrl + y'], () => { + if (!keyboardOptions?.enabled) return true + if (graph.textEditElement) return true + lf.redo() + return false + }) + // delete + keyboard.on(['backspace'], delete_node) +} diff --git a/ui/src/workflow/common/teleport.ts b/ui/src/workflow/common/teleport.ts new file mode 100644 index 000000000..6f88b414f --- /dev/null +++ b/ui/src/workflow/common/teleport.ts @@ -0,0 +1,80 @@ +import { BaseEdgeModel, BaseNodeModel, GraphModel } from '@logicflow/core' +import { defineComponent, h, reactive, isVue3, Teleport, markRaw, Fragment } from 'vue-demi' + +let active = false +const items = reactive<{ [key: string]: any }>({}) + +export function connect( + id: string, + component: any, + container: HTMLDivElement, + node: BaseNodeModel | BaseEdgeModel, + graph: GraphModel, + get_props?: any +) { + if (!get_props) { + get_props = (node: BaseNodeModel | BaseEdgeModel, graph: GraphModel) => { + return { nodeModel: node, graph } + } + } + if (active) { + items[id] = markRaw( + defineComponent({ + render: () => h(Teleport, { to: container } as any, [h(component, get_props(node, graph))]), + provide: () => ({ + getNode: () => node, + getGraph: () => graph + }) + }) + ) + } +} + +export function disconnect(id: string) { + if (active) { + delete items[id] + } +} + +export function isActive() { + return active +} + +export function getTeleport(): any { + if (!isVue3) { + throw new Error('teleport is only available in Vue3') + } + active = true + + return defineComponent({ + props: { + flowId: { + type: String, + required: true + } + }, + setup(props) { + return () => { + const children: Record[] = [] + Object.keys(items).forEach((id) => { + // https://github.com/didi/LogicFlow/issues/1768 + // 多个不同的VueNodeView都会connect注册到items中,因此items存储了可能有多个flowId流程图的数据 + // 当使用多个LogicFlow时,会创建多个flowId + 同时使用KeepAlive + // 每一次items改变,会触发不同flowId持有的setup()执行,由于每次setup()执行就是遍历items,因此存在多次重复渲染元素的问题 + // 即items[0]会在Page1的setup()执行,items[0]也会在Page2的setup()执行,从而生成两个items[0] + + // 比对当前界面显示的flowId,只更新items[当前页面flowId:nodeId]的数据 + // 比如items[0]属于Page1的数据,那么Page2无论active=true/false,都无法执行items[0] + if (id.startsWith(props.flowId)) { + children.push(items[id]) + } + }) + return h( + Fragment, + {}, + children.map((item) => h(item)) + ) + } + } + }) +} diff --git a/ui/src/workflow/common/validate.ts b/ui/src/workflow/common/validate.ts new file mode 100644 index 000000000..cc9378bc3 --- /dev/null +++ b/ui/src/workflow/common/validate.ts @@ -0,0 +1,150 @@ +import { WorkflowType } from '@/enums/workflow' +import { t } from '@/locales' + +const end_nodes: Array = [ + WorkflowType.AiChat, + WorkflowType.Reply, + WorkflowType.FunctionLib, + WorkflowType.FunctionLibCustom, + WorkflowType.ImageUnderstandNode, + WorkflowType.Application, + WorkflowType.SpeechToTextNode, + WorkflowType.TextToSpeechNode, + WorkflowType.ImageGenerateNode, +] +export class WorkFlowInstance { + nodes + edges + workFlowNodes: Array + constructor(workflow: { nodes: Array; edges: Array }) { + this.nodes = workflow.nodes + this.edges = workflow.edges + this.workFlowNodes = [] + } + /** + * 校验开始节点 + */ + private is_valid_start_node() { + const start_node_list = this.nodes.filter((item) => item.id === WorkflowType.Start) + if (start_node_list.length == 0) { + throw t('views.applicationWorkflow.validate.startNodeRequired') + } else if (start_node_list.length > 1) { + throw t('views.applicationWorkflow.validate.startNodeOnly') + } + } + /** + * 校验基本信息节点 + */ + private is_valid_base_node() { + const start_node_list = this.nodes.filter((item) => item.id === WorkflowType.Base) + if (start_node_list.length == 0) { + throw t('views.applicationWorkflow.validate.baseNodeRequired') + } else if (start_node_list.length > 1) { + throw t('views.applicationWorkflow.validate.baseNodeOnly') + } + } + /** + * 校验节点 + */ + is_valid() { + this.is_valid_start_node() + this.is_valid_base_node() + this.is_valid_work_flow() + this.is_valid_nodes() + } + + /** + * 获取开始节点 + * @returns + */ + get_start_node() { + const start_node_list = this.nodes.filter((item) => item.id === WorkflowType.Start) + return start_node_list[0] + } + /** + * 获取基本节点 + * @returns 基本节点 + */ + get_base_node() { + const base_node_list = this.nodes.filter((item) => item.id === WorkflowType.Base) + return base_node_list[0] + } + + /** + * 校验工作流 + * @param up_node 上一个节点 + */ + private _is_valid_work_flow(up_node?: any) { + if (!up_node) { + up_node = this.get_start_node() + } + this.workFlowNodes.push(up_node) + this.is_valid_node(up_node) + const next_nodes = this.get_next_nodes(up_node) + for (const next_node of next_nodes) { + this._is_valid_work_flow(next_node) + } + } + private is_valid_work_flow() { + this.workFlowNodes = [] + this._is_valid_work_flow() + const notInWorkFlowNodes = this.nodes + .filter((node: any) => node.id !== WorkflowType.Start && node.id !== WorkflowType.Base) + .filter((node) => !this.workFlowNodes.includes(node)) + if (notInWorkFlowNodes.length > 0) { + throw `${t('views.applicationWorkflow.validate.notInWorkFlowNode')}:${notInWorkFlowNodes.map((node) => node.properties.stepName).join(',')}` + } + this.workFlowNodes = [] + } + /** + * 获取流程下一个节点列表 + * @param node 节点 + * @returns 节点列表 + */ + private get_next_nodes(node: any) { + const edge_list = this.edges.filter((edge) => edge.sourceNodeId == node.id) + const node_list = edge_list + .map((edge) => this.nodes.filter((node) => node.id == edge.targetNodeId)) + .reduce((x, y) => [...x, ...y], []) + if (node_list.length == 0 && !end_nodes.includes(node.type)) { + throw t('views.applicationWorkflow.validate.noNextNode') + } + return node_list + } + private is_valid_nodes() { + for (const node of this.nodes) { + if (node.type !== WorkflowType.Base && node.type !== WorkflowType.Start) { + if (!this.edges.some((edge) => edge.targetNodeId === node.id)) { + throw `${t('views.applicationWorkflow.validate.notInWorkFlowNode')}:${node.properties.stepName}` + } + } + } + } + /** + * 校验节点 + * @param node 节点 + */ + private is_valid_node(node: any) { + if (node.properties.status && node.properties.status === 500) { + throw `${node.properties.stepName} ${t('views.applicationWorkflow.validate.nodeUnavailable')}` + } + if (node.type === WorkflowType.Condition) { + const branch_list = node.properties.node_data.branch + for (const branch of branch_list) { + const source_anchor_id = `${node.id}_${branch.id}_right` + const edge_list = this.edges.filter((edge) => edge.sourceAnchorId == source_anchor_id) + if (edge_list.length == 0) { + throw `${node.properties.stepName} ${t('views.applicationWorkflow.validate.needConnect1')}${branch.type}${t('views.applicationWorkflow.validate.needConnect2')}` + } + } + } else { + const edge_list = this.edges.filter((edge) => edge.sourceNodeId == node.id) + if (edge_list.length == 0 && !end_nodes.includes(node.type)) { + throw `${node.properties.stepName} ${t('views.applicationWorkflow.validate.cannotEndNode')}` + } + } + if (node.properties.status && node.properties.status !== 200) { + throw `${node.properties.stepName} ${t('views.applicationWorkflow.validate.nodeUnavailable')}` + } + } +} diff --git a/ui/src/workflow/icons/ai-chat-node-icon.vue b/ui/src/workflow/icons/ai-chat-node-icon.vue new file mode 100644 index 000000000..24e6d468e --- /dev/null +++ b/ui/src/workflow/icons/ai-chat-node-icon.vue @@ -0,0 +1,6 @@ + + diff --git a/ui/src/workflow/icons/application-node-icon.vue b/ui/src/workflow/icons/application-node-icon.vue new file mode 100644 index 000000000..347588b51 --- /dev/null +++ b/ui/src/workflow/icons/application-node-icon.vue @@ -0,0 +1,28 @@ + + diff --git a/ui/src/workflow/icons/base-node-icon.vue b/ui/src/workflow/icons/base-node-icon.vue new file mode 100644 index 000000000..a0b5d2752 --- /dev/null +++ b/ui/src/workflow/icons/base-node-icon.vue @@ -0,0 +1,6 @@ + + diff --git a/ui/src/workflow/icons/condition-node-icon.vue b/ui/src/workflow/icons/condition-node-icon.vue new file mode 100644 index 000000000..6deed3108 --- /dev/null +++ b/ui/src/workflow/icons/condition-node-icon.vue @@ -0,0 +1,6 @@ + + diff --git a/ui/src/workflow/icons/document-extract-node-icon.vue b/ui/src/workflow/icons/document-extract-node-icon.vue new file mode 100644 index 000000000..7b81719c6 --- /dev/null +++ b/ui/src/workflow/icons/document-extract-node-icon.vue @@ -0,0 +1,6 @@ + + diff --git a/ui/src/workflow/icons/form-node-icon.vue b/ui/src/workflow/icons/form-node-icon.vue new file mode 100644 index 000000000..40f6ea77f --- /dev/null +++ b/ui/src/workflow/icons/form-node-icon.vue @@ -0,0 +1,6 @@ + + diff --git a/ui/src/workflow/icons/function-lib-node-icon.vue b/ui/src/workflow/icons/function-lib-node-icon.vue new file mode 100644 index 000000000..e8e6f9910 --- /dev/null +++ b/ui/src/workflow/icons/function-lib-node-icon.vue @@ -0,0 +1,25 @@ + + + + diff --git a/ui/src/workflow/icons/function-node-icon.vue b/ui/src/workflow/icons/function-node-icon.vue new file mode 100644 index 000000000..e6e84a801 --- /dev/null +++ b/ui/src/workflow/icons/function-node-icon.vue @@ -0,0 +1,6 @@ + + diff --git a/ui/src/workflow/icons/global-icon.vue b/ui/src/workflow/icons/global-icon.vue new file mode 100644 index 000000000..5d476dc74 --- /dev/null +++ b/ui/src/workflow/icons/global-icon.vue @@ -0,0 +1,4 @@ + + diff --git a/ui/src/workflow/icons/image-generate-node-icon.vue b/ui/src/workflow/icons/image-generate-node-icon.vue new file mode 100644 index 000000000..64ca192b1 --- /dev/null +++ b/ui/src/workflow/icons/image-generate-node-icon.vue @@ -0,0 +1,6 @@ + + diff --git a/ui/src/workflow/icons/image-understand-node-icon.vue b/ui/src/workflow/icons/image-understand-node-icon.vue new file mode 100644 index 000000000..6f58417d2 --- /dev/null +++ b/ui/src/workflow/icons/image-understand-node-icon.vue @@ -0,0 +1,6 @@ + + diff --git a/ui/src/workflow/icons/mcp-node-icon.vue b/ui/src/workflow/icons/mcp-node-icon.vue new file mode 100644 index 000000000..7dc36f22c --- /dev/null +++ b/ui/src/workflow/icons/mcp-node-icon.vue @@ -0,0 +1,6 @@ + + diff --git a/ui/src/workflow/icons/question-node-icon.vue b/ui/src/workflow/icons/question-node-icon.vue new file mode 100644 index 000000000..74ab30d0f --- /dev/null +++ b/ui/src/workflow/icons/question-node-icon.vue @@ -0,0 +1,6 @@ + + diff --git a/ui/src/workflow/icons/reply-node-icon.vue b/ui/src/workflow/icons/reply-node-icon.vue new file mode 100644 index 000000000..07b2ed56f --- /dev/null +++ b/ui/src/workflow/icons/reply-node-icon.vue @@ -0,0 +1,6 @@ + + diff --git a/ui/src/workflow/icons/reranker-node-icon.vue b/ui/src/workflow/icons/reranker-node-icon.vue new file mode 100644 index 000000000..70c8f4842 --- /dev/null +++ b/ui/src/workflow/icons/reranker-node-icon.vue @@ -0,0 +1,6 @@ + + diff --git a/ui/src/workflow/icons/search-dataset-node-icon.vue b/ui/src/workflow/icons/search-dataset-node-icon.vue new file mode 100644 index 000000000..d2b2302b9 --- /dev/null +++ b/ui/src/workflow/icons/search-dataset-node-icon.vue @@ -0,0 +1,6 @@ + + diff --git a/ui/src/workflow/icons/speech-to-text-node-icon.vue b/ui/src/workflow/icons/speech-to-text-node-icon.vue new file mode 100644 index 000000000..37cc26c3b --- /dev/null +++ b/ui/src/workflow/icons/speech-to-text-node-icon.vue @@ -0,0 +1,6 @@ + + diff --git a/ui/src/workflow/icons/start-node-icon.vue b/ui/src/workflow/icons/start-node-icon.vue new file mode 100644 index 000000000..9a01ac96c --- /dev/null +++ b/ui/src/workflow/icons/start-node-icon.vue @@ -0,0 +1,6 @@ + + diff --git a/ui/src/workflow/icons/text-to-speech-node-icon.vue b/ui/src/workflow/icons/text-to-speech-node-icon.vue new file mode 100644 index 000000000..e48a06857 --- /dev/null +++ b/ui/src/workflow/icons/text-to-speech-node-icon.vue @@ -0,0 +1,6 @@ + + diff --git a/ui/src/workflow/icons/utils.ts b/ui/src/workflow/icons/utils.ts new file mode 100644 index 000000000..b667cdc6e --- /dev/null +++ b/ui/src/workflow/icons/utils.ts @@ -0,0 +1,5 @@ +const icons: any = import.meta.glob('./**.vue', { eager: true }) +export function iconComponent(name: string) { + const url = `./${name}.vue` + return icons[url]?.default || null +} diff --git a/ui/src/workflow/icons/variable-assign-node-icon.vue b/ui/src/workflow/icons/variable-assign-node-icon.vue new file mode 100644 index 000000000..a7b580ff3 --- /dev/null +++ b/ui/src/workflow/icons/variable-assign-node-icon.vue @@ -0,0 +1,6 @@ + + diff --git a/ui/src/workflow/index.vue b/ui/src/workflow/index.vue new file mode 100644 index 000000000..03361bcec --- /dev/null +++ b/ui/src/workflow/index.vue @@ -0,0 +1,178 @@ + + + diff --git a/ui/src/workflow/nodes/ai-chat-node/index.ts b/ui/src/workflow/nodes/ai-chat-node/index.ts new file mode 100644 index 000000000..b226719e1 --- /dev/null +++ b/ui/src/workflow/nodes/ai-chat-node/index.ts @@ -0,0 +1,12 @@ +import ChatNodeVue from './index.vue' +import { AppNode, AppNodeModel } from '@/workflow/common/app-node' +class ChatNode extends AppNode { + constructor(props: any) { + super(props, ChatNodeVue) + } +} +export default { + type: 'ai-chat-node', + model: AppNodeModel, + view: ChatNode +} diff --git a/ui/src/workflow/nodes/ai-chat-node/index.vue b/ui/src/workflow/nodes/ai-chat-node/index.vue new file mode 100644 index 000000000..8b4a6aeb9 --- /dev/null +++ b/ui/src/workflow/nodes/ai-chat-node/index.vue @@ -0,0 +1,338 @@ + + + diff --git a/ui/src/workflow/nodes/application-node/index.ts b/ui/src/workflow/nodes/application-node/index.ts new file mode 100644 index 000000000..6292ab5df --- /dev/null +++ b/ui/src/workflow/nodes/application-node/index.ts @@ -0,0 +1,12 @@ +import ChatNodeVue from './index.vue' +import { AppNode, AppNodeModel } from '@/workflow/common/app-node' +class ChatNode extends AppNode { + constructor(props: any) { + super(props, ChatNodeVue) + } +} +export default { + type: 'application-node', + model: AppNodeModel, + view: ChatNode +} diff --git a/ui/src/workflow/nodes/application-node/index.vue b/ui/src/workflow/nodes/application-node/index.vue new file mode 100644 index 000000000..4fc9fba54 --- /dev/null +++ b/ui/src/workflow/nodes/application-node/index.vue @@ -0,0 +1,317 @@ + + + + + diff --git a/ui/src/workflow/nodes/base-node/component/ApiFieldFormDialog.vue b/ui/src/workflow/nodes/base-node/component/ApiFieldFormDialog.vue new file mode 100644 index 000000000..41bc6cabe --- /dev/null +++ b/ui/src/workflow/nodes/base-node/component/ApiFieldFormDialog.vue @@ -0,0 +1,132 @@ + + + diff --git a/ui/src/workflow/nodes/base-node/component/ApiInputFieldTable.vue b/ui/src/workflow/nodes/base-node/component/ApiInputFieldTable.vue new file mode 100644 index 000000000..c81ebc94f --- /dev/null +++ b/ui/src/workflow/nodes/base-node/component/ApiInputFieldTable.vue @@ -0,0 +1,157 @@ + + + + + diff --git a/ui/src/workflow/nodes/base-node/component/FileUploadSettingDialog.vue b/ui/src/workflow/nodes/base-node/component/FileUploadSettingDialog.vue new file mode 100644 index 000000000..0f55fa1dd --- /dev/null +++ b/ui/src/workflow/nodes/base-node/component/FileUploadSettingDialog.vue @@ -0,0 +1,288 @@ + + + + + diff --git a/ui/src/workflow/nodes/base-node/component/UserFieldFormDialog.vue b/ui/src/workflow/nodes/base-node/component/UserFieldFormDialog.vue new file mode 100644 index 000000000..21b72971d --- /dev/null +++ b/ui/src/workflow/nodes/base-node/component/UserFieldFormDialog.vue @@ -0,0 +1,168 @@ + + + diff --git a/ui/src/workflow/nodes/base-node/component/UserInputFieldTable.vue b/ui/src/workflow/nodes/base-node/component/UserInputFieldTable.vue new file mode 100644 index 000000000..88615954c --- /dev/null +++ b/ui/src/workflow/nodes/base-node/component/UserInputFieldTable.vue @@ -0,0 +1,258 @@ + + + + + diff --git a/ui/src/workflow/nodes/base-node/component/UserInputTitleDialog.vue b/ui/src/workflow/nodes/base-node/component/UserInputTitleDialog.vue new file mode 100644 index 000000000..561e75a57 --- /dev/null +++ b/ui/src/workflow/nodes/base-node/component/UserInputTitleDialog.vue @@ -0,0 +1,82 @@ + + + diff --git a/ui/src/workflow/nodes/base-node/index.ts b/ui/src/workflow/nodes/base-node/index.ts new file mode 100644 index 000000000..69ede8e06 --- /dev/null +++ b/ui/src/workflow/nodes/base-node/index.ts @@ -0,0 +1,22 @@ +import BaseNodeVue from './index.vue' +import { AppNode, AppNodeModel } from '@/workflow/common/app-node' + +class BaseNode extends AppNode { + constructor(props: any) { + super(props, BaseNodeVue) + } +} + +class BaseModel extends AppNodeModel { + constructor(data: any, graphModel: any) { + super(data, graphModel) + } + get_width() { + return 600 + } +} +export default { + type: 'base-node', + model: BaseModel, + view: BaseNode +} diff --git a/ui/src/workflow/nodes/base-node/index.vue b/ui/src/workflow/nodes/base-node/index.vue new file mode 100644 index 000000000..5904745b2 --- /dev/null +++ b/ui/src/workflow/nodes/base-node/index.vue @@ -0,0 +1,349 @@ + + + diff --git a/ui/src/workflow/nodes/condition-node/index.ts b/ui/src/workflow/nodes/condition-node/index.ts new file mode 100644 index 000000000..275c63a2d --- /dev/null +++ b/ui/src/workflow/nodes/condition-node/index.ts @@ -0,0 +1,68 @@ +import ConditioNodeVue from './index.vue' +import { cloneDeep, set } from 'lodash' +import { AppNode, AppNodeModel } from '@/workflow/common/app-node' +class ConditioNode extends AppNode { + constructor(props: any) { + super(props, ConditioNodeVue) + } +} +const get_up_index_height = (condition_list: Array, index: number) => { + return condition_list + .filter((item, i) => i < index) + .map((item) => item.height + 8) + .reduce((x, y) => x + y, 0) +} +class ConditionModel extends AppNodeModel { + refreshBranch() { + // 更新节点连接边的path + this.incoming.edges.forEach((edge: any) => { + // 调用自定义的更新方案 + edge.updatePathByAnchor() + }) + this.outgoing.edges.forEach((edge: any) => { + edge.updatePathByAnchor() + }) + } + getDefaultAnchor() { + const { + id, + x, + y, + width, + height, + properties: { branch_condition_list } + } = this + if (this.height === undefined) { + this.height = 200 + } + const showNode = this.properties.showNode === undefined ? true : this.properties.showNode + const anchors: any = [] + anchors.push({ + x: x - width / 2 + 10, + y: showNode ? y : y - 15, + id: `${id}_left`, + edgeAddable: false, + type: 'left' + }) + + if (branch_condition_list) { + for (let index = 0; index < branch_condition_list.length; index++) { + const element = branch_condition_list[index] + const h = get_up_index_height(branch_condition_list, index) + anchors.push({ + x: x + width / 2 - 10, + y: showNode ? y - height / 2 + 75 + h + element.height / 2 : y - 15, + id: `${id}_${element.id}_right`, + type: 'right' + }) + } + } + + return anchors + } +} +export default { + type: 'condition-node', + model: ConditionModel, + view: ConditioNode +} diff --git a/ui/src/workflow/nodes/condition-node/index.vue b/ui/src/workflow/nodes/condition-node/index.vue new file mode 100644 index 000000000..f5044ae95 --- /dev/null +++ b/ui/src/workflow/nodes/condition-node/index.vue @@ -0,0 +1,370 @@ + + + diff --git a/ui/src/workflow/nodes/document-extract-node/index.ts b/ui/src/workflow/nodes/document-extract-node/index.ts new file mode 100644 index 000000000..9a7072d2e --- /dev/null +++ b/ui/src/workflow/nodes/document-extract-node/index.ts @@ -0,0 +1,12 @@ +import DocumentExtractNodeVue from './index.vue' +import { AppNode, AppNodeModel } from '@/workflow/common/app-node' +class RerankerNode extends AppNode { + constructor(props: any) { + super(props, DocumentExtractNodeVue) + } +} +export default { + type: 'document-extract-node', + model: AppNodeModel, + view: RerankerNode +} diff --git a/ui/src/workflow/nodes/document-extract-node/index.vue b/ui/src/workflow/nodes/document-extract-node/index.vue new file mode 100644 index 000000000..4e2047efa --- /dev/null +++ b/ui/src/workflow/nodes/document-extract-node/index.vue @@ -0,0 +1,64 @@ + + + + + \ No newline at end of file diff --git a/ui/src/workflow/nodes/form-node/index.ts b/ui/src/workflow/nodes/form-node/index.ts new file mode 100644 index 000000000..b7c2dbec9 --- /dev/null +++ b/ui/src/workflow/nodes/form-node/index.ts @@ -0,0 +1,12 @@ +import FormNodeVue from './index.vue' +import { AppNode, AppNodeModel } from '@/workflow/common/app-node' +class FormNode extends AppNode { + constructor(props: any) { + super(props, FormNodeVue) + } +} +export default { + type: 'form-node', + model: AppNodeModel, + view: FormNode +} diff --git a/ui/src/workflow/nodes/form-node/index.vue b/ui/src/workflow/nodes/form-node/index.vue new file mode 100644 index 000000000..87fe4788b --- /dev/null +++ b/ui/src/workflow/nodes/form-node/index.vue @@ -0,0 +1,281 @@ + + + diff --git a/ui/src/workflow/nodes/function-lib-node/index.ts b/ui/src/workflow/nodes/function-lib-node/index.ts new file mode 100644 index 000000000..475818c1f --- /dev/null +++ b/ui/src/workflow/nodes/function-lib-node/index.ts @@ -0,0 +1,12 @@ +import FunctionLibNodeVue from './index.vue' +import { AppNode, AppNodeModel } from '@/workflow/common/app-node' +class FunctionLibNode extends AppNode { + constructor(props: any) { + super(props, FunctionLibNodeVue) + } +} +export default { + type: 'function-lib-node', + model: AppNodeModel, + view: FunctionLibNode +} diff --git a/ui/src/workflow/nodes/function-lib-node/index.vue b/ui/src/workflow/nodes/function-lib-node/index.vue new file mode 100644 index 000000000..794cabd20 --- /dev/null +++ b/ui/src/workflow/nodes/function-lib-node/index.vue @@ -0,0 +1,161 @@ + + + diff --git a/ui/src/workflow/nodes/function-node/index.ts b/ui/src/workflow/nodes/function-node/index.ts new file mode 100644 index 000000000..ab3f36edf --- /dev/null +++ b/ui/src/workflow/nodes/function-node/index.ts @@ -0,0 +1,12 @@ +import FunctionNodeVue from './index.vue' +import { AppNode, AppNodeModel } from '@/workflow/common/app-node' +class FunctionLibCustomNode extends AppNode { + constructor(props: any) { + super(props, FunctionNodeVue) + } +} +export default { + type: 'function-node', + model: AppNodeModel, + view: FunctionLibCustomNode +} diff --git a/ui/src/workflow/nodes/function-node/index.vue b/ui/src/workflow/nodes/function-node/index.vue new file mode 100644 index 000000000..c720f61e2 --- /dev/null +++ b/ui/src/workflow/nodes/function-node/index.vue @@ -0,0 +1,214 @@ + + + diff --git a/ui/src/workflow/nodes/image-generate/index.ts b/ui/src/workflow/nodes/image-generate/index.ts new file mode 100644 index 000000000..5afc2e571 --- /dev/null +++ b/ui/src/workflow/nodes/image-generate/index.ts @@ -0,0 +1,14 @@ +import ImageGenerateNodeVue from './index.vue' +import { AppNode, AppNodeModel } from '@/workflow/common/app-node' + +class RerankerNode extends AppNode { + constructor(props: any) { + super(props, ImageGenerateNodeVue) + } +} + +export default { + type: 'image-generate-node', + model: AppNodeModel, + view: RerankerNode +} diff --git a/ui/src/workflow/nodes/image-generate/index.vue b/ui/src/workflow/nodes/image-generate/index.vue new file mode 100644 index 000000000..d71f3adf9 --- /dev/null +++ b/ui/src/workflow/nodes/image-generate/index.vue @@ -0,0 +1,266 @@ + + + + + diff --git a/ui/src/workflow/nodes/image-understand/index.ts b/ui/src/workflow/nodes/image-understand/index.ts new file mode 100644 index 000000000..06695d3c9 --- /dev/null +++ b/ui/src/workflow/nodes/image-understand/index.ts @@ -0,0 +1,14 @@ +import ImageUnderstandNodeVue from './index.vue' +import { AppNode, AppNodeModel } from '@/workflow/common/app-node' + +class RerankerNode extends AppNode { + constructor(props: any) { + super(props, ImageUnderstandNodeVue) + } +} + +export default { + type: 'image-understand-node', + model: AppNodeModel, + view: RerankerNode +} diff --git a/ui/src/workflow/nodes/image-understand/index.vue b/ui/src/workflow/nodes/image-understand/index.vue new file mode 100644 index 000000000..e2ac7c13b --- /dev/null +++ b/ui/src/workflow/nodes/image-understand/index.vue @@ -0,0 +1,280 @@ + + + + + diff --git a/ui/src/workflow/nodes/mcp-node/index.ts b/ui/src/workflow/nodes/mcp-node/index.ts new file mode 100644 index 000000000..d626b0c62 --- /dev/null +++ b/ui/src/workflow/nodes/mcp-node/index.ts @@ -0,0 +1,14 @@ +import McpNodeVue from './index.vue' +import { AppNode, AppNodeModel } from '@/workflow/common/app-node' + +class McpNode extends AppNode { + constructor(props: any) { + super(props, McpNodeVue) + } +} + +export default { + type: 'mcp-node', + model: AppNodeModel, + view: McpNode +} diff --git a/ui/src/workflow/nodes/mcp-node/index.vue b/ui/src/workflow/nodes/mcp-node/index.vue new file mode 100644 index 000000000..7cba4d51e --- /dev/null +++ b/ui/src/workflow/nodes/mcp-node/index.vue @@ -0,0 +1,443 @@ + + + diff --git a/ui/src/workflow/nodes/question-node/index.ts b/ui/src/workflow/nodes/question-node/index.ts new file mode 100644 index 000000000..324c24683 --- /dev/null +++ b/ui/src/workflow/nodes/question-node/index.ts @@ -0,0 +1,12 @@ +import QuestionNodeVue from './index.vue' +import { AppNode, AppNodeModel } from '@/workflow/common/app-node' +class QuestionNode extends AppNode { + constructor(props: any) { + super(props, QuestionNodeVue) + } +} +export default { + type: 'question-node', + model: AppNodeModel, + view: QuestionNode +} diff --git a/ui/src/workflow/nodes/question-node/index.vue b/ui/src/workflow/nodes/question-node/index.vue new file mode 100644 index 000000000..f9e9559a9 --- /dev/null +++ b/ui/src/workflow/nodes/question-node/index.vue @@ -0,0 +1,246 @@ + + + diff --git a/ui/src/workflow/nodes/reply-node/index.ts b/ui/src/workflow/nodes/reply-node/index.ts new file mode 100644 index 000000000..e3bd9d95d --- /dev/null +++ b/ui/src/workflow/nodes/reply-node/index.ts @@ -0,0 +1,12 @@ +import ReplyNodeVue from './index.vue' +import { AppNode, AppNodeModel } from '@/workflow/common/app-node' +class ReplyNode extends AppNode { + constructor(props: any) { + super(props, ReplyNodeVue) + } +} +export default { + type: 'reply-node', + model: AppNodeModel, + view: ReplyNode +} diff --git a/ui/src/workflow/nodes/reply-node/index.vue b/ui/src/workflow/nodes/reply-node/index.vue new file mode 100644 index 000000000..e23fd889f --- /dev/null +++ b/ui/src/workflow/nodes/reply-node/index.vue @@ -0,0 +1,142 @@ + + + diff --git a/ui/src/workflow/nodes/reranker-node/ParamSettingDialog.vue b/ui/src/workflow/nodes/reranker-node/ParamSettingDialog.vue new file mode 100644 index 000000000..42cc546f6 --- /dev/null +++ b/ui/src/workflow/nodes/reranker-node/ParamSettingDialog.vue @@ -0,0 +1,126 @@ + + + diff --git a/ui/src/workflow/nodes/reranker-node/index.ts b/ui/src/workflow/nodes/reranker-node/index.ts new file mode 100644 index 000000000..9b3afc5c6 --- /dev/null +++ b/ui/src/workflow/nodes/reranker-node/index.ts @@ -0,0 +1,12 @@ +import RerankerNodeVue from './index.vue' +import { AppNode, AppNodeModel } from '@/workflow/common/app-node' +class RerankerNode extends AppNode { + constructor(props: any) { + super(props, RerankerNodeVue) + } +} +export default { + type: 'reranker-node', + model: AppNodeModel, + view: RerankerNode +} diff --git a/ui/src/workflow/nodes/reranker-node/index.vue b/ui/src/workflow/nodes/reranker-node/index.vue new file mode 100644 index 000000000..355776883 --- /dev/null +++ b/ui/src/workflow/nodes/reranker-node/index.vue @@ -0,0 +1,271 @@ + + + diff --git a/ui/src/workflow/nodes/search-dataset-node/index.ts b/ui/src/workflow/nodes/search-dataset-node/index.ts new file mode 100644 index 000000000..316854dc4 --- /dev/null +++ b/ui/src/workflow/nodes/search-dataset-node/index.ts @@ -0,0 +1,12 @@ +import SearchDatasetVue from './index.vue' +import { AppNode, AppNodeModel } from '@/workflow/common/app-node' +class SearchDatasetNode extends AppNode { + constructor(props: any) { + super(props, SearchDatasetVue) + } +} +export default { + type: 'search-dataset-node', + model: AppNodeModel, + view: SearchDatasetNode +} diff --git a/ui/src/workflow/nodes/search-dataset-node/index.vue b/ui/src/workflow/nodes/search-dataset-node/index.vue new file mode 100644 index 000000000..572e40daf --- /dev/null +++ b/ui/src/workflow/nodes/search-dataset-node/index.vue @@ -0,0 +1,234 @@ + + + diff --git a/ui/src/workflow/nodes/speech-to-text-node/index.ts b/ui/src/workflow/nodes/speech-to-text-node/index.ts new file mode 100644 index 000000000..aabaf1fc1 --- /dev/null +++ b/ui/src/workflow/nodes/speech-to-text-node/index.ts @@ -0,0 +1,12 @@ +import SpeechToTextVue from './index.vue' +import { AppNode, AppNodeModel } from '@/workflow/common/app-node' +class SpeechToTextNode extends AppNode { + constructor(props: any) { + super(props, SpeechToTextVue) + } +} +export default { + type: 'speech-to-text-node', + model: AppNodeModel, + view: SpeechToTextNode +} diff --git a/ui/src/workflow/nodes/speech-to-text-node/index.vue b/ui/src/workflow/nodes/speech-to-text-node/index.vue new file mode 100644 index 000000000..133acddce --- /dev/null +++ b/ui/src/workflow/nodes/speech-to-text-node/index.vue @@ -0,0 +1,177 @@ + + + + + diff --git a/ui/src/workflow/nodes/start-node/index.ts b/ui/src/workflow/nodes/start-node/index.ts new file mode 100644 index 000000000..5adfb7798 --- /dev/null +++ b/ui/src/workflow/nodes/start-node/index.ts @@ -0,0 +1,12 @@ +import StartNodeVue from './index.vue' +import { AppNode, AppNodeModel } from '@/workflow/common/app-node' +class StartNode extends AppNode { + constructor(props: any) { + super(props, StartNodeVue) + } +} +export default { + type: 'start-node', + model: AppNodeModel, + view: StartNode +} diff --git a/ui/src/workflow/nodes/start-node/index.vue b/ui/src/workflow/nodes/start-node/index.vue new file mode 100644 index 000000000..53df2148f --- /dev/null +++ b/ui/src/workflow/nodes/start-node/index.vue @@ -0,0 +1,115 @@ + + + diff --git a/ui/src/workflow/nodes/text-to-speech-node/index.ts b/ui/src/workflow/nodes/text-to-speech-node/index.ts new file mode 100644 index 000000000..04e2363bf --- /dev/null +++ b/ui/src/workflow/nodes/text-to-speech-node/index.ts @@ -0,0 +1,12 @@ +import TextToSpeechVue from './index.vue' +import { AppNode, AppNodeModel } from '@/workflow/common/app-node' +class TextToSpeechNode extends AppNode { + constructor(props: any) { + super(props, TextToSpeechVue) + } +} +export default { + type: 'text-to-speech-node', + model: AppNodeModel, + view: TextToSpeechNode +} diff --git a/ui/src/workflow/nodes/text-to-speech-node/index.vue b/ui/src/workflow/nodes/text-to-speech-node/index.vue new file mode 100644 index 000000000..d5b9fe609 --- /dev/null +++ b/ui/src/workflow/nodes/text-to-speech-node/index.vue @@ -0,0 +1,203 @@ + + + + + diff --git a/ui/src/workflow/nodes/variable-assign-node/index.ts b/ui/src/workflow/nodes/variable-assign-node/index.ts new file mode 100644 index 000000000..567bf425a --- /dev/null +++ b/ui/src/workflow/nodes/variable-assign-node/index.ts @@ -0,0 +1,14 @@ +import VariableAssignNodeVue from './index.vue' +import { AppNode, AppNodeModel } from '@/workflow/common/app-node' + +class VariableAssignNode extends AppNode { + constructor(props: any) { + super(props, VariableAssignNodeVue) + } +} + +export default { + type: 'variable-assign-node', + model: AppNodeModel, + view: VariableAssignNode +} diff --git a/ui/src/workflow/nodes/variable-assign-node/index.vue b/ui/src/workflow/nodes/variable-assign-node/index.vue new file mode 100644 index 000000000..a786a9d77 --- /dev/null +++ b/ui/src/workflow/nodes/variable-assign-node/index.vue @@ -0,0 +1,267 @@ + + + diff --git a/ui/src/workflow/plugins/dagre.ts b/ui/src/workflow/plugins/dagre.ts new file mode 100644 index 000000000..67b72fdad --- /dev/null +++ b/ui/src/workflow/plugins/dagre.ts @@ -0,0 +1,69 @@ +import { DagreLayout, type DagreLayoutOptions } from '@antv/layout' + +export default class Dagre { + static pluginName = 'dagre' + lf: any + option: DagreLayoutOptions | any + render(lf: any) { + this.lf = lf + } + + /** + * option: { + * rankdir: "TB", // layout 方向, 可选 TB, BT, LR, RL + * align: undefined, // 节点对齐方式,可选 UL, UR, DL, DR + * nodeSize: undefined, // 节点大小 + * nodesepFunc: undefined, // 节点水平间距(px) + * ranksepFunc: undefined, // 每一层节点之间间距 + * nodesep: 40, // 节点水平间距(px) 注意:如果有grid,需要保证nodesep为grid的偶数倍 + * ranksep: 40, // 每一层节点之间间距 注意:如果有grid,需要保证ranksep为grid的偶数倍 + * controlPoints: false, // 是否保留布局连线的控制点 + * radial: false, // 是否基于 dagre 进行辐射布局 + * focusNode: null, // radial 为 true 时生效,关注的节点 + * }; + */ + layout(option = {}) { + const { nodes, edges, gridSize } = this.lf.graphModel + // 为了保证生成的节点在girdSize上,需要处理一下。 + let nodesep = 40 + let ranksep = 40 + if (gridSize > 20) { + nodesep = gridSize * 2 + ranksep = gridSize * 2 + } + this.option = { + type: 'dagre', + rankdir: 'LR', + // align: 'UL', + // align: 'UR', + align: 'DR', + nodesep, + ranksep, + begin: [120, 120], + ...option + } + const layoutInstance = new DagreLayout(this.option) + const layoutData = layoutInstance.layout({ + nodes: nodes.map((node: any) => ({ + id: node.id, + size: { + width: node.width, + height: node.height + }, + model: node + })), + edges: edges.map((edge: any) => ({ + source: edge.sourceNodeId, + target: edge.targetNodeId, + model: edge + })) + }) + + layoutData.nodes?.forEach((node: any) => { + // @ts-ignore: pass node data + const { model } = node + model.set_position({ x: node.x, y: node.y }) + }) + this.lf.fitView() + } +}