feat: resource authorization

This commit is contained in:
wangdan-fit2cloud 2025-06-03 16:08:49 +08:00
parent 3f565602a4
commit af8172a7b5
127 changed files with 17661 additions and 145 deletions

View File

@ -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<Result<any>> = (workspace_id) => {
return get(`${prefix}/${workspace_id}/user_list`)
}
/**
*
* @query
@ -40,4 +49,5 @@ const putResourceAuthorization: (workspace_id: String, body: any) => Promise<Res
export default {
getResourceAuthorization,
putResourceAuthorization,
getUserList
}

View File

@ -1,6 +1,6 @@
export enum AuthorizationEnum {
MANAGE = 'MANAGE',
USE = 'USE',
DATASET = 'DATASET',
KNOWLEDGE = 'KNOWLEDGE',
APPLICATION = 'APPLICATION'
}

View File

@ -0,0 +1,97 @@
export default {
noHistory: '暂无历史记录',
createChat: '新建对话',
history: '历史记录',
only20history: '仅显示最近 20 条对话',
question_count: '条提问',
exportRecords: '导出聊天记录',
chatId: '对话 ID',
userInput: '用户输入',
quote: '引用',
download: '点击下载文件',
transcribing: '转文字中',
passwordValidator: {
title: '请输入密码打开链接',
errorMessage1: '密码不能为空',
errorMessage2: '密码错误'
},
operation: {
play: '点击播放',
pause: '停止',
regeneration: '换个答案',
like: '赞同',
cancelLike: '取消赞同',
oppose: '反对',
cancelOppose: '取消反对',
continue: '继续',
stopChat: '停止回答',
startChat: '开始对话'
},
tip: {
error500Message: '抱歉,当前正在维护,无法提供服务,请稍后再试!',
errorIdentifyMessage: '无法识别用户身份',
errorLimitMessage: '抱歉,您的提问已达到最大限制,请明天再来吧!',
answerMessage: '抱歉,没有查找到相关内容,请重新描述您的问题或提供更多信息。',
stopAnswer: '已停止回答',
answerLoading: '回答中',
recorderTip: `<p>该功能需要使用麦克风,浏览器禁止不安全页面录音,解决方案如下:<br/>
1 https <br/>
2 https Chrome <br/>
(1) chrome://flags/#unsafely-treat-insecure-origin-as-secure<br/>
(2) http 例如: http://127.0.0.1:8080。</p>`,
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: '编辑标题'
}

View File

@ -0,0 +1,12 @@
export default {
quickCreatePlaceholder: '快速创建空白文档',
quickCreateName: '文档名称',
noData: '无匹配数据',
loading: '加载中',
noMore: '到底啦!',
selectParagraph: {
title: '选择分段',
error: '仅执行未成功分段',
all: '全部分段'
}
}

View File

@ -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,
}

View File

@ -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: '反对'
}
}
}

View File

@ -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: {}
}

View File

@ -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
}

View File

@ -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

View File

@ -10,6 +10,20 @@ export const routes: Array<RouteRecordRaw> = [
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',

View File

@ -409,3 +409,11 @@ h5 {
background: var(--el-color-primary);
}
}
/*
内容部分 自适应高度
*/
.main-calc-height {
height: var(--app-main-height);
box-sizing: border-box;
}

View File

@ -0,0 +1,153 @@
<template>
<el-dialog
title="API Key"
v-model="dialogVisible"
width="800"
:close-on-click-modal="false"
:close-on-press-escape="false"
align-center
>
<el-button type="primary" class="mb-16" @click="createApiKey">
{{ $t('common.create') }}
</el-button>
<el-table :data="apiKey" class="mb-16" :loading="loading" height="420">
<el-table-column prop="secret_key" label="API Key">
<template #default="{ row }">
<span class="vertical-middle lighter break-all">
{{ row.secret_key }}
</span>
<el-button type="primary" text @click="copyClick(row.secret_key)">
<AppIcon iconName="app-copy"></AppIcon>
</el-button>
</template>
</el-table-column>
<el-table-column :label="$t('common.status.label')" width="70">
<template #default="{ row }">
<div @click.stop>
<el-switch
size="small"
v-model="row.is_active"
:before-change="() => changeState(row)"
/>
</div>
</template>
</el-table-column>
<el-table-column prop="name" :label="$t('common.createDate')" width="170">
<template #default="{ row }">
{{ datetimeFormat(row.create_time) }}
</template>
</el-table-column>
<el-table-column :label="$t('common.operation')" align="left" width="90">
<template #default="{ row }">
<span class="mr-4">
<el-tooltip effect="dark" :content="$t('common.setting')" placement="top">
<el-button type="primary" text @click.stop="settingApiKey(row)">
<el-icon><Setting /></el-icon>
</el-button>
</el-tooltip>
</span>
<el-tooltip effect="dark" :content="$t('common.delete')" placement="top">
<el-button type="primary" text @click="deleteApiKey(row)">
<el-icon><Delete /></el-icon>
</el-button>
</el-tooltip>
</template>
</el-table-column>
</el-table>
<SettingAPIKeyDialog ref="SettingAPIKeyDialogRef" @refresh="refresh" />
</el-dialog>
</template>
<script setup lang="ts">
import { ref, watch } from 'vue'
import { useRoute } from 'vue-router'
import { copyClick } from '@/utils/clipboard'
import overviewApi from '@/api/application-overview'
import SettingAPIKeyDialog from './SettingAPIKeyDialog.vue'
import { datetimeFormat } from '@/utils/time'
import { MsgSuccess, MsgConfirm } from '@/utils/message'
import { t } from '@/locales'
const route = useRoute()
const {
params: { id }
} = route
const emit = defineEmits(['addData'])
const SettingAPIKeyDialogRef = ref()
const dialogVisible = ref<boolean>(false)
const loading = ref(false)
const apiKey = ref<any>(null)
watch(dialogVisible, (bool) => {
if (!bool) {
apiKey.value = null
}
})
function settingApiKey(row: any) {
SettingAPIKeyDialogRef.value.open(row, 'APPLICATION')
}
function deleteApiKey(row: any) {
MsgConfirm(
// @ts-ignore
`${t('views.applicationOverview.appInfo.APIKeyDialog.msgConfirm1')}: ${row.secret_key}?`,
t('views.applicationOverview.appInfo.APIKeyDialog.msgConfirm2'),
{
confirmButtonText: t('common.confirm'),
cancelButtonText: t('common.cancel'),
confirmButtonClass: 'danger'
}
)
.then(() => {
overviewApi.delAPIKey(id as string, row.id, loading).then(() => {
MsgSuccess(t('common.deleteSuccess'))
getApiKeyList()
})
})
.catch(() => {})
}
function changeState(row: any) {
const obj = {
is_active: !row.is_active
}
const str = obj.is_active
? t('views.applicationOverview.appInfo.APIKeyDialog.enabledSuccess')
: t('views.applicationOverview.appInfo.APIKeyDialog.disabledSuccess')
overviewApi
.putAPIKey(id as string, row.id, obj, loading)
.then((res) => {
MsgSuccess(str)
getApiKeyList()
return true
})
.catch(() => {
return false
})
}
function createApiKey() {
overviewApi.postAPIKey(id as string, loading).then((res) => {
getApiKeyList()
})
}
const open = () => {
getApiKeyList()
dialogVisible.value = true
}
function getApiKeyList() {
overviewApi.getAPIKey(id as string, loading).then((res) => {
apiKey.value = res.data
})
}
function refresh() {
getApiKeyList()
}
defineExpose({ open })
</script>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,103 @@
<template>
<el-dialog
:title="$t('views.applicationOverview.appInfo.SettingDisplayDialog.dialogTitle')"
v-model="dialogVisible"
:close-on-click-modal="false"
:close-on-press-escape="false"
width="550"
>
<el-form label-position="top" ref="displayFormRef" :model="form">
<el-form-item>
<span>{{
$t('views.applicationOverview.appInfo.SettingDisplayDialog.languageLabel')
}}</span>
<el-select v-model="form.language" clearable>
<el-option
v-for="item in langList"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
<el-form-item>
<el-space direction="vertical" alignment="start">
<el-checkbox
v-model="form.show_source"
:label="
isWorkFlow(detail.type)
? $t('views.applicationOverview.appInfo.SettingDisplayDialog.showExecutionDetail')
: $t('views.applicationOverview.appInfo.SettingDisplayDialog.showSourceLabel')
"
/>
</el-space>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click.prevent="dialogVisible = false">{{ $t('common.cancel') }} </el-button>
<el-button type="primary" @click="submit(displayFormRef)" :loading="loading">
{{ $t('common.save') }}
</el-button>
</span>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { ref, watch } from 'vue'
import { useRoute } from 'vue-router'
import type { FormInstance, FormRules, UploadFiles } from 'element-plus'
import applicationApi from '@/api/application'
import { isWorkFlow } from '@/utils/application'
import { MsgSuccess, MsgError } from '@/utils/message'
import { getBrowserLang, langList, t } from '@/locales'
const route = useRoute()
const {
params: { id }
} = route
const emit = defineEmits(['refresh'])
const displayFormRef = ref()
const form = ref<any>({
show_source: false,
language: ''
})
const detail = ref<any>(null)
const dialogVisible = ref<boolean>(false)
const loading = ref(false)
watch(dialogVisible, (bool) => {
if (!bool) {
form.value = {
show_source: false,
language: ''
}
}
})
const open = (data: any, content: any) => {
detail.value = content
form.value.show_source = data.show_source
form.value.language = data.language
dialogVisible.value = true
}
const submit = async (formEl: FormInstance | undefined) => {
if (!formEl) return
await formEl.validate((valid, fields) => {
if (valid) {
applicationApi.putAccessToken(id as string, form.value, loading).then((res) => {
emit('refresh')
// @ts-ignore
MsgSuccess(t('common.settingSuccess'))
dialogVisible.value = false
})
}
})
}
defineExpose({ open })
</script>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,137 @@
<template>
<el-dialog
:title="$t('views.applicationOverview.appInfo.EditAvatarDialog.title')"
v-model="dialogVisible"
:close-on-click-modal="false"
:close-on-press-escape="false"
width="550"
>
<el-radio-group v-model="radioType" class="radio-block mb-16">
<el-radio value="default">
<p>{{ $t('views.applicationOverview.appInfo.EditAvatarDialog.default') }}</p>
<AppAvatar
v-if="detail?.name"
:name="detail?.name"
pinyinColor
class="mt-8 mb-8"
shape="square"
:size="32"
/>
</el-radio>
<el-radio value="custom">
<p>{{ $t('views.applicationOverview.appInfo.EditAvatarDialog.customizeUpload') }}</p>
<div class="flex mt-8">
<AppAvatar
v-if="fileURL"
shape="square"
:size="32"
style="background: none"
class="mr-16"
>
<img :src="fileURL" alt="" />
</AppAvatar>
<el-upload
ref="uploadRef"
action="#"
:auto-upload="false"
:show-file-list="false"
accept="image/jpeg, image/png, image/gif"
:on-change="onChange"
>
<el-button icon="Upload" :disabled="radioType !== 'custom'">{{
$t('views.applicationOverview.appInfo.EditAvatarDialog.upload')
}}</el-button>
</el-upload>
</div>
<div class="el-upload__tip info mt-8">
{{ $t('views.applicationOverview.appInfo.EditAvatarDialog.sizeTip') }}
</div>
</el-radio>
</el-radio-group>
<template #footer>
<span class="dialog-footer">
<el-button @click.prevent="dialogVisible = false"> {{ $t('common.cancel') }}</el-button>
<el-button type="primary" @click="submit" :loading="loading">
{{ $t('common.save') }}</el-button
>
</span>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { ref, watch } from 'vue'
import { useRoute } from 'vue-router'
import overviewApi from '@/api/application-overview'
import { cloneDeep } from 'lodash'
import { MsgSuccess, MsgError } from '@/utils/message'
import { defaultIcon, isAppIcon } from '@/utils/common'
import useStore from '@/stores'
import { t } from '@/locales'
const { application } = useStore()
const route = useRoute()
const {
params: { id } //id
} = route
const emit = defineEmits(['refresh'])
const iconFile = ref<any>(null)
const fileURL = ref<any>(null)
const dialogVisible = ref<boolean>(false)
const loading = ref(false)
const detail = ref<any>(null)
const radioType = ref('default')
watch(dialogVisible, (bool) => {
if (!bool) {
iconFile.value = null
fileURL.value = null
}
})
const open = (data: any) => {
radioType.value = isAppIcon(data.icon) ? 'custom' : 'default'
fileURL.value = isAppIcon(data.icon) ? data.icon : null
detail.value = cloneDeep(data)
dialogVisible.value = true
}
const onChange = (file: any) => {
//110MB
const isLimit = file?.size / 1024 / 1024 < 10
if (!isLimit) {
// @ts-ignore
MsgError(t('views.applicationOverview.appInfo.EditAvatarDialog.fileSizeExceeded'))
return false
} else {
iconFile.value = file
fileURL.value = URL.createObjectURL(file.raw)
}
}
function submit() {
if (radioType.value === 'default') {
application.asyncPutApplication(id as string, { icon: defaultIcon }, loading).then((res) => {
emit('refresh')
MsgSuccess(t('common.saveSuccess'))
dialogVisible.value = false
})
} else if (radioType.value === 'custom' && iconFile.value) {
let fd = new FormData()
fd.append('file', iconFile.value.raw)
overviewApi.putAppIcon(id as string, fd, loading).then((res: any) => {
emit('refresh')
MsgSuccess(t('common.saveSuccess'))
dialogVisible.value = false
})
} else {
MsgError(t('views.applicationOverview.appInfo.EditAvatarDialog.uploadImagePrompt'))
}
}
defineExpose({ open })
</script>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,163 @@
<template>
<el-dialog
:title="$t('views.applicationOverview.appInfo.embedInWebsite')"
v-model="dialogVisible"
width="900"
class="embed-dialog"
:close-on-click-modal="false"
:close-on-press-escape="false"
>
<el-row :gutter="12">
<el-col :span="8">
<div class="border">
<p class="title p-16 bold">
{{ $t('views.applicationOverview.appInfo.EmbedDialog.fullscreenModeTitle') }}
</p>
<img src="@/assets/window1.png" alt="" class="ml-8" height="150" />
<div class="code layout-bg border-t p-8">
<div class="flex-between p-8">
<span class="bold">{{
$t('views.applicationOverview.appInfo.EmbedDialog.copyInstructions')
}}</span>
<el-button text @click="copyClick(source1)">
<AppIcon iconName="app-copy"></AppIcon>
</el-button>
</div>
<el-scrollbar height="150" always>
<div class="pre-wrap p-8 pt-0">
{{ source1 }}
</div>
</el-scrollbar>
</div>
</div>
</el-col>
<el-col :span="8">
<div class="border">
<p class="title p-16 bold">
{{ $t('views.applicationOverview.appInfo.EmbedDialog.mobileModeTitle') }}
</p>
<img src="@/assets/window3.png" alt="" class="ml-8" height="150" />
<div class="code layout-bg border-t p-8">
<div class="flex-between p-8">
<span class="bold">{{
$t('views.applicationOverview.appInfo.EmbedDialog.copyInstructions')
}}</span>
<el-button text @click="copyClick(source3)">
<AppIcon iconName="app-copy"></AppIcon>
</el-button>
</div>
<el-scrollbar height="150" always>
<div class="pre-wrap p-8 pt-0">
{{ source3 }}
</div>
</el-scrollbar>
</div>
</div>
</el-col>
<el-col :span="8">
<div class="border">
<p class="title p-16 bold">
{{ $t('views.applicationOverview.appInfo.EmbedDialog.floatingModeTitle') }}
</p>
<img src="@/assets/window2.png" alt="" class="ml-8" height="150" />
<div class="code layout-bg border-t p-8">
<div class="flex-between p-8">
<span class="bold">{{
$t('views.applicationOverview.appInfo.EmbedDialog.copyInstructions')
}}</span>
<el-button text @click="copyClick(source2)">
<AppIcon iconName="app-copy"></AppIcon>
</el-button>
</div>
<el-scrollbar height="150" always>
<div class="pre-wrap p-8 pt-0">
{{ source2 }}
</div>
</el-scrollbar>
</div>
</div>
</el-col>
</el-row>
</el-dialog>
</template>
<script setup lang="ts">
import { computed, ref, watch } from 'vue'
import { copyClick } from '@/utils/clipboard'
import useStore from '@/stores'
const { application } = useStore()
const props = defineProps({
data: Object,
apiInputParams: String
})
const emit = defineEmits(['addData'])
const dialogVisible = ref<boolean>(false)
const source1 = ref('')
const source2 = ref('')
const source3 = ref('')
const urlParams1 = computed(() => (props.apiInputParams ? '?' + props.apiInputParams : ''))
const urlParams2 = computed(() => (props.apiInputParams ? '&' + props.apiInputParams : ''))
const urlParams3 = computed(() =>
props.apiInputParams ? '?mode=mobile&' + props.apiInputParams : '?mode=mobile'
)
watch(dialogVisible, (bool) => {
if (!bool) {
source1.value = ''
source2.value = ''
source3.value = ''
}
})
const open = (val: string) => {
source1.value = `<iframe
src="${application.location + val + urlParams1.value}"
style="width: 100%; height: 100%;"
frameborder="0"
allow="microphone">
</iframe>
`
source2.value = `<script
async
defer
src="${window.location.origin}/api/application/embed?protocol=${window.location.protocol.replace(
':',
''
)}&host=${window.location.host}&token=${val}${urlParams2.value}">
<\/script>
`
source3.value = `<iframe
src="${application.location + val + urlParams3.value}"
style="width: 100%; height: 100%;"
frameborder="0"
allow="microphone">
</iframe>
`
dialogVisible.value = true
}
defineExpose({ open })
</script>
<style lang="scss" scoped>
.embed-dialog {
.title {
color: var(--app-text-color) !important;
}
.code {
color: var(--app-text-color) !important;
font-weight: 400;
font-size: 13px;
white-space: pre;
height: 188px;
}
}
</style>

View File

@ -0,0 +1,196 @@
<template>
<el-dialog
:title="$t('views.applicationOverview.appInfo.accessControl')"
v-model="dialogVisible"
:close-on-click-modal="false"
:close-on-press-escape="false"
width="650"
>
<el-form label-position="top" ref="limitFormRef" :model="form">
<!-- <el-form-item
:label="$t('views.applicationOverview.appInfo.LimitDialog.showSourceLabel')"
@click.prevent
>
<el-switch size="small" v-model="form.show_source"></el-switch>
</el-form-item> -->
<el-form-item
:label="$t('views.applicationOverview.appInfo.LimitDialog.clientQueryLimitLabel')"
>
<el-input-number
v-model="form.access_num"
:min="0"
:step="1"
:max="10000"
:value-on-clear="0"
controls-position="right"
style="width: 268px"
step-strictly
/>
<span class="ml-4">{{
$t('views.applicationOverview.appInfo.LimitDialog.timesDays')
}}</span>
</el-form-item>
<!-- 身份验证 -->
<el-form-item
:label="$t('views.applicationOverview.appInfo.LimitDialog.authentication')"
v-hasPermission="new ComplexPermission([], ['x-pack'], 'OR')"
>
<el-switch size="small" v-model="form.authentication" @change="firstGeneration"></el-switch>
</el-form-item>
<el-form-item
prop="authentication_value"
v-if="form.authentication"
:label="$t('views.applicationOverview.appInfo.LimitDialog.authenticationValue')"
v-hasPermission="new ComplexPermission([], ['x-pack'], 'OR')"
>
<el-input
class="authentication-append-input"
v-model="form.authentication_value"
readonly
style="width: 268px"
disabled
>
<template #append>
<el-tooltip :content="$t('common.copy')" placement="top">
<el-button
type="primary"
text
@click="copyClick(form.authentication_value)"
style="margin: 0 4px !important"
>
<AppIcon iconName="app-copy"></AppIcon>
</el-button>
</el-tooltip>
<el-tooltip :content="$t('common.refresh')" placement="top">
<el-button
@click="refreshAuthentication"
type="primary"
text
style="margin: 0 4px 0 0 !important"
>
<el-icon><RefreshRight /></el-icon>
</el-button>
</el-tooltip>
</template>
</el-input>
</el-form-item>
<el-form-item
:label="$t('views.applicationOverview.appInfo.LimitDialog.whitelistLabel')"
@click.prevent
>
<el-switch size="small" v-model="form.white_active"></el-switch>
</el-form-item>
<el-form-item>
<el-input
v-model="form.white_list"
:placeholder="$t('views.applicationOverview.appInfo.LimitDialog.whitelistPlaceholder')"
:rows="10"
type="textarea"
/>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click.prevent="dialogVisible = false">{{ $t('common.cancel') }} </el-button>
<el-button type="primary" @click="submit(limitFormRef)" :loading="loading">
{{ $t('common.save') }}
</el-button>
</span>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { ref, watch } from 'vue'
import { useRoute } from 'vue-router'
import type { FormInstance, FormRules } from 'element-plus'
import applicationApi from '@/api/application'
import { MsgSuccess } from '@/utils/message'
import { t } from '@/locales'
import { copyClick } from '@/utils/clipboard'
import { ComplexPermission } from '@/utils/permission/type'
const route = useRoute()
const {
params: { id }
} = route
const emit = defineEmits(['refresh'])
const limitFormRef = ref()
const form = ref<any>({
access_num: 0,
white_active: true,
white_list: '',
authentication_value: '',
authentication: false
})
const dialogVisible = ref<boolean>(false)
const loading = ref(false)
watch(dialogVisible, (bool) => {
if (!bool) {
form.value = {
access_num: 0,
white_active: true,
white_list: ''
}
}
})
const open = (data: any) => {
form.value.access_num = data.access_num
form.value.white_active = data.white_active
form.value.white_list = data.white_list?.length ? data.white_list?.join('\n') : ''
form.value.authentication_value = data.authentication_value
form.value.authentication = data.authentication
dialogVisible.value = true
}
const submit = async (formEl: FormInstance | undefined) => {
if (!formEl) return
await formEl.validate((valid, fields) => {
if (valid) {
const obj = {
white_list: form.value.white_list ? form.value.white_list.split('\n') : [],
white_active: form.value.white_active,
access_num: form.value.access_num,
authentication: form.value.authentication,
authentication_value: form.value.authentication_value
}
applicationApi.putAccessToken(id as string, obj, loading).then((res) => {
emit('refresh')
// @ts-ignore
MsgSuccess(t('common.settingSuccess'))
dialogVisible.value = false
})
}
})
}
function generateAuthenticationValue(length: number = 10) {
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
const randomValues = new Uint8Array(length)
window.crypto.getRandomValues(randomValues)
return Array.from(randomValues)
.map((value) => chars[value % chars.length])
.join('')
}
function refreshAuthentication() {
form.value.authentication_value = generateAuthenticationValue()
}
function firstGeneration() {
if (form.value.authentication && !form.value.authentication_value) {
form.value.authentication_value = generateAuthenticationValue()
}
}
defineExpose({ open })
</script>
<style lang="scss" scoped>
.authentication-append-input {
.el-input-group__append {
padding: 0 !important;
}
}
</style>

View File

@ -0,0 +1,113 @@
<template>
<el-dialog
:title="$t('common.setting')"
v-model="dialogVisible"
:close-on-click-modal="false"
:close-on-press-escape="false"
>
<el-form label-position="top" ref="settingFormRef" :model="form">
<el-form-item
:label="$t('views.applicationOverview.appInfo.SettingAPIKeyDialog.allowCrossDomainLabel')"
@click.prevent
>
<el-switch size="small" v-model="form.allow_cross_domain"></el-switch>
</el-form-item>
<el-form-item>
<el-input
v-model="form.cross_domain_list"
:placeholder="
$t('views.applicationOverview.appInfo.SettingAPIKeyDialog.crossDomainPlaceholder')
"
:rows="10"
type="textarea"
/>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click.prevent="dialogVisible = false">{{ $t('common.cancel') }}</el-button>
<el-button type="primary" @click="submit(settingFormRef)" :loading="loading">
{{ $t('common.save') }}
</el-button>
</span>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { ref, watch } from 'vue'
import { useRoute } from 'vue-router'
import type { FormInstance, FormRules } from 'element-plus'
import overviewApi from '@/api/application-overview'
import overviewSystemApi from '@/api/system-api-key'
import { MsgSuccess } from '@/utils/message'
import { t } from '@/locales'
const route = useRoute()
const {
params: { id }
} = route
const emit = defineEmits(['refresh'])
const settingFormRef = ref()
const form = ref<any>({
allow_cross_domain: false,
cross_domain_list: ''
})
const dialogVisible = ref<boolean>(false)
const loading = ref(false)
const APIKeyId = ref('')
const APIType = ref('APPLICATION')
watch(dialogVisible, (bool) => {
if (!bool) {
form.value = {
allow_cross_domain: false,
cross_domain_list: ''
}
}
})
const open = (data: any, type: string) => {
APIKeyId.value = data.id
APIType.value = type
form.value.allow_cross_domain = data.allow_cross_domain
form.value.cross_domain_list = data.cross_domain_list?.length
? data.cross_domain_list?.join('\n')
: ''
dialogVisible.value = true
}
const submit = async (formEl: FormInstance | undefined) => {
if (!formEl) return
await formEl.validate((valid, fields) => {
if (valid) {
const obj = {
allow_cross_domain: form.value.allow_cross_domain,
cross_domain_list: form.value.cross_domain_list
? form.value.cross_domain_list.split('\n').filter(function (item: string) {
return item !== ''
})
: []
}
const apiCall =
APIType.value === 'APPLICATION'
? overviewApi.putAPIKey(id as string, APIKeyId.value, obj, loading)
: overviewSystemApi.putAPIKey(APIKeyId.value, obj, loading)
apiCall.then((res) => {
emit('refresh')
//@ts-ignore
MsgSuccess(t('common.settingSuccess'))
dialogVisible.value = false
})
}
})
}
defineExpose({ open })
</script>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,163 @@
<template>
<el-row :gutter="16">
<el-col
:xs="12"
:sm="12"
:md="12"
:lg="6"
:xl="6"
v-for="(item, index) in statisticsType"
:key="index"
class="mb-16"
>
<el-card shadow="never">
<div class="flex align-center ml-8 mr-8">
<el-avatar :size="40" shape="square" :style="{ background: item.background }">
<appIcon :iconName="item.icon" :style="{ fontSize: '24px', color: item.color }" />
</el-avatar>
<div class="ml-12">
<p class="color-secondary lighter mb-4">{{ item.name }}</p>
<div v-if="item.id !== 'starCharts'" class="flex align-baseline">
<h2>{{ numberFormat(item.sum?.[0]) }}</h2>
<span v-if="item.sum.length > 1" class="ml-12" style="color: #f54a45"
>+{{ numberFormat(item.sum?.[1]) }}</span
>
</div>
<div v-else class="flex align-center mr-8">
<AppIcon iconName="app-like-color"></AppIcon>
<h2 class="ml-4">{{ item.sum?.[0] }}</h2>
<AppIcon class="ml-12" iconName="app-oppose-color"></AppIcon>
<h2 class="ml-4">{{ item.sum?.[1] }}</h2>
</div>
</div>
</div>
</el-card>
</el-col>
</el-row>
<el-row :gutter="16">
<el-col
:xs="24"
:sm="24"
:md="24"
:lg="12"
:xl="12"
v-for="(item, index) in statisticsType"
:key="index"
class="mb-16"
>
<el-card shadow="never">
<div class="p-8">
<AppCharts height="316px" :id="item.id" type="line" :option="item.option" />
</div>
</el-card>
</el-col>
</el-row>
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import AppCharts from '@/components/app-charts/index.vue'
import { getAttrsArray, getSum, numberFormat } from '@/utils/utils'
import { t } from '@/locales'
const props = defineProps({
data: {
type: Array,
default: () => []
}
})
const statisticsType = computed(() => [
{
id: 'customerCharts',
// @ts-ignore
name: t('views.applicationOverview.monitor.charts.customerTotal'),
icon: 'app-user',
background: '#EBF1FF',
color: '#3370FF',
sum: [
getSum(getAttrsArray(props.data, 'customer_num') || 0),
getSum(getAttrsArray(props.data, 'customer_added_count') || 0)
],
option: {
title: t('views.applicationOverview.monitor.charts.customerTotal'),
xData: getAttrsArray(props.data, 'day'),
yData: [
{
name: t('views.applicationOverview.monitor.charts.customerTotal'),
type: 'line',
area: true,
data: getAttrsArray(props.data, 'customer_num')
},
{
name: t('views.applicationOverview.monitor.charts.customerNew'),
type: 'line',
area: true,
data: getAttrsArray(props.data, 'customer_added_count')
}
]
}
},
{
id: 'chatRecordCharts',
name: t('views.applicationOverview.monitor.charts.queryCount'),
icon: 'app-question',
background: '#FFF3E5',
color: '#FF8800',
sum: [getSum(getAttrsArray(props.data, 'chat_record_count') || 0)],
option: {
title: t('views.applicationOverview.monitor.charts.queryCount'),
xData: getAttrsArray(props.data, 'day'),
yData: [
{
type: 'line',
data: getAttrsArray(props.data, 'chat_record_count')
}
]
}
},
{
id: 'tokensCharts',
name: t('views.applicationOverview.monitor.charts.tokensTotal'),
icon: 'app-tokens',
background: '#E5FBF8',
color: '#00D6B9',
sum: [getSum(getAttrsArray(props.data, 'tokens_num') || 0)],
option: {
title: t('views.applicationOverview.monitor.charts.tokensTotal'),
xData: getAttrsArray(props.data, 'day'),
yData: [
{
type: 'line',
data: getAttrsArray(props.data, 'tokens_num')
}
]
}
},
{
id: 'starCharts',
name: t('views.applicationOverview.monitor.charts.userSatisfaction'),
icon: 'app-user-stars',
background: '#FEEDEC',
color: '#F54A45',
sum: [
getSum(getAttrsArray(props.data, 'star_num') || 0),
getSum(getAttrsArray(props.data, 'trample_num') || 0)
],
option: {
title: t('views.applicationOverview.monitor.charts.userSatisfaction'),
xData: getAttrsArray(props.data, 'day'),
yData: [
{
name: t('views.applicationOverview.monitor.charts.approval'),
type: 'line',
data: getAttrsArray(props.data, 'star_num')
},
{
name: t('views.applicationOverview.monitor.charts.disapproval'),
type: 'line',
data: getAttrsArray(props.data, 'trample_num')
}
]
}
}
])
</script>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,639 @@
<template>
<el-dialog
:title="$t('views.applicationOverview.appInfo.SettingDisplayDialog.dialogTitle')"
width="900"
v-model="dialogVisible"
:close-on-click-modal="false"
:close-on-press-escape="false"
:destroy-on-close="true"
align-center
class="display-setting-dialog"
>
<template #header="{ titleId, titleClass }">
<div class="flex-between mb-8">
<h4 :id="titleId" :class="titleClass">
{{ $t('views.applicationOverview.appInfo.SettingDisplayDialog.dialogTitle') }}
</h4>
<div class="flex align-center">
<el-button type="primary" @click.prevent="resetForm" link>
<el-icon class="mr-4">
<Refresh />
</el-icon>
{{ $t('views.applicationOverview.appInfo.SettingDisplayDialog.restoreDefault') }}
</el-button>
<el-divider direction="vertical" />
</div>
</div>
</template>
<div class="flex">
<div class="setting-preview border border-r-4 mr-16" style="min-width: 400px">
<div class="setting-preview-container">
<div class="setting-preview-header" :style="customStyle">
<div class="flex-between">
<div class="flex align-center">
<div class="mr-12 ml-24 flex">
<AppAvatar
v-if="isAppIcon(detail?.icon)"
shape="square"
:size="32"
style="background: none"
>
<img :src="detail?.icon" alt="" />
</AppAvatar>
<AppAvatar
v-else-if="detail?.name"
:name="detail?.name"
pinyinColor
shape="square"
:size="32"
/>
</div>
<h4 class="ellipsis">
{{ detail?.name || $t('views.application.applicationForm.form.appName.label') }}
</h4>
</div>
<div class="mr-16">
<el-button link>
<AppIcon
:iconName="'app-magnify'"
:style="{
color: xpackForm.custom_theme?.header_font_color
}"
style="font-size: 20px"
></AppIcon>
</el-button>
<el-button link>
<el-icon
:size="20"
class="color-secondary"
:style="{
color: xpackForm.custom_theme?.header_font_color
}"
>
<Close />
</el-icon>
</el-button>
</div>
</div>
</div>
<div>
<div class="p-16" style="position: relative">
<div class="flex">
<div class="avatar" v-if="xpackForm.show_avatar">
<el-image
v-if="imgUrl.avatar"
:src="imgUrl.avatar"
alt=""
fit="cover"
style="width: 28px; height: 28px; display: block"
/>
<LogoIcon
v-else
height="28px"
style="width: 28px; height: 28px; display: block"
/>
</div>
<img
src="@/assets/display-bg2.png"
alt=""
:width="
xpackForm.show_avatar
? xpackForm.show_user_avatar
? '232px'
: '270px'
: xpackForm.show_user_avatar
? '260px'
: '300px'
"
/>
</div>
<div class="flex mt-4" style="justify-content: flex-end">
<img
src="@/assets/display-bg3.png"
alt=""
:width="
xpackForm.show_user_avatar
? xpackForm.show_avatar
? '227px'
: '255px'
: xpackForm.show_avatar
? '265px'
: '292px'
"
style="object-fit: contain"
/>
<div class="avatar ml-8" v-if="xpackForm.show_user_avatar">
<el-image
v-if="imgUrl.user_avatar"
:src="imgUrl.user_avatar"
alt=""
fit="cover"
style="width: 28px; height: 28px; display: block"
/>
<AppAvatar v-else>
<img src="@/assets/user-icon.svg" style="width: 54%" alt="" />
</AppAvatar>
</div>
</div>
</div>
<div
style="position: absolute; bottom: 0; padding-bottom: 8px; box-sizing: border-box"
class="p-16 text-center w-full"
>
<img src="@/assets/display-bg1.png" alt="" class="w-full" />
<el-text type="info" v-if="xpackForm.disclaimer" class="mt-8" style="font-size: 12px">
<auto-tooltip :content="xpackForm.disclaimer_value">
{{ xpackForm.disclaimer_value }}
</auto-tooltip>
</el-text>
</div>
</div>
</div>
<div class="float_icon">
<el-image
v-if="imgUrl.float_icon"
:src="imgUrl.float_icon"
alt=""
fit="cover"
style="width: 40px; height: 40px; display: block"
/>
<img
v-else
src="/MaxKB.gif"
height="50px"
style="width: 40px; height: 40px; display: block"
/>
</div>
</div>
<el-form ref="displayFormRef" :model="xpackForm">
<el-row class="w-full mb-8">
<el-col :span="12">
<h5 class="mb-8">
{{ $t('views.applicationOverview.appInfo.SettingDisplayDialog.customThemeColor') }}
</h5>
<div>
<el-color-picker v-model="xpackForm.custom_theme.theme_color" />
{{
!xpackForm.custom_theme.theme_color
? $t('views.applicationOverview.appInfo.SettingDisplayDialog.default')
: ''
}}
</div>
</el-col>
<el-col :span="12">
<h5 class="mb-8">
{{
$t('views.applicationOverview.appInfo.SettingDisplayDialog.headerTitleFontColor')
}}
</h5>
<el-color-picker v-model="xpackForm.custom_theme.header_font_color" />
</el-col>
</el-row>
<el-row class="w-full mb-8">
<h5 class="mb-8">
{{ $t('views.applicationOverview.appInfo.SettingDisplayDialog.languageLabel') }}
</h5>
<el-select v-model="xpackForm.language" clearable>
<el-option
v-for="item in langList"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-row>
<el-card shadow="never" class="mb-8">
<div class="flex-between mb-8">
<span class="lighter">{{
$t('views.applicationOverview.appInfo.SettingDisplayDialog.AIAvatar')
}}</span>
<span class="flex align-center">
<el-checkbox v-model="xpackForm.show_avatar">{{
$t('views.applicationOverview.appInfo.SettingDisplayDialog.display')
}}</el-checkbox>
<el-upload
class="ml-8"
ref="uploadRef"
action="#"
:auto-upload="false"
:show-file-list="false"
accept="image/jpeg, image/png, image/gif"
:on-change="(file: any, fileList: any) => onChange(file, fileList, 'avatar')"
>
<el-button size="small">
{{ $t('views.applicationOverview.appInfo.SettingDisplayDialog.replace') }}
</el-button>
</el-upload>
</span>
</div>
<el-text type="info" size="small">
{{ $t('views.applicationOverview.appInfo.SettingDisplayDialog.imageMessage') }}
</el-text>
</el-card>
<el-card shadow="never" class="mb-8">
<div class="flex-between mb-8">
<span class="lighter">{{
$t('views.applicationOverview.appInfo.SettingDisplayDialog.askUserAvatar')
}}</span>
<span class="flex align-center">
<el-checkbox v-model="xpackForm.show_user_avatar">
{{
$t('views.applicationOverview.appInfo.SettingDisplayDialog.display')
}}</el-checkbox
>
<el-upload
class="ml-8"
ref="uploadRef"
action="#"
:auto-upload="false"
:show-file-list="false"
accept="image/jpeg, image/png, image/gif"
:on-change="(file: any, fileList: any) => onChange(file, fileList, 'user_avatar')"
>
<el-button size="small">
{{ $t('views.applicationOverview.appInfo.SettingDisplayDialog.replace') }}
</el-button>
</el-upload>
</span>
</div>
<el-text type="info" size="small"
>{{ $t('views.applicationOverview.appInfo.SettingDisplayDialog.imageMessage') }}
</el-text>
</el-card>
<el-card shadow="never" class="mb-8">
<div class="flex-between mb-8">
<span class="lighter">{{
$t('views.applicationOverview.appInfo.SettingDisplayDialog.floatIcon')
}}</span>
<el-upload
ref="uploadRef"
action="#"
:auto-upload="false"
:show-file-list="false"
accept="image/jpeg, image/png, image/gif"
:on-change="(file: any, fileList: any) => onChange(file, fileList, 'float_icon')"
>
<el-button size="small">
{{ $t('views.applicationOverview.appInfo.SettingDisplayDialog.replace') }}
</el-button>
</el-upload>
</div>
<el-text type="info" size="small">
{{ $t('views.applicationOverview.appInfo.SettingDisplayDialog.imageMessage') }}
</el-text>
<div class="border-t mt-8">
<div class="flex-between mb-8">
<span class="lighter">{{
$t('views.applicationOverview.appInfo.SettingDisplayDialog.iconDefaultPosition')
}}</span>
<el-checkbox
v-model="xpackForm.draggable"
:label="
$t('views.applicationOverview.appInfo.SettingDisplayDialog.draggablePosition')
"
/>
</div>
<el-row :gutter="8" class="w-full mb-8">
<el-col :span="12">
<div class="flex align-center">
<el-select v-model="xpackForm.float_location.x.type" style="width: 80px">
<el-option
:label="
$t(
'views.applicationOverview.appInfo.SettingDisplayDialog.iconPosition.left'
)
"
value="left"
/>
<el-option
:label="
$t(
'views.applicationOverview.appInfo.SettingDisplayDialog.iconPosition.right'
)
"
value="right"
/>
</el-select>
<el-input-number
v-model="xpackForm.float_location.x.value"
:min="0"
:step="1"
:precision="0"
:value-on-clear="0"
step-strictly
controls-position="right"
/>
<span class="ml-4">px</span>
</div>
</el-col>
<el-col :span="12">
<div class="flex align-center">
<el-select v-model="xpackForm.float_location.y.type" style="width: 80px">
<el-option
:label="
$t(
'views.applicationOverview.appInfo.SettingDisplayDialog.iconPosition.top'
)
"
value="top"
/>
<el-option
:label="
$t(
'views.applicationOverview.appInfo.SettingDisplayDialog.iconPosition.bottom'
)
"
value="bottom"
/>
</el-select>
<el-input-number
v-model="xpackForm.float_location.y.value"
:min="0"
:step="1"
:precision="0"
:value-on-clear="0"
step-strictly
controls-position="right"
/>
<span class="ml-4">px</span>
</div>
</el-col>
</el-row>
</div>
</el-card>
<el-space direction="vertical" alignment="start" :size="2">
<el-checkbox
v-model="xpackForm.show_source"
:label="
isWorkFlow(detail.type)
? $t('views.applicationOverview.appInfo.SettingDisplayDialog.showExecutionDetail')
: $t('views.applicationOverview.appInfo.SettingDisplayDialog.showSourceLabel')
"
/>
<el-checkbox
v-model="xpackForm.show_history"
:label="$t('views.applicationOverview.appInfo.SettingDisplayDialog.showHistory')"
/>
<el-checkbox
v-model="xpackForm.show_guide"
:label="$t('views.applicationOverview.appInfo.SettingDisplayDialog.displayGuide')"
/>
<el-checkbox
v-model="xpackForm.disclaimer"
:label="$t('views.applicationOverview.appInfo.SettingDisplayDialog.disclaimer')"
@change="changeDisclaimer"
/>
<span v-if="xpackForm.disclaimer"
><el-tooltip :content="xpackForm.disclaimer_value" placement="top">
<el-input
v-model="xpackForm.disclaimer_value"
style="width: 422px; margin-bottom: 10px"
@change="changeValue"
:maxlength="128"
/> </el-tooltip
></span>
</el-space>
</el-form>
</div>
<template #footer>
<span class="dialog-footer">
<el-button @click.prevent="dialogVisible = false">{{ $t('common.cancel') }} </el-button>
<el-button type="primary" @click="submit(displayFormRef)" :loading="loading">
{{ $t('common.save') }}
</el-button>
</span>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { computed, ref, watch } from 'vue'
import { useRoute } from 'vue-router'
import type { FormInstance, FormRules, UploadFiles } from 'element-plus'
import { isWorkFlow } from '@/utils/application'
import { isAppIcon } from '@/utils/common'
import applicationXpackApi from '@/api/application-xpack'
import { MsgSuccess, MsgError } from '@/utils/message'
import { langList, t } from '@/locales'
import useStore from '@/stores'
import { cloneDeep } from 'lodash'
const { user } = useStore()
const route = useRoute()
const {
params: { id }
} = route
const emit = defineEmits(['refresh'])
const defaultSetting = {
show_source: false,
language: '',
show_history: true,
draggable: true,
show_guide: true,
avatar: '',
avatar_url: '',
float_icon: '',
float_icon_url: '',
user_avatar: '',
user_avatar_url: '',
disclaimer: false,
disclaimer_value: t('views.applicationOverview.appInfo.SettingDisplayDialog.disclaimerValue'),
custom_theme: {
theme_color: '',
header_font_color: '#1f2329'
},
float_location: {
y: { type: 'bottom', value: 30 },
x: { type: 'right', value: 0 }
},
show_avatar: true,
show_user_avatar: false
}
const displayFormRef = ref()
const xpackForm = ref<any>({
show_source: false,
language: '',
show_history: false,
draggable: false,
show_guide: false,
avatar: '',
avatar_url: '',
float_icon: '',
float_icon_url: '',
user_avatar: '',
user_avatar_url: '',
disclaimer: false,
disclaimer_value: t('views.applicationOverview.appInfo.SettingDisplayDialog.disclaimerValue'),
custom_theme: {
theme_color: '',
header_font_color: '#1f2329'
},
float_location: {
y: { type: 'bottom', value: 30 },
x: { type: 'right', value: 0 }
},
show_avatar: true,
show_user_avatar: false
})
const imgUrl = ref<any>({
avatar: '',
float_icon: '',
user_avatar: ''
})
const dialogVisible = ref<boolean>(false)
const loading = ref(false)
const detail = ref<any>(null)
const customStyle = computed(() => {
return {
background: xpackForm.value.custom_theme?.theme_color,
color: xpackForm.value.custom_theme?.header_font_color
}
})
function resetForm() {
xpackForm.value = cloneDeep(defaultSetting)
imgUrl.value = {
avatar: '',
float_icon: '',
user_avatar: ''
}
}
const onChange = (file: any, fileList: UploadFiles, attr: string) => {
//1 10 MB
const isLimit = file?.size / 1024 / 1024 < 10
if (!isLimit) {
// @ts-ignore
MsgError(t('views.applicationOverview.appInfo.EditAvatarDialog.fileSizeExceeded'))
return false
} else {
xpackForm.value[attr] = file.raw
imgUrl.value[attr] = URL.createObjectURL(file.raw)
xpackForm.value[`${attr}_url`] = ''
}
}
const open = (data: any, content: any) => {
detail.value = content
xpackForm.value.show_source = data.show_source
xpackForm.value.show_history = data.show_history
xpackForm.value.language = data.language
xpackForm.value.draggable = data.draggable
xpackForm.value.show_guide = data.show_guide
imgUrl.value.avatar = data.avatar
imgUrl.value.float_icon = data.float_icon
imgUrl.value.user_avatar = data.user_avatar
xpackForm.value.disclaimer = data.disclaimer
xpackForm.value.disclaimer_value = data.disclaimer_value
if (
xpackForm.value.disclaimer_value ===
t('views.applicationOverview.appInfo.SettingDisplayDialog.disclaimerValue')
) {
xpackForm.value.disclaimer_value = t(
'views.applicationOverview.appInfo.SettingDisplayDialog.disclaimerValue'
)
}
xpackForm.value.avatar_url = data.avatar
xpackForm.value.user_avatar_url = data.user_avatar
xpackForm.value.float_icon_url = data.float_icon
xpackForm.value.show_avatar = data.show_avatar
xpackForm.value.show_user_avatar = data.show_user_avatar
xpackForm.value.custom_theme = {
theme_color: data.custom_theme?.theme_color || '',
header_font_color: data.custom_theme?.header_font_color || '#1f2329'
}
xpackForm.value.float_location = data.float_location
dialogVisible.value = true
}
const changeValue = (value: string) => {
xpackForm.value.disclaimer_value = value
}
const changeDisclaimer = (value: boolean) => {
xpackForm.value.disclaimer = value
}
const submit = async (formEl: FormInstance | undefined) => {
if (!formEl) return
await formEl.validate((valid, fields) => {
if (valid) {
let fd = new FormData()
Object.keys(xpackForm.value).map((item) => {
if (['custom_theme', 'float_location'].includes(item)) {
fd.append(item, JSON.stringify(xpackForm.value[item]))
} else {
fd.append(item, xpackForm.value[item])
}
})
applicationXpackApi.putAccessToken(id as string, fd, loading).then((res) => {
emit('refresh')
// @ts-ignore
MsgSuccess(t('common.settingSuccess'))
dialogVisible.value = false
})
}
})
}
defineExpose({ open })
</script>
<style lang="scss">
.setting-preview {
background: #f5f6f7;
height: 570px;
position: relative;
.float_icon {
position: absolute;
right: 8px;
bottom: 15px;
}
.setting-preview-container {
position: absolute;
left: 16px;
top: 25px;
border-radius: 8px;
border: 1px solid #ffffff;
background: var(--dialog-bg-gradient-color);
box-shadow: 0px 4px 8px 0px rgba(31, 35, 41, 0.1);
overflow: hidden;
width: 330px;
height: 520px;
.setting-preview-header {
background: var(--app-header-bg-color);
height: var(--app-header-height);
line-height: var(--app-header-height);
box-sizing: border-box;
border-bottom: 1px solid var(--el-border-color);
}
}
}
.display-setting-dialog {
.el-dialog__header {
padding-right: 8px;
}
.el-dialog__headerbtn {
top: 8px;
}
}
</style>

View File

@ -0,0 +1,434 @@
<template>
<LayoutContainer :header="$t('views.applicationOverview.title')">
<el-scrollbar>
<div class="main-calc-height p-24">
<h4 class="title-decoration-1 mb-16">
{{ $t('views.applicationOverview.appInfo.header') }}
</h4>
<el-card shadow="never" class="overview-card" v-loading="loading">
<div class="title flex align-center">
<div
class="edit-avatar mr-12"
@mouseenter="showEditIcon = true"
@mouseleave="showEditIcon = false"
>
<AppAvatar
v-if="isAppIcon(detail?.icon)"
shape="square"
:size="32"
style="background: none"
>
<img :src="detail?.icon" alt="" />
</AppAvatar>
<AppAvatar
v-else-if="detail?.name"
:name="detail?.name"
pinyinColor
shape="square"
:size="32"
/>
<AppAvatar
v-if="showEditIcon"
shape="square"
class="edit-mask"
:size="32"
@click="openEditAvatar"
>
<el-icon><EditPen /></el-icon>
</AppAvatar>
</div>
<h4>{{ detail?.name }}</h4>
</div>
<el-row :gutter="12">
<el-col :span="12" class="mt-16">
<div class="flex">
<el-text type="info">{{
$t('views.applicationOverview.appInfo.publicAccessLink')
}}</el-text>
<el-switch
v-model="accessToken.is_active"
class="ml-8"
size="small"
inline-prompt
:active-text="$t('views.applicationOverview.appInfo.openText')"
:inactive-text="$t('views.applicationOverview.appInfo.closeText')"
:before-change="() => changeState(accessToken.is_active)"
/>
</div>
<div class="mt-4 mb-16 url-height flex align-center" style="margin-bottom: 37px">
<span class="vertical-middle lighter break-all ellipsis-1">
{{ shareUrl }}
</span>
<el-tooltip effect="dark" :content="$t('common.copy')" placement="top">
<el-button type="primary" text @click="copyClick(shareUrl)">
<AppIcon iconName="app-copy"></AppIcon>
</el-button>
</el-tooltip>
<el-tooltip effect="dark" :content="$t('common.refresh')" placement="top">
<el-button
@click="refreshAccessToken"
type="primary"
text
style="margin-left: 1px"
>
<el-icon><RefreshRight /></el-icon>
</el-button>
</el-tooltip>
</div>
<div>
<el-button
v-if="accessToken?.is_active"
:disabled="!accessToken?.is_active"
type="primary"
tag="a"
:href="shareUrl"
target="_blank"
>
{{ $t('views.applicationOverview.appInfo.demo') }}
</el-button>
<el-button v-else :disabled="!accessToken?.is_active" type="primary">
{{ $t('views.applicationOverview.appInfo.demo') }}
</el-button>
<el-button :disabled="!accessToken?.is_active" @click="openDialog">
{{ $t('views.applicationOverview.appInfo.embedInWebsite') }}
</el-button>
<el-button @click="openLimitDialog">
{{ $t('views.applicationOverview.appInfo.accessControl') }}
</el-button>
<el-button @click="openDisplaySettingDialog">
{{ $t('views.applicationOverview.appInfo.displaySetting') }}
</el-button>
</div>
</el-col>
<el-col :span="12" class="mt-16">
<div class="flex">
<el-text type="info"
>{{ $t('views.applicationOverview.appInfo.apiAccessCredentials') }}
</el-text>
</div>
<div class="mt-4 mb-16 url-height">
<div>
<el-text>API {{ $t('common.fileUpload.document') }}</el-text
><el-button
type="primary"
link
@click="toUrl(apiUrl)"
class="vertical-middle lighter break-all"
>
{{ apiUrl }}
</el-button>
</div>
<div class="flex align-center">
<span class="flex">
<el-text style="width: 80px">Base URL</el-text>
</span>
<span class="vertical-middle lighter break-all ellipsis-1">{{
baseUrl + id
}}</span>
<el-tooltip effect="dark" :content="$t('common.copy')" placement="top">
<el-button type="primary" text @click="copyClick(baseUrl + id)">
<AppIcon iconName="app-copy"></AppIcon>
</el-button>
</el-tooltip>
</div>
</div>
<div>
<el-button @click="openAPIKeyDialog">{{
$t('views.applicationOverview.appInfo.apiKey')
}}</el-button>
</div>
</el-col>
</el-row>
</el-card>
<h4 class="title-decoration-1 mt-16 mb-16">
{{ $t('views.applicationOverview.monitor.monitoringStatistics') }}
</h4>
<div class="mb-16">
<el-select
v-model="history_day"
class="mr-12"
@change="changeDayHandle"
style="width: 180px"
>
<el-option
v-for="item in dayOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
<el-date-picker
v-if="history_day === 'other'"
v-model="daterangeValue"
type="daterange"
:start-placeholder="$t('views.applicationOverview.monitor.startDatePlaceholder')"
:end-placeholder="$t('views.applicationOverview.monitor.endDatePlaceholder')"
format="YYYY-MM-DD"
value-format="YYYY-MM-DD"
@change="changeDayRangeHandle"
/>
</div>
<div v-loading="statisticsLoading">
<StatisticsCharts :data="statisticsData" />
</div>
</div>
</el-scrollbar>
<EmbedDialog
ref="EmbedDialogRef"
:data="detail"
:api-input-params="mapToUrlParams(apiInputParams)"
/>
<APIKeyDialog ref="APIKeyDialogRef" />
<LimitDialog ref="LimitDialogRef" @refresh="refresh" />
<EditAvatarDialog ref="EditAvatarDialogRef" @refresh="refreshIcon" />
<XPackDisplaySettingDialog
ref="XPackDisplaySettingDialogRef"
@refresh="refresh"
v-if="user.isEnterprise()"
/>
<DisplaySettingDialog ref="DisplaySettingDialogRef" @refresh="refresh" v-else />
</LayoutContainer>
</template>
<script setup lang="ts">
import { ref, computed, onMounted, watch } from 'vue'
import { useRoute } from 'vue-router'
import EmbedDialog from './component/EmbedDialog.vue'
import APIKeyDialog from './component/APIKeyDialog.vue'
import LimitDialog from './component/LimitDialog.vue'
import DisplaySettingDialog from './component/DisplaySettingDialog.vue'
import XPackDisplaySettingDialog from './component/XPackDisplaySettingDialog.vue'
import EditAvatarDialog from './component/EditAvatarDialog.vue'
import StatisticsCharts from './component/StatisticsCharts.vue'
import applicationApi from '@/api/application'
import overviewApi from '@/api/application-overview'
import { nowDate, beforeDay } from '@/utils/time'
import { MsgSuccess, MsgConfirm } from '@/utils/message'
import { copyClick } from '@/utils/clipboard'
import { isAppIcon } from '@/utils/common'
import useStore from '@/stores'
import { t } from '@/locales'
const { user, application } = useStore()
const route = useRoute()
const {
params: { id }
} = route as any
const apiUrl = window.location.origin + '/doc/chat/'
const baseUrl = window.location.origin + '/api/application/'
const DisplaySettingDialogRef = ref()
const XPackDisplaySettingDialogRef = ref()
const EditAvatarDialogRef = ref()
const LimitDialogRef = ref()
const APIKeyDialogRef = ref()
const EmbedDialogRef = ref()
const accessToken = ref<any>({})
const detail = ref<any>(null)
const loading = ref(false)
const urlParams = computed(() =>
mapToUrlParams(apiInputParams.value) ? '?' + mapToUrlParams(apiInputParams.value) : ''
)
const shareUrl = computed(
() => application.location + accessToken.value.access_token + urlParams.value
)
const dayOptions = [
{
value: 7,
// @ts-ignore
label: t('views.applicationOverview.monitor.pastDayOptions.past7Days')
},
{
value: 30,
label: t('views.applicationOverview.monitor.pastDayOptions.past30Days')
},
{
value: 90,
label: t('views.applicationOverview.monitor.pastDayOptions.past90Days')
},
{
value: 183,
label: t('views.applicationOverview.monitor.pastDayOptions.past183Days')
},
{
value: 'other',
label: t('views.applicationOverview.monitor.pastDayOptions.other')
}
]
const history_day = ref<number | string>(7)
//
const daterangeValue = ref('')
//
const daterange = ref({
start_time: '',
end_time: ''
})
const statisticsLoading = ref(false)
const statisticsData = ref([])
const showEditIcon = ref(false)
const apiInputParams = ref([])
function toUrl(url: string) {
window.open(url, '_blank')
}
function openDisplaySettingDialog() {
if (user.isEnterprise()) {
XPackDisplaySettingDialogRef.value?.open(accessToken.value, detail.value)
} else {
DisplaySettingDialogRef.value?.open(accessToken.value, detail.value)
}
}
function openEditAvatar() {
EditAvatarDialogRef.value.open(detail.value)
}
function changeDayHandle(val: number | string) {
if (val !== 'other') {
daterange.value.start_time = beforeDay(val)
daterange.value.end_time = nowDate
getAppStatistics()
}
}
function changeDayRangeHandle(val: string) {
daterange.value.start_time = val[0]
daterange.value.end_time = val[1]
getAppStatistics()
}
function getAppStatistics() {
overviewApi.getStatistics(id, daterange.value, statisticsLoading).then((res: any) => {
statisticsData.value = res.data
})
}
function refreshAccessToken() {
MsgConfirm(
t('views.applicationOverview.appInfo.refreshToken.msgConfirm1'),
t('views.applicationOverview.appInfo.refreshToken.msgConfirm2'),
{
confirmButtonText: t('common.confirm'),
cancelButtonText: t('common.cancel')
}
)
.then(() => {
const obj = {
access_token_reset: true
}
// @ts-ignore
const str = t('views.applicationOverview.appInfo.refreshToken.refreshSuccess')
updateAccessToken(obj, str)
})
.catch(() => {})
}
function changeState(bool: Boolean) {
const obj = {
is_active: !bool
}
const str = obj.is_active ? t('common.status.enableSuccess') : t('common.status.disableSuccess')
updateAccessToken(obj, str)
.then(() => {
return true
})
.catch(() => {
return false
})
}
async function updateAccessToken(obj: any, str: string) {
applicationApi.putAccessToken(id as string, obj, loading).then((res) => {
accessToken.value = res?.data
MsgSuccess(str)
})
}
function openLimitDialog() {
LimitDialogRef.value.open(accessToken.value)
}
function openAPIKeyDialog() {
APIKeyDialogRef.value.open()
}
function openDialog() {
EmbedDialogRef.value.open(accessToken.value?.access_token)
}
function getAccessToken() {
application.asyncGetAccessToken(id, loading).then((res: any) => {
accessToken.value = res?.data
})
}
function getDetail() {
application.asyncGetApplicationDetail(id, loading).then((res: any) => {
detail.value = res.data
detail.value.work_flow?.nodes
?.filter((v: any) => v.id === 'base-node')
.map((v: any) => {
apiInputParams.value = v.properties.api_input_field_list
? v.properties.api_input_field_list.map((v: any) => {
return {
name: v.variable,
value: v.default_value
}
})
: v.properties.input_field_list
? v.properties.input_field_list
.filter((v: any) => v.assignment_method === 'api_input')
.map((v: any) => {
return {
name: v.variable,
value: v.default_value
}
})
: []
})
})
}
function refresh() {
getAccessToken()
}
function refreshIcon() {
getDetail()
}
function mapToUrlParams(map: any[]) {
const params = new URLSearchParams()
map.forEach((item: any) => {
params.append(encodeURIComponent(item.name), encodeURIComponent(item.value))
})
return params.toString() // URL
}
onMounted(() => {
getDetail()
getAccessToken()
changeDayHandle(history_day.value)
})
</script>
<style lang="scss" scoped>
.overview-card {
position: relative;
.active-button {
position: absolute;
right: 16px;
top: 21px;
}
}
</style>

View File

@ -0,0 +1,302 @@
<template>
<div v-show="show" class="workflow-dropdown-menu border border-r-4">
<el-tabs v-model="activeName" class="workflow-dropdown-tabs">
<div style="display: flex; width: 100%; justify-content: center" class="mb-4">
<el-input
v-model="search_text"
style="width: 240px"
:placeholder="$t('views.applicationWorkflow.searchBar.placeholder')"
>
<template #suffix>
<el-icon class="el-input__icon"><search /></el-icon>
</template>
</el-input>
</div>
<el-tab-pane :label="$t('views.applicationWorkflow.baseComponent')" name="base">
<el-scrollbar height="400">
<div v-if="filter_menu_nodes.length > 0">
<template v-for="(item, index) in filter_menu_nodes" :key="index">
<div
class="workflow-dropdown-item cursor flex p-8-12"
@click.stop="clickNodes(item)"
@mousedown.stop="onmousedown(item)"
>
<component :is="iconComponent(`${item.type}-icon`)" class="mr-8 mt-4" :size="32" />
<div class="pre-wrap">
<div class="lighter">{{ item.label }}</div>
<el-text type="info" size="small">{{ item.text }}</el-text>
</div>
</div>
</template>
</div>
<div v-else class="ml-16 mt-8">
<el-text type="info">{{ $t('views.applicationWorkflow.tip.noData') }}</el-text>
</div>
</el-scrollbar>
</el-tab-pane>
<el-tab-pane :label="$t('views.functionLib.title')" name="function">
<el-scrollbar height="400">
<div
class="workflow-dropdown-item cursor flex p-8-12"
@click.stop="clickNodes(functionNode)"
@mousedown.stop="onmousedown(functionNode)"
>
<component :is="iconComponent(`function-lib-node-icon`)" class="mr-8 mt-4" :size="32" />
<div class="pre-wrap">
<div class="lighter">{{ functionNode.label }}</div>
<el-text type="info" size="small">{{ functionNode.text }}</el-text>
</div>
</div>
<template v-for="(item, index) in filter_function_lib_list" :key="index">
<div
class="workflow-dropdown-item cursor flex p-8-12 align-center"
@click.stop="clickNodes(functionLibNode, item, 'function')"
@mousedown.stop="onmousedown(functionLibNode, item, 'function')"
>
<component
:is="iconComponent(`function-lib-node-icon`)"
class="mr-8"
:size="32"
:item="item"
/>
<div class="pre-wrap">
<div class="lighter ellipsis-1" :title="item.name">{{ item.name }}</div>
<p>
<el-text
class="ellipsis-1"
type="info"
size="small"
:title="item.desc"
v-if="item.desc"
>{{ item.desc }}</el-text
>
</p>
</div>
</div>
</template>
</el-scrollbar>
</el-tab-pane>
<el-tab-pane :label="$t('views.application.title')" name="application">
<el-scrollbar height="400">
<div v-if="filter_application_list.length > 0">
<template v-for="(item, index) in filter_application_list" :key="index">
<div
class="workflow-dropdown-item cursor flex align-center p-8-12"
@click.stop="clickNodes(applicationNode, item, 'application')"
@mousedown.stop="onmousedown(applicationNode, item, 'application')"
>
<component
:is="iconComponent(`application-node-icon`)"
class="mr-8"
:size="32"
:item="item"
/>
<div class="pre-wrap" style="width: 60%">
<div class="lighter ellipsis" :title="item.name">
{{ item.name }}
</div>
<p>
<el-text
class="ellipsis"
type="info"
size="small"
:title="item.desc"
v-if="item.desc"
>{{ item.desc }}</el-text
>
</p>
</div>
<div class="status-tag" style="margin-left: auto">
<el-tag type="warning" v-if="isWorkFlow(item.type)" style="height: 22px">
{{ $t('views.application.workflow') }}</el-tag
>
<el-tag class="blue-tag" v-else style="height: 22px">{{
$t('views.application.simple')
}}</el-tag>
</div>
</div>
</template>
</div>
<div v-else class="ml-16 mt-8">
<el-text type="info">{{ $t('views.applicationWorkflow.tip.noData') }}</el-text>
</div>
</el-scrollbar>
</el-tab-pane>
</el-tabs>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, computed } from 'vue'
import { menuNodes, functionLibNode, functionNode, applicationNode } from '@/workflow/common/data'
import { iconComponent } from '@/workflow/icons/utils'
import applicationApi from '@/api/application'
import { isWorkFlow } from '@/utils/application'
import { isAppIcon } from '@/utils/common'
const search_text = ref<string>('')
const props = defineProps({
show: {
type: Boolean,
default: false
},
id: {
type: String,
default: ''
},
workflowRef: Object
})
const emit = defineEmits(['clickNodes', 'onmousedown'])
const loading = ref(false)
const activeName = ref('base')
const functionLibList = ref<any[]>([])
const filter_function_lib_list = computed(() => {
return functionLibList.value.filter((item: any) =>
item.name.toLocaleLowerCase().includes(search_text.value.toLocaleLowerCase())
)
})
const applicationList = ref<any[]>([])
const filter_application_list = computed(() => {
return applicationList.value.filter((item: any) =>
item.name.toLocaleLowerCase().includes(search_text.value.toLocaleLowerCase())
)
})
const filter_menu_nodes = computed(() => {
return menuNodes.filter((item) =>
item.label.toLocaleLowerCase().includes(search_text.value.toLocaleLowerCase())
)
})
function clickNodes(item: any, data?: any, type?: string) {
if (data) {
item['properties']['stepName'] = data.name
if (type == 'function') {
item['properties']['node_data'] = {
...data,
function_lib_id: data.id,
input_field_list: data.input_field_list.map((field: any) => ({
...field,
value: field.source == 'reference' ? [] : ''
}))
}
}
if (type == 'application') {
if (isWorkFlow(data.type)) {
const nodeData = data.work_flow.nodes[0].properties.node_data
const fileUploadSetting = nodeData.file_upload_setting
item['properties']['node_data'] = {
name: data.name,
icon: data.icon,
application_id: data.id,
api_input_field_list: data.work_flow.nodes[0].properties.api_input_field_list,
user_input_field_list: data.work_flow.nodes[0].properties.user_input_field_list,
...(!fileUploadSetting
? {}
: {
...(fileUploadSetting.document ? { document_list: [] } : {}),
...(fileUploadSetting.image ? { image_list: [] } : {}),
...(fileUploadSetting.audio ? { audio_list: [] } : {})
})
}
} else {
item['properties']['node_data'] = {
name: data.name,
icon: data.icon,
application_id: data.id
}
}
}
}
props.workflowRef?.addNode(item)
emit('clickNodes', item)
}
function onmousedown(item: any, data?: any, type?: string) {
if (data) {
item['properties']['stepName'] = data.name
if (type == 'function') {
item['properties']['node_data'] = {
...data,
function_lib_id: data.id,
input_field_list: data.input_field_list.map((field: any) => ({
...field,
value: field.source == 'reference' ? [] : ''
}))
}
}
if (type == 'application') {
if (isWorkFlow(data.type)) {
const nodeData = data.work_flow.nodes[0].properties.node_data
const fileUploadSetting = nodeData.file_upload_setting
item['properties']['node_data'] = {
name: data.name,
icon: data.icon,
application_id: data.id,
api_input_field_list: data.work_flow.nodes[0].properties.api_input_field_list,
user_input_field_list: data.work_flow.nodes[0].properties.user_input_field_list,
...(!fileUploadSetting
? {}
: {
...(fileUploadSetting.document ? { document_list: [] } : {}),
...(fileUploadSetting.image ? { image_list: [] } : {}),
...(fileUploadSetting.audio ? { audio_list: [] } : {})
})
}
} else {
item['properties']['node_data'] = {
name: data.name,
icon: data.icon,
application_id: data.id
}
}
}
}
props.workflowRef?.onmousedown(item)
emit('onmousedown', item)
}
function getList() {
applicationApi.listFunctionLib(props.id, loading).then((res: any) => {
functionLibList.value = res.data
})
applicationApi.getApplicationList(props.id, loading).then((res: any) => {
applicationList.value = res.data
})
}
onMounted(() => {
getList()
})
</script>
<style lang="scss" scoped>
.workflow-dropdown-menu {
-moz-user-select: none; /* Firefox */
-webkit-user-select: none; /* WebKit内核 */
-ms-user-select: none; /* IE10及以后 */
-khtml-user-select: none; /* 早期浏览器 */
-o-user-select: none; /* Opera */
user-select: none; /* CSS3属性 */
position: absolute;
top: 49px;
right: 122px;
z-index: 99;
width: 268px;
box-shadow: 0px 4px 8px 0px var(--app-text-color-light-1);
background: #ffffff;
padding-bottom: 8px;
.title {
padding: 12px 12px 4px;
}
.workflow-dropdown-item {
&:hover {
background: var(--app-text-color-light-1);
}
}
}
</style>

View File

@ -0,0 +1,147 @@
<template>
<div class="workflow-publish-history border-l">
<h4 class="border-b p-16-24">{{ $t('views.applicationWorkflow.setting.releaseHistory') }}</h4>
<div class="list-height pt-0">
<el-scrollbar>
<div class="p-8 pt-0">
<common-list
:data="LogData"
class="mt-8"
v-loading="loading"
@click="clickListHandle"
@mouseenter="mouseenter"
@mouseleave="mouseId = ''"
>
<template #default="{ row, index }">
<div class="flex-between">
<div style="max-width: 80%">
<h5 :class="index === 0 ? 'primary' : ''" class="flex">
<ReadWrite
@change="editName($event, row)"
:data="row.name || datetimeFormat(row.update_time)"
trigger="manual"
:write="row.writeStatus"
@close="closeWrite(row)"
/>
<el-tag v-if="index === 0" class="default-tag ml-4">{{
$t('views.applicationWorkflow.setting.latestRelease')
}}</el-tag>
</h5>
<el-text type="info" class="color-secondary flex mt-8">
<AppAvatar :size="20" class="avatar-grey mr-4">
<el-icon><UserFilled /></el-icon>
</AppAvatar>
{{ row.publish_user_name }}
</el-text>
</div>
<div @click.stop v-show="mouseId === row.id">
<el-dropdown trigger="click" :teleported="false">
<el-button text>
<el-icon><MoreFilled /></el-icon>
</el-button>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item @click.stop="openEditVersion(row)">
<el-icon><EditPen /></el-icon>
{{ $t('common.edit') }}
</el-dropdown-item>
<el-dropdown-item @click="refreshVersion(row)">
<el-icon><RefreshLeft /></el-icon>
{{ $t('views.applicationWorkflow.setting.restoreCurrentVersion') }}
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</div>
</template>
<template #empty>
<div class="text-center">
<el-text type="info"> {{ $t('chat.noHistory') }}</el-text>
</div>
</template>
</common-list>
</div>
</el-scrollbar>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, computed } from 'vue'
import { useRoute } from 'vue-router'
import applicationApi from '@/api/application'
import { datetimeFormat } from '@/utils/time'
import { MsgSuccess, MsgError } from '@/utils/message'
import { t } from '@/locales'
const route = useRoute()
const {
params: { id }
} = route as any
const emit = defineEmits(['click', 'refreshVersion'])
const loading = ref(false)
const LogData = ref<any[]>([])
const mouseId = ref('')
function mouseenter(row: any) {
mouseId.value = row.id
}
function clickListHandle(item: any) {
emit('click', item)
}
function refreshVersion(item: any) {
emit('refreshVersion', item)
}
function openEditVersion(item: any) {
item['writeStatus'] = true
}
function closeWrite(item: any) {
item['writeStatus'] = false
}
function editName(val: string, item: any) {
if (val) {
const obj = {
name: val
}
applicationApi.putWorkFlowVersion(id as string, item.id, obj, loading).then(() => {
MsgSuccess(t('common.modifySuccess'))
item['writeStatus'] = false
getList()
})
} else {
MsgError(t('views.applicationWorkflow.tip.nameMessage'))
}
}
function getList() {
applicationApi.getWorkFlowVersion(id, loading).then((res: any) => {
LogData.value = res.data
})
}
onMounted(() => {
getList()
})
</script>
<style lang="scss" scoped>
.workflow-publish-history {
width: 320px;
position: absolute;
right: 0;
top: 57px;
background: #ffffff;
height: calc(100vh - 57px);
z-index: 9;
.list-height {
height: calc(100vh - 120px);
}
}
</style>

View File

@ -0,0 +1,487 @@
<template>
<div class="application-workflow" v-loading="loading">
<div class="header border-b flex-between p-12-24">
<div class="flex align-center">
<back-button @click="back"></back-button>
<h4>{{ detail?.name }}</h4>
<div v-if="showHistory && disablePublic">
<el-text type="info" class="ml-16 color-secondary"
>{{ $t('views.applicationWorkflow.info.previewVersion') }}
{{ currentVersion.name || datetimeFormat(currentVersion.update_time) }}</el-text
>
</div>
<el-text type="info" class="ml-16 color-secondary" v-else-if="saveTime"
>{{ $t('views.applicationWorkflow.info.saveTime')
}}{{ datetimeFormat(saveTime) }}</el-text
>
</div>
<div v-if="showHistory && disablePublic">
<el-button type="primary" class="mr-8" @click="refreshVersion()">
{{ $t('views.applicationWorkflow.setting.restoreVersion') }}
</el-button>
<el-divider direction="vertical" />
<el-button text @click="closeHistory">
<el-icon><Close /></el-icon>
</el-button>
</div>
<div v-else>
<el-button icon="Plus" @click="showPopover = !showPopover">
{{ $t('views.applicationWorkflow.setting.addComponent') }}
</el-button>
<el-button @click="clickShowDebug" :disabled="showDebug">
<AppIcon iconName="app-play-outlined" class="mr-4"></AppIcon>
{{ $t('views.applicationWorkflow.setting.debug') }}</el-button
>
<el-button @click="saveApplication(true)">
<AppIcon iconName="app-save-outlined" class="mr-4"></AppIcon>
{{ $t('common.save') }}
</el-button>
<el-button type="primary" @click="publicHandle">
{{ $t('views.applicationWorkflow.setting.public') }}
</el-button>
<el-dropdown trigger="click">
<el-button text @click.stop class="ml-8 mt-4">
<el-icon class="rotate-90"><MoreFilled /></el-icon>
</el-button>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item @click="openHistory">
<AppIcon iconName="app-history-outlined"></AppIcon>
{{ $t('views.applicationWorkflow.setting.releaseHistory') }}
</el-dropdown-item>
<el-dropdown-item>
<AppIcon iconName="app-save-outlined"></AppIcon>
{{ $t('views.applicationWorkflow.setting.autoSave') }}
<div class="ml-4">
<el-switch size="small" v-model="isSave" @change="changeSave" />
</div>
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</div>
<!-- 下拉框 -->
<el-collapse-transition>
<DropdownMenu
:show="showPopover"
:id="id"
v-click-outside="clickoutside"
@clickNodes="clickNodes"
@onmousedown="onmousedown"
:workflowRef="workflowRef"
/>
</el-collapse-transition>
<!-- 主画布 -->
<div class="workflow-main" ref="workflowMainRef">
<workflow ref="workflowRef" v-if="detail" :data="detail?.work_flow" />
</div>
<!-- 调试 -->
<el-collapse-transition>
<div class="workflow-debug-container" :class="enlarge ? 'enlarge' : ''" v-if="showDebug">
<div class="workflow-debug-header" :class="!isDefaultTheme ? 'custom-header' : ''">
<div class="flex-between">
<div class="flex align-center">
<div class="mr-12 ml-24 flex">
<AppAvatar
v-if="isAppIcon(detail?.icon)"
shape="square"
:size="32"
style="background: none"
>
<img :src="detail?.icon" alt="" />
</AppAvatar>
<AppAvatar
v-else-if="detail?.name"
:name="detail?.name"
pinyinColor
shape="square"
:size="32"
/>
</div>
<h4>
{{ detail?.name || $t('views.application.applicationForm.form.appName.label') }}
</h4>
</div>
<div class="mr-16">
<el-button link @click="enlarge = !enlarge">
<AppIcon
:iconName="enlarge ? 'app-minify' : 'app-magnify'"
class="color-secondary"
style="font-size: 20px"
></AppIcon>
</el-button>
<el-button link @click="showDebug = false">
<el-icon :size="20" class="color-secondary"><Close /></el-icon>
</el-button>
</div>
</div>
</div>
<div class="scrollbar-height">
<AiChat :application-details="detail" :type="'debug-ai-chat'"></AiChat>
</div>
</div>
</el-collapse-transition>
<!-- 发布历史 -->
<PublishHistory
v-if="showHistory"
@click="checkVersion"
v-click-outside="clickoutsideHistory"
@refreshVersion="refreshVersion"
/>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, onBeforeUnmount, computed, nextTick } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import type { Action } from 'element-plus'
import Workflow from '@/workflow/index.vue'
import DropdownMenu from '@/views/application-workflow/component/DropdownMenu.vue'
import PublishHistory from '@/views/application-workflow/component/PublishHistory.vue'
import applicationApi from '@/api/application'
import { isAppIcon } from '@/utils/common'
import { MsgSuccess, MsgError, MsgConfirm } from '@/utils/message'
import { datetimeFormat } from '@/utils/time'
import useStore from '@/stores'
import { WorkFlowInstance } from '@/workflow/common/validate'
import { hasPermission } from '@/utils/permission'
import { t } from '@/locales'
const { user, application } = useStore()
const router = useRouter()
const route = useRoute()
const isDefaultTheme = computed(() => {
return user.isDefaultTheme()
})
const {
params: { id }
} = route as any
let interval: any
const workflowRef = ref()
const workflowMainRef = ref()
const loading = ref(false)
const detail = ref<any>(null)
const showPopover = ref(false)
const showDebug = ref(false)
const enlarge = ref(false)
const saveTime = ref<any>('')
const isSave = ref(false)
const showHistory = ref(false)
const disablePublic = ref(false)
const currentVersion = ref<any>({})
const cloneWorkFlow = ref(null)
function back() {
if (JSON.stringify(cloneWorkFlow.value) !== JSON.stringify(getGraphData())) {
MsgConfirm(t('common.tip'), t('views.applicationWorkflow.tip.saveMessage'), {
confirmButtonText: t('views.applicationWorkflow.setting.exitSave'),
cancelButtonText: t('views.applicationWorkflow.setting.exit'),
type: 'warning',
distinguishCancelAndClose: true
})
.then(() => {
saveApplication(true, true)
})
.catch((action: Action) => {
action === 'cancel' && router.push({ path: `/application/${id}/WORK_FLOW/overview` })
})
} else {
router.push({ path: `/application/${id}/WORK_FLOW/overview` })
}
}
function clickoutsideHistory() {
if (!disablePublic.value) {
showHistory.value = false
disablePublic.value = false
}
}
function refreshVersion(item?: any) {
if (item) {
renderGraphData(item)
}
if (hasPermission(`APPLICATION:MANAGE:${id}`, 'AND') && isSave.value) {
initInterval()
}
showHistory.value = false
disablePublic.value = false
}
function checkVersion(item: any) {
disablePublic.value = true
currentVersion.value = item
renderGraphData(item)
closeInterval()
}
function renderGraphData(item: any) {
item.work_flow['nodes'].map((v: any) => {
v['properties']['noRender'] = true
})
detail.value.work_flow = item.work_flow
saveTime.value = item?.update_time
workflowRef.value?.clearGraphData()
nextTick(() => {
workflowRef.value?.render(item.work_flow)
})
}
function closeHistory() {
getDetail()
if (hasPermission(`APPLICATION:MANAGE:${id}`, 'AND') && isSave.value) {
initInterval()
}
showHistory.value = false
disablePublic.value = false
}
function openHistory() {
showHistory.value = true
}
function changeSave(bool: boolean) {
bool ? initInterval() : closeInterval()
localStorage.setItem('workflowAutoSave', bool.toString())
}
function clickNodes(item: any) {
// workflowRef.value?.addNode(item)
showPopover.value = false
}
function onmousedown(item: any) {
// workflowRef.value?.onmousedown(item)
showPopover.value = false
}
function clickoutside() {
showPopover.value = false
}
async function publicHandle() {
//
workflowRef.value
?.validate()
.then(async () => {
const obj = {
work_flow: getGraphData()
}
await application.asyncPutApplication(id, obj, loading)
const workflow = new WorkFlowInstance(obj.work_flow)
try {
workflow.is_valid()
} catch (e: any) {
MsgError(e.toString())
return
}
applicationApi.putPublishApplication(id as String, obj, loading).then(() => {
application.asyncGetApplicationDetail(id, loading).then((res: any) => {
detail.value.name = res.data.name
MsgSuccess(t('views.applicationWorkflow.tip.publicSuccess'))
})
})
})
.catch((res: any) => {
const node = res.node
const err_message = res.errMessage
if (typeof err_message == 'string') {
MsgError(
res.node.properties?.stepName +
` ${t('views.applicationWorkflow.node').toLowerCase()} ` +
err_message.toLowerCase()
)
} else {
const keys = Object.keys(err_message)
MsgError(
node.properties?.stepName +
` ${t('views.applicationWorkflow.node').toLowerCase()} ` +
err_message[keys[0]]?.[0]?.message.toLowerCase()
)
}
})
}
const clickShowDebug = () => {
workflowRef.value
?.validate()
.then(() => {
const graphData = getGraphData()
const workflow = new WorkFlowInstance(graphData)
try {
workflow.is_valid()
detail.value = {
...detail.value,
type: 'WORK_FLOW',
...workflow.get_base_node()?.properties.node_data,
work_flow: getGraphData()
}
showDebug.value = true
} catch (e: any) {
MsgError(e.toString())
}
})
.catch((res: any) => {
const node = res.node
const err_message = res.errMessage
if (typeof err_message == 'string') {
MsgError(
res.node.properties?.stepName + ` ${t('views.applicationWorkflow.node')}` + err_message
)
} else {
const keys = Object.keys(err_message)
MsgError(
node.properties?.stepName +
` ${t('views.applicationWorkflow.node')}` +
err_message[keys[0]]?.[0]?.message
)
}
})
}
// function clickoutsideDebug(e: any) {
// if (workflowMainRef.value && e && e.target && workflowMainRef.value.contains(e?.target)) {
// showDebug.value = false
// }
// }
function getGraphData() {
return workflowRef.value?.getGraphData()
}
function getDetail() {
application.asyncGetApplicationDetail(id).then((res: any) => {
res.data?.work_flow['nodes'].map((v: any) => {
v['properties']['noRender'] = true
})
detail.value = res.data
detail.value.stt_model_id = res.data.stt_model
detail.value.tts_model_id = res.data.tts_model
detail.value.tts_type = res.data.tts_type
saveTime.value = res.data?.update_time
application.asyncGetAccessToken(id, loading).then((res: any) => {
detail.value = { ...detail.value, ...res.data }
})
workflowRef.value?.clearGraphData()
nextTick(() => {
workflowRef.value?.render(detail.value.work_flow)
cloneWorkFlow.value = getGraphData()
})
})
}
function saveApplication(bool?: boolean, back?: boolean) {
const obj = {
work_flow: getGraphData()
}
loading.value = back || false
application
.asyncPutApplication(id, obj)
.then((res) => {
saveTime.value = new Date()
if (bool) {
cloneWorkFlow.value = getGraphData()
MsgSuccess(t('common.saveSuccess'))
if (back) {
router.push({ path: `/application/${id}/WORK_FLOW/overview` })
}
}
})
.catch(() => {
loading.value = false
})
}
/**
* 定时保存
*/
const initInterval = () => {
interval = setInterval(() => {
saveApplication()
}, 60000)
}
/**
* 关闭定时
*/
const closeInterval = () => {
if (interval) {
clearInterval(interval)
}
}
onMounted(() => {
getDetail()
const workflowAutoSave = localStorage.getItem('workflowAutoSave')
isSave.value = workflowAutoSave === 'true' ? true : false
//
if (hasPermission(`APPLICATION:MANAGE:${id}`, 'AND') && isSave.value) {
initInterval()
}
})
onBeforeUnmount(() => {
//
closeInterval()
workflowRef.value?.clearGraphData()
})
</script>
<style lang="scss">
.application-workflow {
background: var(--app-layout-bg-color);
height: 100%;
.header {
background: #ffffff;
}
.workflow-main {
height: calc(100vh - 62px);
box-sizing: border-box;
}
.workflow-dropdown-tabs {
.el-tabs__nav-wrap {
padding: 0 16px;
}
}
}
.workflow-debug-container {
z-index: 2000;
position: relative;
border-radius: 8px;
border: 1px solid #ffffff;
background: var(--dialog-bg-gradient-color);
box-shadow: 0px 4px 8px 0px rgba(31, 35, 41, 0.1);
position: fixed;
bottom: 16px;
right: 16px;
overflow: hidden;
width: 450px;
height: 600px;
.workflow-debug-header {
background: var(--app-header-bg-color);
height: var(--app-header-height);
line-height: var(--app-header-height);
box-sizing: border-box;
border-bottom: 1px solid var(--el-border-color);
}
.scrollbar-height {
height: calc(100% - var(--app-header-height) - 24px);
padding-top: 24px;
}
&.enlarge {
width: 50% !important;
height: 100% !important;
bottom: 0 !important;
right: 0 !important;
}
.chat-width {
max-width: 100% !important;
margin: 0 auto;
}
}
</style>

View File

@ -0,0 +1,185 @@
<template>
<div class="p-16-24">
<h4 class="mb-16">{{ $t('views.application.applicationAccess.title') }}</h4>
<el-row :gutter="16">
<el-col
:xs="24"
:sm="24"
:md="12"
:lg="12"
:xl="12"
class="mb-16"
v-for="(item, index) in platforms"
:key="index"
>
<el-card shadow="hover" class="border-none cursor" style="--el-card-padding: 24px">
<div class="flex-between">
<div class="flex align-center ml-8 mr-8">
<img :src="item.logoSrc" alt="" class="icon" />
<div class="ml-12">
<h5 class="mb-4">{{ item.name }}</h5>
<el-text type="info" style="font-size: 12px">{{ item.description }}</el-text>
</div>
</div>
<div>
<el-switch
size="small"
v-model="item.isActive"
@change="changeStatus(item.key, item.isActive)"
:disabled="!item.exists"
/>
<el-divider direction="vertical" />
<el-button class="mr-4" @click="openDrawer(item.key)">{{
$t('views.application.applicationAccess.setting')
}}</el-button>
</div>
</div>
</el-card>
</el-col>
</el-row>
<AccessSettingDrawer ref="AccessSettingDrawerRef" @refresh="refresh" />
</div>
</template>
<script setup lang="ts">
import { reactive, ref, onMounted } from 'vue'
import AccessSettingDrawer from './component/AccessSettingDrawer.vue'
import applicationApi from '@/api/application'
import { MsgSuccess } from '@/utils/message'
import { useRoute } from 'vue-router'
import { t } from '@/locales'
//
const platforms = reactive([
{
key: 'wecom',
logoSrc: new URL(`../../assets/logo_wechat-work.svg`, import.meta.url).href,
name: t('views.application.applicationAccess.wecom'),
description: t('views.application.applicationAccess.wecomTip'),
isActive: false,
exists: false
},
{
key: 'dingtalk',
logoSrc: new URL(`../../assets/logo_dingtalk.svg`, import.meta.url).href,
name: t('views.application.applicationAccess.dingtalk'),
description: t('views.application.applicationAccess.dingtalkTip'),
isActive: false,
exists: false
},
{
key: 'wechat',
logoSrc: new URL(`../../assets/logo_wechat.svg`, import.meta.url).href,
name: t('views.application.applicationAccess.wechat'),
description: t('views.application.applicationAccess.wechatTip'),
isActive: false,
exists: false
},
{
key: 'feishu',
logoSrc: new URL(`../../assets/logo_lark.svg`, import.meta.url).href,
name: t('views.application.applicationAccess.lark'),
description: t('views.application.applicationAccess.larkTip'),
isActive: false,
exists: false
},
{
key: 'slack',
logoSrc: new URL(`../../assets/logo_slack.svg`, import.meta.url).href,
name: t('views.application.applicationAccess.slack'),
description: t('views.application.applicationAccess.slackTip'),
isActive: false,
exists: false
}
])
const AccessSettingDrawerRef = ref()
const loading = ref(false)
const route = useRoute()
const {
params: { id }
} = route as any
function openDrawer(key: string) {
AccessSettingDrawerRef.value.open(id, key)
}
function refresh() {
getPlatformStatus()
}
function getPlatformStatus() {
loading.value = true
applicationApi.getPlatformStatus(id).then((res: any) => {
platforms.forEach((platform) => {
platform.isActive = res.data[platform.key][1]
platform.exists = res.data[platform.key][0]
})
loading.value = false
})
}
function changeStatus(type: string, value: boolean) {
const data = {
type: type,
status: value
}
applicationApi.updatePlatformStatus(id, data).then(() => {
MsgSuccess(t('common.saveSuccess'))
})
}
onMounted(() => {
getPlatformStatus()
})
</script>
<style lang="scss" scoped>
.p-16-24 {
padding: 16px 24px;
}
.mb-16 {
margin-bottom: 16px;
}
.flex-between {
display: flex;
justify-content: space-between;
align-items: center;
}
.flex {
display: flex;
}
.align-center {
align-items: center;
}
.ml-8 {
margin-left: 8px;
}
.mr-8 {
margin-right: 8px;
}
.ml-12 {
margin-left: 12px;
}
.mr-4 {
margin-right: 4px;
}
.cursor {
cursor: pointer;
}
.icon {
width: 32px; //
height: 32px; //
}
</style>

View File

@ -0,0 +1,816 @@
<template>
<LayoutContainer class="create-application">
<template #header>
<div class="flex-between w-full">
<h3>
{{ $t('common.setting') }}
</h3>
<el-button type="primary" @click="submit(applicationFormRef)" :disabled="loading">
{{ $t('views.application.applicationForm.buttons.publish') }}
</el-button>
</div>
</template>
<el-row v-loading="loading">
<el-col :span="10">
<div class="p-24 mb-16" style="padding-bottom: 0">
<h4 class="title-decoration-1">
{{ $t('views.applicationOverview.appInfo.header') }}
</h4>
</div>
<div class="scrollbar-height-left">
<el-scrollbar>
<el-form
hide-required-asterisk
ref="applicationFormRef"
:model="applicationForm"
:rules="rules"
label-position="top"
require-asterisk-position="right"
class="p-24"
style="padding-top: 0"
>
<el-form-item prop="name">
<template #label>
<div class="flex-between">
<span
>{{ $t('views.application.applicationForm.form.appName.label') }}
<span class="danger">*</span></span
>
</div>
</template>
<el-input
v-model="applicationForm.name"
maxlength="64"
:placeholder="$t('views.application.applicationForm.form.appName.placeholder')"
show-word-limit
@blur="applicationForm.name = applicationForm.name?.trim()"
/>
</el-form-item>
<el-form-item
:label="$t('views.application.applicationForm.form.appDescription.label')"
>
<el-input
v-model="applicationForm.desc"
type="textarea"
:placeholder="
$t('views.application.applicationForm.form.appDescription.placeholder')
"
:rows="3"
maxlength="256"
show-word-limit
/>
</el-form-item>
<el-form-item :label="$t('views.application.applicationForm.form.aiModel.label')">
<template #label>
<div class="flex-between">
<span>{{ $t('views.application.applicationForm.form.aiModel.label') }}</span>
<el-button
type="primary"
link
@click="openAIParamSettingDialog"
:disabled="!applicationForm.model_id"
>
{{ $t('common.paramSetting') }}
</el-button>
</div>
</template>
<ModelSelect
v-model="applicationForm.model_id"
:placeholder="$t('views.application.applicationForm.form.aiModel.placeholder')"
:options="modelOptions"
@change="model_change"
@submitModel="getModel"
showFooter
:model-type="'LLM'"
></ModelSelect>
</el-form-item>
<el-form-item
:label="$t('views.application.applicationForm.form.roleSettings.label')"
>
<MdEditorMagnify
:title="$t('views.application.applicationForm.form.roleSettings.label')"
v-model="applicationForm.model_setting.system"
style="height: 120px"
@submitDialog="submitSystemDialog"
:placeholder="
$t('views.application.applicationForm.form.roleSettings.placeholder')
"
/>
</el-form-item>
<el-form-item
prop="model_setting.no_references_prompt"
:rules="{
required: applicationForm.model_id,
message: $t('views.application.applicationForm.form.prompt.requiredMessage'),
trigger: 'blur'
}"
>
<template #label>
<div class="flex align-center">
<span class="mr-4"
>{{
$t('views.application.applicationForm.form.prompt.label') +
$t('views.application.applicationForm.form.prompt.noReferences')
}}
</span>
<el-tooltip
effect="dark"
:content="
$t('views.application.applicationForm.form.prompt.noReferencesTooltip', {
question: '{question}'
})
"
placement="right"
popper-class="max-w-350"
>
<AppIcon iconName="app-warning" class="app-warning-icon"></AppIcon>
</el-tooltip>
<span class="danger ml-4" v-if="applicationForm.model_id">*</span>
</div>
</template>
<MdEditorMagnify
:title="
$t('views.application.applicationForm.form.prompt.label') +
$t('views.application.applicationForm.form.prompt.noReferences')
"
v-model="applicationForm.model_setting.no_references_prompt"
style="height: 120px"
@submitDialog="submitNoReferencesPromptDialog"
placeholder="{question}"
/>
</el-form-item>
<el-form-item
:label="$t('views.application.applicationForm.form.historyRecord.label')"
@click.prevent
>
<el-input-number
v-model="applicationForm.dialogue_number"
:min="0"
:value-on-clear="0"
controls-position="right"
class="w-full"
:step="1"
:step-strictly="true"
/>
</el-form-item>
<el-form-item
label="$t('views.application.applicationForm.form.relatedKnowledgeBase')"
>
<template #label>
<div class="flex-between">
<span>{{
$t('views.application.applicationForm.form.relatedKnowledge.label')
}}</span>
<div>
<el-button type="primary" link @click="openParamSettingDialog">
<AppIcon iconName="app-operation" class="mr-4"></AppIcon>
{{ $t('common.paramSetting') }}
</el-button>
<el-button type="primary" link @click="openDatasetDialog">
<el-icon class="mr-4">
<Plus />
</el-icon>
{{ $t('common.add') }}
</el-button>
</div>
</div>
</template>
<div class="w-full">
<el-text type="info" v-if="applicationForm.dataset_id_list?.length === 0"
>{{ $t('views.application.applicationForm.form.relatedKnowledge.placeholder') }}
</el-text>
<el-row :gutter="12" v-else>
<el-col
:xs="24"
:sm="24"
:md="24"
:lg="12"
:xl="12"
class="mb-8"
v-for="(item, index) in applicationForm.dataset_id_list"
:key="index"
>
<el-card class="relate-dataset-card border-r-4" shadow="never">
<div class="flex-between">
<div class="flex align-center" style="width: 80%">
<AppAvatar
v-if="relatedObject(datasetList, item, 'id')?.type === '1'"
class="mr-8 avatar-purple"
shape="square"
:size="32"
>
<img src="@/assets/knowledge/icon_web.svg" style="width: 58%" alt="" />
</AppAvatar>
<AppAvatar
v-else-if="relatedObject(datasetList, item, 'id')?.type === '2'"
class="mr-8 avatar-purple"
shape="square"
:size="32"
style="background: none"
>
<img src="@/assets/knowledge/logo_lark.svg" style="width: 100%" alt="" />
</AppAvatar>
<AppAvatar v-else class="mr-8 avatar-blue" shape="square" :size="32">
<img src="@/assets/knowledge/icon_document.svg" style="width: 58%" alt="" />
</AppAvatar>
<span
class="ellipsis cursor"
:title="relatedObject(datasetList, item, 'id')?.name"
>
{{ relatedObject(datasetList, item, 'id')?.name }}</span
>
</div>
<el-button text @click="removeDataset(item)">
<el-icon>
<Close />
</el-icon>
</el-button>
</div>
</el-card>
</el-col>
</el-row>
</div>
</el-form-item>
<el-form-item
:label="$t('views.application.applicationForm.form.prompt.label')"
prop="model_setting.prompt"
:rules="{
required: applicationForm.model_id,
message: $t('views.application.applicationForm.form.prompt.requiredMessage'),
trigger: 'blur'
}"
>
<template #label>
<div class="flex align-center">
<span class="mr-4">
{{ $t('views.application.applicationForm.form.prompt.label') }}
{{ $t('views.application.applicationForm.form.prompt.references') }}
</span>
<el-tooltip
effect="dark"
:content="
$t('views.application.applicationForm.form.prompt.referencesTooltip', {
data: '{data}',
question: '{question}'
})
"
popper-class="max-w-350"
placement="right"
>
<AppIcon iconName="app-warning" class="app-warning-icon"></AppIcon>
</el-tooltip>
<span class="danger ml-4" v-if="applicationForm.model_id">*</span>
</div>
</template>
<MdEditorMagnify
:title="
$t('views.application.applicationForm.form.prompt.label') +
$t('views.application.applicationForm.form.prompt.references')
"
v-model="applicationForm.model_setting.prompt"
style="height: 150px"
@submitDialog="submitPromptDialog"
:placeholder="defaultPrompt"
/>
</el-form-item>
<el-form-item :label="$t('views.application.applicationForm.form.prologue')">
<MdEditorMagnify
:title="$t('views.application.applicationForm.form.prologue')"
v-model="applicationForm.prologue"
style="height: 150px"
@submitDialog="submitPrologueDialog"
/>
</el-form-item>
<el-form-item @click.prevent>
<template #label>
<div class="flex-between">
<span class="mr-4">
{{ $t('views.application.applicationForm.form.reasoningContent.label') }}
</span>
<div class="flex">
<el-button type="primary" link @click="openReasoningParamSettingDialog">
<el-icon><Setting /></el-icon>
</el-button>
<el-switch
class="ml-8"
size="small"
v-model="applicationForm.model_setting.reasoning_content_enable"
@change="sttModelEnableChange"
/>
</div>
</div>
</template>
</el-form-item>
<el-form-item
prop="stt_model_id"
:rules="{
required: applicationForm.stt_model_enable,
message: $t('views.application.applicationForm.form.voiceInput.requiredMessage'),
trigger: 'change'
}"
>
<template #label>
<div class="flex-between">
<span class="mr-4">
{{ $t('views.application.applicationForm.form.voiceInput.label') }}
<span class="danger" v-if="applicationForm.stt_model_enable">*</span>
</span>
<div class="flex">
<el-checkbox
v-if="applicationForm.stt_model_enable"
v-model="applicationForm.stt_autosend"
>{{
$t('views.application.applicationForm.form.voiceInput.autoSend')
}}</el-checkbox
>
<el-switch
class="ml-8"
size="small"
v-model="applicationForm.stt_model_enable"
@change="sttModelEnableChange"
/>
</div>
</div>
</template>
<ModelSelect
v-show="applicationForm.stt_model_enable"
v-model="applicationForm.stt_model_id"
:placeholder="$t('views.application.applicationForm.form.voiceInput.placeholder')"
:options="sttModelOptions"
:model-type="'STT'"
></ModelSelect>
</el-form-item>
<el-form-item
prop="tts_model_id"
:rules="{
required: applicationForm.tts_type === 'TTS' && applicationForm.tts_model_enable,
message: $t('views.application.applicationForm.form.voicePlay.requiredMessage'),
trigger: 'change'
}"
>
<template #label>
<div class="flex-between">
<span class="mr-4"
>{{ $t('views.application.applicationForm.form.voicePlay.label') }}
<span
class="danger"
v-if="
applicationForm.tts_type === 'TTS' && applicationForm.tts_model_enable
"
>*</span
>
</span>
<div class="flex">
<el-checkbox
v-if="applicationForm.tts_model_enable"
v-model="applicationForm.tts_autoplay"
>{{
$t('views.application.applicationForm.form.voicePlay.autoPlay')
}}</el-checkbox
>
<el-switch
class="ml-8"
size="small"
v-model="applicationForm.tts_model_enable"
@change="ttsModelEnableChange"
/>
</div>
</div>
</template>
<div class="w-full">
<el-radio-group
v-model="applicationForm.tts_type"
v-show="applicationForm.tts_model_enable"
class="mb-8"
>
<el-radio value="BROWSER">{{
$t('views.application.applicationForm.form.voicePlay.browser')
}}</el-radio>
<el-radio value="TTS">{{
$t('views.application.applicationForm.form.voicePlay.tts')
}}</el-radio>
</el-radio-group>
</div>
<div class="flex-between w-full">
<ModelSelect
v-if="applicationForm.tts_type === 'TTS' && applicationForm.tts_model_enable"
v-model="applicationForm.tts_model_id"
:placeholder="
$t('views.application.applicationForm.form.voicePlay.placeholder')
"
:options="ttsModelOptions"
@change="ttsModelChange()"
:model-type="'TTS'"
></ModelSelect>
<el-button
v-if="applicationForm.tts_type === 'TTS'"
@click="openTTSParamSettingDialog"
:disabled="!applicationForm.tts_model_id"
class="ml-8"
>
<el-icon><Operation /></el-icon>
</el-button>
</div>
</el-form-item>
</el-form>
</el-scrollbar>
</div>
</el-col>
<el-col :span="14" class="p-24 border-l">
<h4 class="title-decoration-1 mb-16">
{{ $t('views.application.applicationForm.title.appTest') }}
</h4>
<div class="dialog-bg">
<div class="flex align-center p-16 mb-8">
<div
class="edit-avatar mr-12"
@mouseenter="showEditIcon = true"
@mouseleave="showEditIcon = false"
>
<AppAvatar
v-if="isAppIcon(applicationForm?.icon)"
shape="square"
:size="32"
style="background: none"
>
<img :src="applicationForm?.icon" alt="" />
</AppAvatar>
<AppAvatar
v-else-if="applicationForm?.name"
:name="applicationForm?.name"
pinyinColor
shape="square"
:size="32"
/>
<AppAvatar
v-if="showEditIcon"
shape="square"
class="edit-mask"
:size="32"
@click="openEditAvatar"
>
<el-icon><EditPen /></el-icon>
</AppAvatar>
</div>
<h4>
{{
applicationForm?.name || $t('views.application.applicationForm.form.appName.label')
}}
</h4>
</div>
<div class="scrollbar-height">
<AiChat :applicationDetails="applicationForm" :type="'debug-ai-chat'"></AiChat>
</div>
</div>
</el-col>
</el-row>
<AIModeParamSettingDialog ref="AIModeParamSettingDialogRef" @refresh="refreshForm" />
<TTSModeParamSettingDialog ref="TTSModeParamSettingDialogRef" @refresh="refreshTTSForm" />
<ParamSettingDialog ref="ParamSettingDialogRef" @refresh="refreshParam" />
<AddDatasetDialog
ref="AddDatasetDialogRef"
@addData="addDataset"
:data="datasetList"
@refresh="refresh"
:loading="datasetLoading"
/>
<EditAvatarDialog ref="EditAvatarDialogRef" @refresh="refreshIcon" />
<ReasoningParamSettingDialog
ref="ReasoningParamSettingDialogRef"
@refresh="submitReasoningDialog"
/>
</LayoutContainer>
</template>
<script setup lang="ts">
import { reactive, ref, onMounted } from 'vue'
import { useRoute } from 'vue-router'
import { groupBy } from 'lodash'
import AIModeParamSettingDialog from './component/AIModeParamSettingDialog.vue'
import ParamSettingDialog from './component/ParamSettingDialog.vue'
import AddDatasetDialog from './component/AddDatasetDialog.vue'
import EditAvatarDialog from '@/views/application-overview/component/EditAvatarDialog.vue'
import applicationApi from '@/api/application'
import { isAppIcon } from '@/utils/common'
import type { FormInstance, FormRules } from 'element-plus'
import type { ApplicationFormType } from '@/api/type/application'
import { relatedObject } from '@/utils/utils'
import { MsgSuccess, MsgWarning } from '@/utils/message'
import useStore from '@/stores'
import { t } from '@/locales'
import TTSModeParamSettingDialog from './component/TTSModeParamSettingDialog.vue'
import ReasoningParamSettingDialog from './component/ReasoningParamSettingDialog.vue'
const { model, application } = useStore()
const route = useRoute()
const {
params: { id }
} = route as any
// @ts-ignore
const defaultPrompt = t('views.application.applicationForm.form.prompt.defaultPrompt', {
data: '{data}',
question: '{question}'
})
const optimizationPrompt =
t('views.application.applicationForm.dialog.defaultPrompt1', {
question: '{question}'
}) +
'<data></data>' +
t('views.application.applicationForm.dialog.defaultPrompt2')
const AIModeParamSettingDialogRef = ref<InstanceType<typeof AIModeParamSettingDialog>>()
const ReasoningParamSettingDialogRef = ref<InstanceType<typeof ReasoningParamSettingDialog>>()
const TTSModeParamSettingDialogRef = ref<InstanceType<typeof TTSModeParamSettingDialog>>()
const ParamSettingDialogRef = ref<InstanceType<typeof ParamSettingDialog>>()
const applicationFormRef = ref<FormInstance>()
const AddDatasetDialogRef = ref()
const EditAvatarDialogRef = ref()
const loading = ref(false)
const datasetLoading = ref(false)
const applicationForm = ref<ApplicationFormType>({
name: '',
desc: '',
model_id: '',
dialogue_number: 1,
prologue: t('views.application.applicationForm.form.defaultPrologue'),
dataset_id_list: [],
dataset_setting: {
top_n: 3,
similarity: 0.6,
max_paragraph_char_number: 5000,
search_mode: 'embedding',
no_references_setting: {
status: 'ai_questioning',
value: '{question}'
}
},
model_setting: {
prompt: defaultPrompt,
system: t('views.application.applicationForm.form.roleSettings.placeholder'),
no_references_prompt: '{question}',
reasoning_content_enable: false
},
model_params_setting: {},
problem_optimization: false,
problem_optimization_prompt: optimizationPrompt,
stt_model_id: '',
tts_model_id: '',
stt_model_enable: false,
tts_model_enable: false,
tts_type: 'BROWSER',
type: 'SIMPLE'
})
const rules = reactive<FormRules<ApplicationFormType>>({
name: [
{
required: true,
message: t('views.application.applicationForm.form.appName.placeholder'),
trigger: 'blur'
}
]
})
const modelOptions = ref<any>(null)
const datasetList = ref([])
const sttModelOptions = ref<any>(null)
const ttsModelOptions = ref<any>(null)
const showEditIcon = ref(false)
function submitPrologueDialog(val: string) {
applicationForm.value.prologue = val
}
function submitPromptDialog(val: string) {
applicationForm.value.model_setting.prompt = val
}
function submitNoReferencesPromptDialog(val: string) {
applicationForm.value.model_setting.no_references_prompt = val
}
function submitSystemDialog(val: string) {
applicationForm.value.model_setting.system = val
}
function submitReasoningDialog(val: any) {
applicationForm.value.model_setting = {
...applicationForm.value.model_setting,
...val
}
}
const submit = async (formEl: FormInstance | undefined) => {
if (!formEl) return
await formEl.validate((valid, fields) => {
if (valid) {
application.asyncPutApplication(id, applicationForm.value, loading).then((res) => {
MsgSuccess(t('common.saveSuccess'))
})
}
})
}
const model_change = (model_id?: string) => {
applicationForm.value.model_id = model_id
if (model_id) {
AIModeParamSettingDialogRef.value?.reset_default(model_id, id)
} else {
refreshForm({})
}
}
const openAIParamSettingDialog = () => {
if (applicationForm.value.model_id) {
AIModeParamSettingDialogRef.value?.open(
applicationForm.value.model_id,
id,
applicationForm.value.model_params_setting
)
}
}
const openReasoningParamSettingDialog = () => {
ReasoningParamSettingDialogRef.value?.open(applicationForm.value.model_setting)
}
const openTTSParamSettingDialog = () => {
if (applicationForm.value.tts_model_id) {
TTSModeParamSettingDialogRef.value?.open(
applicationForm.value.tts_model_id,
id,
applicationForm.value.tts_model_params_setting
)
}
}
const openParamSettingDialog = () => {
ParamSettingDialogRef.value?.open(applicationForm.value)
}
function refreshParam(data: any) {
applicationForm.value = { ...applicationForm.value, ...data }
}
function refreshForm(data: any) {
applicationForm.value.model_params_setting = data
}
function refreshTTSForm(data: any) {
applicationForm.value.tts_model_params_setting = data
}
function removeDataset(id: any) {
if (applicationForm.value.dataset_id_list) {
applicationForm.value.dataset_id_list.splice(
applicationForm.value.dataset_id_list.indexOf(id),
1
)
}
}
function addDataset(val: Array<string>) {
applicationForm.value.dataset_id_list = val
}
function openDatasetDialog() {
AddDatasetDialogRef.value.open(applicationForm.value.dataset_id_list)
}
function getDetail() {
application.asyncGetApplicationDetail(id, loading).then((res: any) => {
applicationForm.value = res.data
applicationForm.value.model_id = res.data.model
applicationForm.value.stt_model_id = res.data.stt_model
applicationForm.value.tts_model_id = res.data.tts_model
applicationForm.value.tts_type = res.data.tts_type
applicationForm.value.model_setting.no_references_prompt =
res.data.model_setting.no_references_prompt || '{question}'
application.asyncGetAccessToken(id, loading).then((res: any) => {
applicationForm.value = { ...applicationForm.value, ...res.data }
})
})
}
function getDataset() {
application.asyncGetApplicationDataset(id, datasetLoading).then((res: any) => {
datasetList.value = res.data
})
}
function getModel() {
loading.value = true
applicationApi
.getApplicationModel(id)
.then((res: any) => {
modelOptions.value = groupBy(res?.data, 'provider')
loading.value = false
})
.catch(() => {
loading.value = false
})
}
function getSTTModel() {
loading.value = true
applicationApi
.getApplicationSTTModel(id)
.then((res: any) => {
sttModelOptions.value = groupBy(res?.data, 'provider')
loading.value = false
})
.catch(() => {
loading.value = false
})
}
function getTTSModel() {
loading.value = true
applicationApi
.getApplicationTTSModel(id)
.then((res: any) => {
ttsModelOptions.value = groupBy(res?.data, 'provider')
loading.value = false
})
.catch(() => {
loading.value = false
})
}
function ttsModelChange() {
if (applicationForm.value.tts_model_id) {
TTSModeParamSettingDialogRef.value?.reset_default(applicationForm.value.tts_model_id, id)
} else {
refreshTTSForm({})
}
}
function ttsModelEnableChange() {
if (!applicationForm.value.tts_model_enable) {
applicationForm.value.tts_model_id = ''
applicationForm.value.tts_type = 'BROWSER'
}
}
function sttModelEnableChange() {
if (!applicationForm.value.stt_model_enable) {
applicationForm.value.stt_model_id = ''
}
}
function openEditAvatar() {
EditAvatarDialogRef.value.open(applicationForm.value)
}
function refreshIcon() {
getDetail()
}
function refresh() {
getDataset()
}
onMounted(() => {
getModel()
getDataset()
getDetail()
getSTTModel()
getTTSModel()
})
</script>
<style lang="scss" scoped>
.create-application {
.relate-dataset-card {
color: var(--app-text-color);
}
.dialog-bg {
border-radius: 8px;
background: var(--dialog-bg-gradient-color);
overflow: hidden;
box-sizing: border-box;
}
.scrollbar-height-left {
height: calc(var(--app-main-height) - 64px);
}
.scrollbar-height {
height: calc(var(--app-main-height) - 166px);
}
}
.prologue-md-editor {
height: 150px;
}
:deep(.el-form-item__label) {
display: block;
}
</style>

View File

@ -0,0 +1,91 @@
<template>
<el-dialog
align-center
:title="$t('common.paramSetting')"
v-model="dialogVisible"
style="width: 550px"
append-to-body
destroy-on-close
:close-on-click-modal="false"
:close-on-press-escape="false"
>
<DynamicsForm
v-model="form_data"
:model="form_data"
label-position="top"
require-asterisk-position="right"
:render_data="model_form_field"
ref="dynamicsFormRef"
>
</DynamicsForm>
<template #footer>
<span class="dialog-footer">
<el-button @click.prevent="dialogVisible = false">
{{ $t('common.cancel') }}
</el-button>
<el-button type="primary" @click="submit" :loading="loading">
{{ $t('common.confirm') }}
</el-button>
</span>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import type { FormField } from '@/components/dynamics-form/type'
import modelAPi from '@/api/model'
import applicationApi from '@/api/application'
import DynamicsForm from '@/components/dynamics-form/index.vue'
const model_form_field = ref<Array<FormField>>([])
const emit = defineEmits(['refresh'])
const dynamicsFormRef = ref<InstanceType<typeof DynamicsForm>>()
const form_data = ref<any>({})
const dialogVisible = ref(false)
const loading = ref(false)
const getApi = (model_id: string, application_id?: string) => {
return application_id
? applicationApi.getModelParamsForm(application_id, model_id, loading)
: modelAPi.getModelParamsForm(model_id, loading)
}
const open = (model_id: string, application_id?: string, model_setting_data?: any) => {
form_data.value = {}
const api = getApi(model_id, application_id)
api.then((ok) => {
model_form_field.value = ok.data
//
dynamicsFormRef.value?.render(model_form_field.value, model_setting_data)
})
dialogVisible.value = true
}
const reset_default = (model_id: string, application_id?: string) => {
const api = getApi(model_id, application_id)
api.then((ok) => {
model_form_field.value = ok.data
const model_setting_data = ok.data
.map((item) => {
if (item.show_default_value === false) {
return { [item.field]: undefined }
} else {
return { [item.field]: item.default_value }
}
})
.reduce((x, y) => ({ ...x, ...y }), {})
emit('refresh', model_setting_data)
})
}
const submit = async () => {
dynamicsFormRef.value?.validate().then(() => {
emit('refresh', form_data.value)
dialogVisible.value = false
})
}
defineExpose({ open, reset_default })
</script>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,400 @@
<template>
<el-drawer v-model="visible" size="60%" :append-to-body="true">
<template #header>
<div class="flex align-center" style="margin-left: -8px">
<h4>{{ drawerTitle }}</h4>
</div>
</template>
<el-form
v-if="dataLoaded"
ref="formRef"
:model="form[configType]"
label-width="120px"
:rules="rules[configType]"
label-position="top"
require-asterisk-position="right"
>
<h4 class="title-decoration-1 mb-16">{{ infoTitle }}</h4>
<template v-for="(item, key) in configFields[configType]" :key="key">
<el-form-item :label="item.label" :prop="key">
<el-input
v-model="form[configType][key]"
:type="isPasswordField(key) ? (passwordVisible[key] ? 'text' : 'password') : 'text'"
:placeholder="item.placeholder"
:show-password="isPasswordField(key)"
>
</el-input>
</el-form-item>
</template>
<div v-if="configType === 'wechat'" class="flex align-center mb-16">
<span class="lighter mr-8">{{
$t('views.application.applicationAccess.wecomSetting.authenticationSuccessful')
}}</span>
<el-switch v-if="configType === 'wechat'" v-model="form[configType].is_certification" />
</div>
<h4 class="title-decoration-1 mb-16">
{{ $t('views.application.applicationAccess.callback') }}
</h4>
<el-form-item label="URL" prop="callback_url">
<el-input
v-model="form[configType].callback_url"
:placeholder="$t('views.application.applicationAccess.callbackTip')"
readonly
>
<template #append>
<el-button @click="copyClick(form[configType].callback_url)">
<AppIcon iconName="app-copy"></AppIcon>
</el-button>
</template>
</el-input>
<el-text type="info" v-if="configType === 'wechat'">
{{ $t('views.application.applicationAccess.copyUrl') }}
<a
class="primary"
href="https://mp.weixin.qq.com/advanced/advanced?action=dev&t=advanced/dev"
target="_blank"
>{{ $t('views.application.applicationAccess.wechatPlatform') }}</a
>{{ $t('views.application.applicationAccess.wechatSetting.urlInfo') }}
</el-text>
<el-text type="info" v-if="configType === 'dingtalk'">
{{ $t('views.application.applicationAccess.copyUrl') }}
<a
class="primary"
href="https://open-dev.dingtalk.com/fe/app?hash=%23%2Fcorp%2Fapp#/corp/app"
target="_blank"
>{{ $t('views.application.applicationAccess.dingtalkPlatform') }}</a
>{{ $t('views.application.applicationAccess.dingtalkSetting.urlInfo') }}
</el-text>
<el-text type="info" v-if="configType === 'wecom'">
{{ $t('views.application.applicationAccess.copyUrl') }}
<a
class="primary"
href="https://work.weixin.qq.com/wework_admin/frame#apps"
target="_blank"
>{{ $t('views.application.applicationAccess.wecomPlatform') }}</a
>{{ $t('views.application.applicationAccess.wecomSetting.urlInfo') }}
</el-text>
<el-text type="info" v-if="configType === 'feishu'">
{{ $t('views.application.applicationAccess.copyUrl') }}
<a class="primary" href="https://open.feishu.cn/app/" target="_blank">{{
$t('views.application.applicationAccess.larkPlatform')
}}</a
>{{ $t('views.application.applicationAccess.larkSetting.urlInfo') }}
</el-text>
</el-form-item>
</el-form>
<template #footer>
<div>
<el-button @click="closeDrawer">{{ $t('common.cancel') }}</el-button>
<el-button type="primary" @click="submit" :disabled="loading">
{{ $t('common.save') }}
</el-button>
</div>
</template>
</el-drawer>
</template>
<script setup lang="ts">
import { ref, reactive, computed } from 'vue'
import type { FormInstance } from 'element-plus'
import applicationApi from '@/api/application'
import { useRoute } from 'vue-router'
import { MsgError, MsgSuccess } from '@/utils/message'
import { copyClick } from '@/utils/clipboard'
import { t } from '@/locales'
type PlatformType = 'wechat' | 'dingtalk' | 'wecom' | 'feishu' | 'slack'
const formRef = ref<FormInstance>()
const visible = ref(false)
const loading = ref(false)
const dataLoaded = ref(false)
const configType = ref<PlatformType>('wechat')
const route = useRoute()
const emit = defineEmits(['refresh'])
const {
params: { id }
} = route as any
const form = reactive<any>({
wechat: {
app_id: '',
app_secret: '',
token: '',
encoding_aes_key: '',
is_certification: false,
callback_url: ''
},
dingtalk: { client_id: '', client_secret: '', callback_url: '' },
wecom: {
app_id: '',
agent_id: '',
secret: '',
token: '',
encoding_aes_key: '',
callback_url: ''
},
feishu: { app_id: '', app_secret: '', verification_token: '', callback_url: '' },
slack: { signing_secret: '', bot_user_token: '', callback_url: '' }
})
const rules = reactive<{ [propName: string]: any }>({
wechat: {
app_id: [
{
required: true,
message: t('views.application.applicationAccess.wechatSetting.appIdPlaceholder'),
trigger: 'blur'
}
],
app_secret: [
{
required: true,
message: t('views.application.applicationAccess.wechatSetting.appSecretPlaceholder'),
trigger: 'blur'
}
],
token: [
{
required: true,
message: t('views.application.applicationAccess.wechatSetting.tokenPlaceholder'),
trigger: 'blur'
}
],
encoding_aes_key: [
{
required: true,
message: t('views.application.applicationAccess.wechatSetting.aesKeyPlaceholder'),
trigger: 'blur'
}
]
},
dingtalk: {
client_id: [
{
required: true,
message: t('views.application.applicationAccess.dingtalkSetting.clientIdPlaceholder'),
trigger: 'blur'
}
],
client_secret: [
{
required: true,
message: t('views.application.applicationAccess.dingtalkSetting.clientSecretPlaceholder'),
trigger: 'blur'
}
]
},
wecom: {
app_id: [
{
required: true,
message: t('views.application.applicationAccess.wecomSetting.cropIdPlaceholder'),
trigger: 'blur'
}
],
agent_id: [
{
required: true,
message: t('views.application.applicationAccess.wecomSetting.agentIdPlaceholder'),
trigger: 'blur'
}
],
secret: [
{
required: true,
message: t('views.application.applicationAccess.wecomSetting.secretPlaceholder'),
trigger: 'blur'
}
],
token: [
{
required: true,
message: t('views.application.applicationAccess.wecomSetting.tokenPlaceholder'),
trigger: 'blur'
}
],
encoding_aes_key: [
{
required: true,
message: t('views.application.applicationAccess.wecomSetting.encodingAesKeyPlaceholder'),
trigger: 'blur'
}
]
},
feishu: {
app_id: [
{
required: true,
message: t('views.application.applicationAccess.larkSetting.appIdPlaceholder'),
trigger: 'blur'
}
],
app_secret: [
{
required: true,
message: t('views.application.applicationAccess.larkSetting.appSecretPlaceholder'),
trigger: 'blur'
}
],
verification_token: [
{
required: false,
message: t('views.application.applicationAccess.larkSetting.verificationTokenPlaceholder'),
trigger: 'blur'
}
]
},
slack: {
signing_secret: [
{
required: true,
message: t('views.application.applicationAccess.slackSetting.signingSecretPlaceholder'),
trigger: 'blur'
}
],
bot_user_token: [
{
required: true,
message: t('views.application.applicationAccess.slackSetting.botUserTokenPlaceholder'),
trigger: 'blur'
}
]
}
})
const configFields: { [propName: string]: { [propName: string]: any } } = {
wechat: {
app_id: {
label: t('views.application.applicationAccess.wechatSetting.appId'),
placeholder: ''
},
app_secret: {
label: t('views.application.applicationAccess.wechatSetting.appSecret'),
placeholder: ''
},
token: { label: t('views.application.applicationAccess.wechatSetting.token'), placeholder: '' },
encoding_aes_key: {
label: t('views.application.applicationAccess.wechatSetting.aesKey'),
placeholder: ''
}
},
dingtalk: {
client_id: { label: 'Client ID', placeholder: '' },
client_secret: { label: 'Client Secret', placeholder: '' }
},
wecom: {
app_id: {
label: t('views.application.applicationAccess.wecomSetting.cropId'),
placeholder: ''
},
agent_id: { label: 'Agent ID', placeholder: '' },
secret: { label: 'Secret', placeholder: '' },
token: { label: 'Token', placeholder: '' },
encoding_aes_key: { label: 'EncodingAESKey', placeholder: '' }
},
feishu: {
app_id: { label: 'App ID', placeholder: '' },
app_secret: { label: 'App Secret', placeholder: '' },
verification_token: { label: 'Verification Token', placeholder: '' }
},
slack: {
signing_secret: { label: 'Signing Secret', placeholder: '' },
bot_user_token: { label: 'Bot User Token', placeholder: '' }
}
}
const passwordFields = new Set([
'app_secret',
'client_secret',
'secret',
'bot_user_token',
'signing_secret'
])
const drawerTitle = computed(
() =>
({
wechat: t('views.application.applicationAccess.wechatSetting.title'),
dingtalk: t('views.application.applicationAccess.dingtalkSetting.title'),
wecom: t('views.application.applicationAccess.wecomSetting.title'),
feishu: t('views.application.applicationAccess.larkSetting.title'),
slack: t('views.application.applicationAccess.slackSetting.title')
}[configType.value])
)
const infoTitle = computed(
() =>
({
wechat: t('views.applicationOverview.appInfo.header'),
dingtalk: t('views.applicationOverview.appInfo.header'),
wecom: t('views.applicationOverview.appInfo.header'),
feishu: t('views.applicationOverview.appInfo.header'),
slack: t('views.applicationOverview.appInfo.header')
}[configType.value])
)
const passwordVisible = reactive<Record<string, boolean>>(
Object.keys(configFields[configType.value]).reduce(
(acc, key) => {
if (passwordFields.has(key)) {
acc[key] = false
}
return acc
},
{} as Record<string, boolean>
)
)
const isPasswordField = (key: any) => passwordFields.has(key)
const closeDrawer = () => {
visible.value = false
}
const submit = async () => {
if (loading.value) return
formRef.value?.validate(async (valid) => {
if (valid) {
try {
applicationApi
.updatePlatformConfig(id, configType.value, form[configType.value], loading)
.then(() => {
MsgSuccess(t('common.saveSuccess'))
closeDrawer()
emit('refresh')
})
} catch {
MsgError(t('views.application.tip.saveErrorMessage'))
}
}
})
}
const open = async (id: string, type: PlatformType) => {
visible.value = true
configType.value = type
loading.value = true
dataLoaded.value = false
formRef.value?.resetFields()
try {
const res = await applicationApi.getPlatformConfig(id, type)
if (res.data) {
form[configType.value] = res.data
}
dataLoaded.value = true
} catch {
MsgError(t('views.application.tip.loadingErrorMessage'))
} finally {
loading.value = false
form[configType.value].callback_url = `${window.location.origin}/api/${type}/${id}`
}
}
defineExpose({ open })
</script>

View File

@ -0,0 +1,168 @@
<template>
<el-dialog
:title="$t('views.application.applicationForm.dialog.addDataset')"
v-model="dialogVisible"
width="600"
append-to-body
class="addDataset-dialog"
align-center
:close-on-click-modal="false"
:close-on-press-escape="false"
>
<template #header="{ titleId, titleClass }">
<div class="flex-between mb-8">
<h4 :id="titleId" :class="titleClass">
{{ $t('views.application.applicationForm.dialog.addDataset') }}
</h4>
<div class="flex align-center mr-8">
<el-button link class="ml-16" @click="refresh">
<el-icon class="mr-4"><Refresh /></el-icon>{{ $t('common.refresh') }}
</el-button>
<el-divider direction="vertical" />
</div>
</div>
<div class="flex-between">
<el-text type="info" class="color-secondary">
{{ $t('views.application.applicationForm.dialog.addDatasetPlaceholder') }}
</el-text>
<el-input
v-model="searchValue"
:placeholder="$t('common.search')"
prefix-icon="Search"
class="w-240"
clearable
/>
</div>
</template>
<el-scrollbar>
<div class="max-height">
<el-row :gutter="12" v-loading="loading">
<el-col :span="12" v-for="(item, index) in filterData" :key="index" class="mb-16">
<CardCheckbox value-field="id" :data="item" v-model="checkList" @change="changeHandle">
<span class="ellipsis cursor" :title="item.name"> {{ item.name }}</span>
</CardCheckbox>
</el-col>
</el-row>
</div>
</el-scrollbar>
<template #footer>
<div class="flex-between">
<div class="flex">
<el-text type="info" class="color-secondary mr-8" v-if="checkList.length > 0">
{{ $t('views.application.applicationForm.dialog.selected') }} {{ checkList.length }}
{{ $t('views.application.applicationForm.dialog.countDataset') }}
</el-text>
<el-button link type="primary" v-if="checkList.length > 0" @click="clearCheck">
{{ $t('common.clear') }}
</el-button>
</div>
<span>
<el-button @click.prevent="dialogVisible = false">
{{ $t('common.cancel') }}
</el-button>
<el-button type="primary" @click="submitHandle">
{{ $t('common.confirm') }}
</el-button>
</span>
</div>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { computed, ref, watch } from 'vue'
const props = defineProps({
data: {
type: Array<any>,
default: () => []
},
loading: Boolean
})
const emit = defineEmits(['addData', 'refresh'])
const dialogVisible = ref<boolean>(false)
const checkList = ref([])
const currentEmbedding = ref('')
const searchValue = ref('')
const searchDate = ref<any[]>([])
const filterData = computed(() => {
return currentEmbedding.value
? searchDate.value.filter((v) => v.embedding_mode_id === currentEmbedding.value)
: searchDate.value
})
watch(dialogVisible, (bool) => {
if (!bool) {
checkList.value = []
currentEmbedding.value = ''
searchValue.value = ''
}
})
watch(searchValue, (val) => {
if (val) {
searchDate.value = props.data.filter((v) => v.name.includes(val))
} else {
searchDate.value = props.data
}
})
function changeHandle() {
if (checkList.value.length > 0) {
currentEmbedding.value = props.data.filter(
(v) => v.id === checkList.value[0]
)[0].embedding_mode_id
} else if (checkList.value.length === 0) {
currentEmbedding.value = ''
}
}
function clearCheck() {
checkList.value = []
currentEmbedding.value = ''
}
const open = (checked: any) => {
searchDate.value = props.data
checkList.value = checked
if (checkList.value.length > 0) {
currentEmbedding.value = props.data.filter(
(v) => v.id === checkList.value[0]
)[0].embedding_mode_id
}
dialogVisible.value = true
}
const submitHandle = () => {
emit('addData', checkList.value)
dialogVisible.value = false
}
const refresh = () => {
emit('refresh')
}
defineExpose({ open })
</script>
<style lang="scss">
.addDataset-dialog {
padding: 0;
.el-dialog__header {
padding: 24px 24px 8px 24px;
}
.el-dialog__body {
padding: 8px !important;
}
.el-dialog__footer {
padding: 8px 24px 24px 24px;
}
.el-dialog__headerbtn {
top: 9px;
}
.max-height {
max-height: calc(100vh - 260px);
padding: 0 16px;
}
}
</style>

View File

@ -0,0 +1,178 @@
<template>
<el-dialog
:title="$t('views.application.copyApplication')"
v-model="dialogVisible"
width="650"
append-to-body
:close-on-click-modal="false"
:close-on-press-escape="false"
>
<el-form
ref="applicationFormRef"
:model="applicationForm"
:rules="rules"
label-position="top"
require-asterisk-position="right"
>
<el-form-item :label="$t('views.application.applicationForm.form.appName.label')" prop="name">
<el-input
v-model="applicationForm.name"
maxlength="64"
:placeholder="$t('views.application.applicationForm.form.appName.placeholder')"
show-word-limit
/>
</el-form-item>
<el-form-item :label="$t('views.application.applicationForm.form.appDescription.label')">
<el-input
v-model="applicationForm.desc"
type="textarea"
:placeholder="$t('views.application.applicationForm.form.appDescription.placeholder')"
:rows="3"
maxlength="256"
show-word-limit
/>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click.prevent="dialogVisible = false" :loading="loading">
{{ $t('common.cancel') }}
</el-button>
<el-button type="primary" @click="submitValid(applicationFormRef)" :loading="loading">
{{ $t('common.copy') }}
</el-button>
</span>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { ref, watch, reactive } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { cloneDeep } from 'lodash'
import type { ApplicationFormType } from '@/api/type/application'
import type { FormInstance, FormRules } from 'element-plus'
import applicationApi from '@/api/application'
import { MsgSuccess, MsgAlert } from '@/utils/message'
import { isWorkFlow } from '@/utils/application'
import { t } from '@/locales'
import useStore from '@/stores'
import { ValidType, ValidCount } from '@/enums/common'
const router = useRouter()
const { common, user } = useStore()
// @ts-ignore
const defaultPrompt = t('views.application.applicationForm.form.prompt.defaultPrompt', {
data: '{data}',
question: '{question}'
})
const applicationFormRef = ref()
const loading = ref(false)
const dialogVisible = ref<boolean>(false)
// @ts-ignore
const applicationForm = ref<ApplicationFormType>({
name: '',
desc: '',
model_id: '',
dialogue_number: 0,
prologue: t('views.application.applicationForm.form.defaultPrologue'),
dataset_id_list: [],
dataset_setting: {
top_n: 3,
similarity: 0.6,
max_paragraph_char_number: 5000,
search_mode: 'embedding',
no_references_setting: {
status: 'ai_questioning',
value: '{question}'
}
},
model_setting: {
prompt: defaultPrompt
},
problem_optimization: false,
type: 'SIMPLE'
})
const rules = reactive<FormRules<ApplicationFormType>>({
name: [
{
required: true,
message: t('views.application.applicationForm.form.appName.placeholder'),
trigger: 'blur'
}
]
})
watch(dialogVisible, (bool) => {
if (!bool) {
applicationForm.value = {
name: '',
desc: '',
model_id: '',
dialogue_number: 0,
prologue: t('views.application.applicationForm.form.defaultPrologue'),
dataset_id_list: [],
dataset_setting: {
top_n: 3,
similarity: 0.6,
max_paragraph_char_number: 5000,
search_mode: 'embedding',
no_references_setting: {
status: 'ai_questioning',
value: '{question}'
}
},
model_setting: {
prompt: defaultPrompt
},
problem_optimization: false,
type: 'SIMPLE'
}
applicationFormRef.value?.clearValidate()
}
})
const open = (data: any) => {
const obj = cloneDeep(data)
delete obj['id']
obj['name'] = obj['name'] + ` ${t('views.application.applicationForm.title.copy')}`
applicationForm.value = obj
dialogVisible.value = true
}
const submitValid = (formEl: FormInstance | undefined) => {
if (user.isEnterprise()) {
submitHandle(formEl)
} else {
common
.asyncGetValid(ValidType.Application, ValidCount.Application, loading)
.then(async (res: any) => {
if (res?.data) {
submitHandle(formEl)
} else {
MsgAlert(t('common.tip'), t('views.application.tip.professionalMessage'))
}
})
}
}
const submitHandle = async (formEl: FormInstance | undefined) => {
if (!formEl) return
await formEl.validate((valid) => {
if (valid) {
applicationApi.postApplication(applicationForm.value, loading).then((res) => {
MsgSuccess(t('common.createSuccess'))
if (isWorkFlow(applicationForm.value.type)) {
router.push({ path: `/application/${res.data.id}/workflow` })
} else {
router.push({ path: `/application/${res.data.id}/${res.data.type}/setting` })
}
dialogVisible.value = false
})
}
})
}
defineExpose({ open })
</script>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,271 @@
<template>
<el-dialog
:title="$t('views.application.createApplication')"
v-model="dialogVisible"
width="650"
append-to-body
:close-on-click-modal="false"
:close-on-press-escape="false"
>
<el-form
ref="applicationFormRef"
:model="applicationForm"
:rules="rules"
label-position="top"
require-asterisk-position="right"
@submit.prevent
>
<el-form-item :label="$t('views.application.applicationForm.form.appName.label')" prop="name">
<el-input
v-model="applicationForm.name"
maxlength="64"
:placeholder="$t('views.application.applicationForm.form.appName.placeholder')"
show-word-limit
@blur="applicationForm.name = applicationForm.name?.trim()"
/>
</el-form-item>
<el-form-item :label="$t('views.application.applicationForm.form.appDescription.label')">
<el-input
v-model="applicationForm.desc"
type="textarea"
:placeholder="$t('views.application.applicationForm.form.appDescription.placeholder')"
:rows="3"
maxlength="256"
show-word-limit
/>
</el-form-item>
<el-form-item :label="$t('views.application.applicationForm.form.appType.label')">
<el-radio-group v-model="applicationForm.type" class="card__radio">
<el-row :gutter="16">
<el-col :span="12">
<el-card shadow="never" :class="applicationForm.type === 'SIMPLE' ? 'active' : ''">
<el-radio value="SIMPLE" size="large">
<p class="mb-4">{{ $t('views.application.simple') }}</p>
<el-text type="info">{{
$t('views.application.applicationForm.form.appType.simplePlaceholder')
}}</el-text>
</el-radio>
</el-card>
</el-col>
<el-col :span="12">
<el-card shadow="never" :class="isWorkFlow(applicationForm.type) ? 'active' : ''">
<el-radio value="WORK_FLOW" size="large">
<p class="mb-4">{{ $t('views.application.workflow') }}</p>
<el-text type="info">{{
$t('views.application.applicationForm.form.appType.workflowPlaceholder')
}}</el-text>
</el-radio>
</el-card>
</el-col>
</el-row>
</el-radio-group>
</el-form-item>
<el-form-item
:label="$t('views.document.upload.template')"
v-if="applicationForm.type === 'WORK_FLOW'"
>
<div class="w-full">
<el-row :gutter="16">
<el-col :span="12">
<el-card
class="radio-card cursor"
shadow="never"
@click="selectedType('blank')"
:class="appTemplate === 'blank' ? 'active' : ''"
>
{{ $t('views.application.applicationForm.form.appTemplate.blankApp') }}
</el-card>
</el-col>
<el-col :span="12">
<el-card
class="radio-card cursor"
shadow="never"
:class="appTemplate === 'assistant' ? 'active' : ''"
@click="selectedType('assistant')"
>
{{ $t('views.application.applicationForm.form.appTemplate.assistantApp') }}
</el-card>
</el-col>
</el-row>
</div>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click.prevent="dialogVisible = false" :loading="loading">
{{ $t('common.cancel') }}
</el-button>
<el-button type="primary" @click="submitHandle(applicationFormRef)" :loading="loading">
{{ $t('common.create') }}
</el-button>
</span>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { ref, watch, reactive } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import type { ApplicationFormType } from '@/api/type/application'
import type { FormInstance, FormRules } from 'element-plus'
import applicationApi from '@/api/application'
import { MsgSuccess, MsgAlert } from '@/utils/message'
import { isWorkFlow } from '@/utils/application'
import { baseNodes } from '@/workflow/common/data'
import { t } from '@/locales'
const router = useRouter()
const emit = defineEmits(['refresh'])
// @ts-ignore
const defaultPrompt = t('views.application.applicationForm.form.prompt.defaultPrompt', {
data: '{data}',
question: '{question}'
})
const optimizationPrompt =
t('views.application.applicationForm.dialog.defaultPrompt1', {
question: '{question}'
}) +
'<data></data>' +
t('views.application.applicationForm.dialog.defaultPrompt2')
const workflowDefault = ref<any>({
edges: [],
nodes: baseNodes
})
const appTemplate = ref('blank')
const applicationFormRef = ref()
const loading = ref(false)
const dialogVisible = ref<boolean>(false)
const applicationForm = ref<ApplicationFormType>({
name: '',
desc: '',
model_id: '',
dialogue_number: 1,
prologue: t('views.application.applicationForm.form.defaultPrologue'),
dataset_id_list: [],
dataset_setting: {
top_n: 3,
similarity: 0.6,
max_paragraph_char_number: 5000,
search_mode: 'embedding',
no_references_setting: {
status: 'ai_questioning',
value: '{question}'
}
},
model_setting: {
prompt: defaultPrompt,
system: t('views.application.applicationForm.form.roleSettings.placeholder'),
no_references_prompt: '{question}'
},
model_params_setting: {},
problem_optimization: false,
problem_optimization_prompt: optimizationPrompt,
stt_model_id: '',
tts_model_id: '',
stt_model_enable: false,
tts_model_enable: false,
tts_type: 'BROWSER',
type: 'SIMPLE'
})
const rules = reactive<FormRules<ApplicationFormType>>({
name: [
{
required: true,
message: t('views.application.applicationForm.form.appName.placeholder'),
trigger: 'blur'
}
],
model_id: [
{
required: false,
message: t('views.application.applicationForm.form.aiModel.placeholder'),
trigger: 'change'
}
]
})
watch(dialogVisible, (bool) => {
if (!bool) {
applicationForm.value = {
name: '',
desc: '',
model_id: '',
dialogue_number: 1,
prologue: t('views.application.applicationForm.form.defaultPrologue'),
dataset_id_list: [],
dataset_setting: {
top_n: 3,
similarity: 0.6,
max_paragraph_char_number: 5000,
search_mode: 'embedding',
no_references_setting: {
status: 'ai_questioning',
value: '{question}'
}
},
model_setting: {
prompt: defaultPrompt,
system: t('views.application.applicationForm.form.roleSettings.placeholder'),
no_references_prompt: '{question}'
},
model_params_setting: {},
problem_optimization: false,
problem_optimization_prompt: optimizationPrompt,
stt_model_id: '',
tts_model_id: '',
stt_model_enable: false,
tts_model_enable: false,
tts_type: 'BROWSER',
type: 'SIMPLE'
}
applicationFormRef.value?.clearValidate()
}
})
const open = () => {
dialogVisible.value = true
}
const submitHandle = async (formEl: FormInstance | undefined) => {
if (!formEl) return
await formEl.validate((valid) => {
if (valid) {
if (isWorkFlow(applicationForm.value.type) && appTemplate.value === 'blank') {
workflowDefault.value.nodes[0].properties.node_data.desc = applicationForm.value.desc
workflowDefault.value.nodes[0].properties.node_data.name = applicationForm.value.name
applicationForm.value['work_flow'] = workflowDefault.value
}
applicationApi.postApplication(applicationForm.value, loading).then((res) => {
MsgSuccess(t('common.createSuccess'))
emit('refresh')
if (isWorkFlow(applicationForm.value.type)) {
router.push({ path: `/application/${res.data.id}/workflow` })
} else {
router.push({ path: `/application/${res.data.id}/${res.data.type}/setting` })
}
dialogVisible.value = false
})
}
})
}
function selectedType(type: string) {
appTemplate.value = type
}
defineExpose({ open })
</script>
<style lang="scss" scoped>
.radio-card {
line-height: 22px;
&.active {
border-color: var(--el-color-primary);
color: var(--el-color-primary);
}
}
</style>

View File

@ -0,0 +1,91 @@
<template>
<el-dialog
align-center
:title="$t('common.setting')"
v-model="dialogVisible"
style="width: 550px"
append-to-body
:close-on-click-modal="false"
:close-on-press-escape="false"
>
<el-form
label-position="top"
ref="paramFormRef"
:model="form"
require-asterisk-position="right"
>
<el-form-item label="MCP" prop="mcp_enable">
<el-switch v-model="form.mcp_enable" />
</el-form-item>
<el-form-item
v-if="form.mcp_enable"
:label="$t('views.applicationWorkflow.nodes.mcpNode.configLabel')"
prop="mcp_servers"
:rules="[{ required: true, message: $t('common.required') }]"
>
<el-input
v-model="form.mcp_servers"
:rows="6"
type="textarea"
:placeholder="mcpServerJson"
/>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click.prevent="dialogVisible = false">{{ $t('common.cancel') }}</el-button>
<el-button type="primary" @click="submit()" :loading="loading">
{{ $t('common.save') }}
</el-button>
</span>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { ref, watch } from 'vue'
const emit = defineEmits(['refresh'])
const paramFormRef = ref()
const mcpServerJson = `{
"math": {
"url": "your_server",
"transport": "sse"
}
}`
const form = ref<any>({
mcp_servers: '',
mcp_enable: false
})
const dialogVisible = ref<boolean>(false)
const loading = ref(false)
watch(dialogVisible, (bool) => {
if (!bool) {
form.value = {
mcp_servers: '',
mcp_enable: false
}
}
})
const open = (data: any) => {
form.value = { ...form.value, ...data }
dialogVisible.value = true
}
const submit = () => {
paramFormRef.value.validate().then((valid: any) => {
if (valid) {
emit('refresh', form.value)
dialogVisible.value = false
}
})
}
defineExpose({ open })
</script>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,327 @@
<template>
<el-dialog
align-center
:title="$t('common.paramSetting')"
class="param-dialog"
v-model="dialogVisible"
style="width: 550px"
append-to-body
:close-on-click-modal="false"
:close-on-press-escape="false"
>
<el-scrollbar max-height="550">
<div class="p-16">
<el-form label-position="top" ref="paramFormRef" :model="form" v-loading="loading">
<el-form-item :label="$t('views.application.applicationForm.dialog.selectSearchMode')">
<el-radio-group
v-model="form.dataset_setting.search_mode"
class="card__radio"
@change="changeHandle"
>
<el-card
shadow="never"
class="mb-16"
:class="form.search_mode === 'embedding' ? 'active' : ''"
>
<el-radio value="embedding" size="large">
<p class="mb-4">
{{ $t('views.application.applicationForm.dialog.vectorSearch') }}
</p>
<el-text type="info">{{
$t('views.application.applicationForm.dialog.vectorSearchTooltip')
}}</el-text>
</el-radio>
</el-card>
<el-card
shadow="never"
class="mb-16"
:class="form.dataset_setting.search_mode === 'keywords' ? 'active' : ''"
>
<el-radio value="keywords" size="large">
<p class="mb-4">
{{ $t('views.application.applicationForm.dialog.fullTextSearch') }}
</p>
<el-text type="info">{{
$t('views.application.applicationForm.dialog.fullTextSearchTooltip')
}}</el-text>
</el-radio>
</el-card>
<el-card
shadow="never"
:class="form.dataset_setting.search_mode === 'blend' ? 'active' : ''"
>
<el-radio value="blend" size="large">
<p class="mb-4">
{{ $t('views.application.applicationForm.dialog.hybridSearch') }}
</p>
<el-text type="info">{{
$t('views.application.applicationForm.dialog.hybridSearchTooltip')
}}</el-text>
</el-radio>
</el-card>
</el-radio-group>
</el-form-item>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item>
<template #label>
<div class="flex align-center">
<span class="mr-4">{{
$t('views.application.applicationForm.dialog.similarityThreshold')
}}</span>
<el-tooltip
effect="dark"
:content="$t('views.application.applicationForm.dialog.similarityTooltip')"
placement="right"
>
<AppIcon iconName="app-warning" class="app-warning-icon"></AppIcon>
</el-tooltip>
</div>
</template>
<el-input-number
v-model="form.dataset_setting.similarity"
:min="0"
:max="form.dataset_setting.search_mode === 'blend' ? 2 : 1"
:precision="3"
:step="0.1"
:value-on-clear="0"
controls-position="right"
class="w-full"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item :label="$t('views.application.applicationForm.dialog.topReferences')">
<el-input-number
v-model="form.dataset_setting.top_n"
:min="1"
:max="10000"
:value-on-clear="1"
controls-position="right"
class="w-full"
/>
</el-form-item>
</el-col>
</el-row>
<el-form-item :label="$t('views.application.applicationForm.dialog.maxCharacters')">
<el-slider
v-model="form.dataset_setting.max_paragraph_char_number"
show-input
:show-input-controls="false"
:min="500"
:max="100000"
class="custom-slider"
/>
</el-form-item>
<el-form-item
v-if="!isWorkflowType"
:label="$t('views.application.applicationForm.dialog.noReferencesAction')"
>
<el-form
label-position="top"
ref="noReferencesformRef"
:model="noReferencesform"
:rules="noReferencesRules"
:hide-required-asterisk="true"
class="w-full"
>
<el-radio-group
v-model="form.dataset_setting.no_references_setting.status"
class="radio-block-avatar"
>
<el-radio value="ai_questioning">
<p>
{{ $t('views.application.applicationForm.dialog.continueQuestioning') }}
</p>
</el-radio>
<el-radio value="designated_answer">
<p>{{ $t('views.application.applicationForm.dialog.provideAnswer') }}</p>
<el-form-item
v-if="form.dataset_setting.no_references_setting.status === 'designated_answer'"
prop="designated_answer"
>
<el-input
v-model="noReferencesform.designated_answer"
:rows="2"
type="textarea"
maxlength="2048"
:placeholder="defaultValue['designated_answer']"
/>
</el-form-item>
</el-radio>
</el-radio-group>
</el-form>
</el-form-item>
<el-form-item @click.prevent v-if="!isWorkflowType">
<template #label>
<div class="flex align-center">
<span class="mr-4">{{
$t('views.application.applicationForm.form.problemOptimization.label')
}}</span>
</div>
</template>
<el-switch size="small" v-model="form.problem_optimization"></el-switch>
</el-form-item>
<el-form-item
v-if="form.problem_optimization"
:label="$t('views.application.applicationForm.form.prompt.label')"
>
<el-input
v-model="form.problem_optimization_prompt"
:rows="6"
type="textarea"
maxlength="2048"
:placeholder="defaultPrompt"
/>
</el-form-item>
</el-form>
</div>
</el-scrollbar>
<template #footer>
<span class="dialog-footer p-16">
<el-button @click.prevent="dialogVisible = false">{{ $t('common.cancel') }}</el-button>
<el-button type="primary" @click="submit(noReferencesformRef)" :loading="loading">
{{ $t('common.save') }}
</el-button>
</span>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { ref, watch, reactive } from 'vue'
import type { FormInstance, FormRules } from 'element-plus'
import { isWorkFlow } from '@/utils/application'
import { t } from '@/locales'
const emit = defineEmits(['refresh'])
const paramFormRef = ref()
const noReferencesformRef = ref()
const defaultValue = {
ai_questioning: '{question}',
// @ts-ignore
designated_answer: t('views.application.applicationForm.dialog.designated_answer')
}
const defaultPrompt =
t('views.application.applicationForm.dialog.defaultPrompt1', {
question: '{question}'
}) +
'<data></data>' +
t('views.application.applicationForm.dialog.defaultPrompt2')
const form = ref<any>({
dataset_setting: {
search_mode: 'embedding',
top_n: 3,
similarity: 0.6,
max_paragraph_char_number: 5000,
no_references_setting: {
status: 'ai_questioning',
value: '{question}'
}
},
problem_optimization: false,
problem_optimization_prompt: defaultPrompt
})
const noReferencesform = ref<any>({
ai_questioning: defaultValue['ai_questioning'],
designated_answer: defaultValue['designated_answer']
})
const noReferencesRules = reactive<FormRules<any>>({
ai_questioning: [
{
required: true,
message: t('views.application.applicationForm.form.aiModel.placeholder'),
trigger: 'blur'
}
],
designated_answer: [
{
required: true,
message: t('views.application.applicationForm.form.prompt.requiredMessage'),
trigger: 'blur'
}
]
})
const dialogVisible = ref<boolean>(false)
const loading = ref(false)
const isWorkflowType = ref(false)
watch(dialogVisible, (bool) => {
if (!bool) {
// form.value = {
// dataset_setting: {
// search_mode: 'embedding',
// top_n: 3,
// similarity: 0.6,
// max_paragraph_char_number: 5000,
// no_references_setting: {
// status: 'ai_questioning',
// value: '{question}'
// }
// },
// problem_optimization: false,
// problem_optimization_prompt: ''
// }
noReferencesform.value = {
ai_questioning: defaultValue['ai_questioning'],
designated_answer: defaultValue['designated_answer']
}
noReferencesformRef.value?.clearValidate()
}
})
const open = (data: any, type?: string) => {
isWorkflowType.value = isWorkFlow(type)
form.value = {
dataset_setting: { ...data.dataset_setting },
problem_optimization: data.problem_optimization,
problem_optimization_prompt: data.problem_optimization_prompt
}
if (!isWorkflowType.value) {
noReferencesform.value[form.value.dataset_setting.no_references_setting.status] =
form.value.dataset_setting.no_references_setting.value
}
dialogVisible.value = true
}
const submit = async (formEl: FormInstance | undefined) => {
if (isWorkflowType.value) {
delete form.value['no_references_setting']
emit('refresh', form.value)
dialogVisible.value = false
} else {
if (!formEl) return
await formEl.validate((valid, fields) => {
if (valid) {
form.value.dataset_setting.no_references_setting.value =
noReferencesform.value[form.value.dataset_setting.no_references_setting.status]
emit('refresh', form.value)
dialogVisible.value = false
}
})
}
}
function changeHandle(val: string) {
if (val === 'keywords') {
form.value.dataset_setting.similarity = 0
} else {
form.value.dataset_setting.similarity = 0.6
}
}
defineExpose({ open })
</script>
<style lang="scss"></style>

View File

@ -0,0 +1,87 @@
<template>
<el-dialog
align-center
:title="$t('common.setting')"
class="param-dialog"
v-model="dialogVisible"
style="width: 550px"
append-to-body
:close-on-click-modal="false"
:close-on-press-escape="false"
>
<el-form label-position="top" ref="paramFormRef" :model="form" class="p-12-16">
<el-text type="info" class="color-secondary">{{
$t('views.application.applicationForm.form.reasoningContent.tooltip')
}}</el-text>
<el-row class="mt-16" :gutter="20">
<el-col :span="12">
<el-form-item
:label="$t('views.application.applicationForm.form.reasoningContent.start')"
>
<el-input
type="textarea"
v-model="form.reasoning_content_start"
:rows="6"
maxlength="50"
placeholder="<think>"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item :label="$t('views.application.applicationForm.form.reasoningContent.end')">
<el-input
type="textarea"
v-model="form.reasoning_content_end"
:rows="6"
maxlength="50"
placeholder="</think>"
/>
</el-form-item>
</el-col>
</el-row>
</el-form>
<template #footer>
<span class="dialog-footer p-16">
<el-button @click.prevent="dialogVisible = false">{{ $t('common.cancel') }}</el-button>
<el-button type="primary" @click="submit()" :loading="loading">
{{ $t('common.save') }}
</el-button>
</span>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { ref, watch, reactive } from 'vue'
const emit = defineEmits(['refresh'])
const form = ref<any>({
reasoning_content_start: '<think>',
reasoning_content_end: '</think>'
})
const dialogVisible = ref<boolean>(false)
const loading = ref(false)
watch(dialogVisible, (bool) => {
if (!bool) {
form.value = {
reasoning_content_start: '<think>',
reasoning_content_end: '</think>'
}
}
})
const open = (data: any) => {
form.value = { ...form.value, ...data }
dialogVisible.value = true
}
const submit = () => {
emit('refresh', form.value)
dialogVisible.value = false
}
defineExpose({ open })
</script>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,153 @@
<template>
<el-dialog
align-center
:title="$t('common.paramSetting')"
v-model="dialogVisible"
style="width: 550px"
append-to-body
:close-on-click-modal="false"
:close-on-press-escape="false"
>
<DynamicsForm
v-model="form_data"
:model="form_data"
label-position="top"
require-asterisk-position="right"
:render_data="model_form_field"
ref="dynamicsFormRef"
>
</DynamicsForm>
<template #footer>
<div class="flex-between">
<el-button @click="testPlay" :loading="playLoading">
<AppIcon iconName="app-video-play" class="mr-4"></AppIcon>
{{ $t('views.application.applicationForm.form.voicePlay.listeningTest') }}
</el-button>
<span class="dialog-footer">
<el-button @click.prevent="dialogVisible = false">
{{ $t('common.cancel') }}
</el-button>
<el-button type="primary" @click="submit" :loading="loading">
{{ $t('common.confirm') }}
</el-button>
</span>
</div>
</template>
</el-dialog>
<!-- 先渲染不然不能播放 -->
<audio ref="audioPlayer" controls hidden="hidden"></audio>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import type { FormField } from '@/components/dynamics-form/type'
import modelAPi from '@/api/model'
import applicationApi from '@/api/application'
import DynamicsForm from '@/components/dynamics-form/index.vue'
import { keys } from 'lodash'
import { app } from '@/main'
import { MsgError } from '@/utils/message'
const {
params: { id }
} = app.config.globalProperties.$route as any
const tts_model_id = ref('')
const model_form_field = ref<Array<FormField>>([])
const emit = defineEmits(['refresh'])
const dynamicsFormRef = ref<InstanceType<typeof DynamicsForm>>()
const form_data = ref<any>({})
const dialogVisible = ref(false)
const loading = ref(false)
const playLoading = ref(false)
const getApi = (model_id: string, application_id?: string) => {
return application_id
? applicationApi.getModelParamsForm(application_id, model_id, loading)
: modelAPi.getModelParamsForm(model_id, loading)
}
const open = (model_id: string, application_id?: string, model_setting_data?: any) => {
form_data.value = {}
tts_model_id.value = model_id
const api = getApi(model_id, application_id)
api.then((ok) => {
model_form_field.value = ok.data
const resp = ok.data
.map((item: any) => ({
[item.field]: item.show_default_value !== false ? item.default_value : undefined
}))
.reduce((x, y) => ({ ...x, ...y }), {})
//
if (model_setting_data) {
Object.keys(model_setting_data).forEach((key) => {
if (!(key in resp)) {
delete model_setting_data[key]
}
})
}
model_setting_data = { ...resp, ...model_setting_data }
//
dynamicsFormRef.value?.render(model_form_field.value, model_setting_data)
})
dialogVisible.value = true
}
const reset_default = (model_id: string, application_id?: string) => {
const api = getApi(model_id, application_id)
api.then((ok) => {
model_form_field.value = ok.data
const model_setting_data = ok.data
.map((item) => ({
[item.field]: item.show_default_value !== false ? item.default_value : undefined
}))
.reduce((x, y) => ({ ...x, ...y }), {})
emit('refresh', model_setting_data)
})
}
const submit = async () => {
dynamicsFormRef.value?.validate().then(() => {
emit('refresh', form_data.value)
dialogVisible.value = false
})
}
const audioPlayer = ref<HTMLAudioElement | null>(null)
const testPlay = () => {
const data = {
...form_data.value,
tts_model_id: tts_model_id.value
}
applicationApi
.playDemoText(id as string, data, playLoading)
.then(async (res: any) => {
if (res.type === 'application/json') {
const text = await res.text()
MsgError(text)
return
}
// Blob
const blob = new Blob([res], { type: 'audio/mp3' })
// URL
const url = URL.createObjectURL(blob)
// audioPlayer DOM
if (audioPlayer.value instanceof HTMLAudioElement) {
audioPlayer.value.src = url
audioPlayer.value.play() //
} else {
console.error('audioPlayer.value is not an instance of HTMLAudioElement')
}
})
.catch((err) => {
console.log('err: ', err)
})
}
defineExpose({ open, reset_default })
</script>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,444 @@
<template>
<div class="application-list-container p-24" style="padding-top: 16px">
<div class="flex-between mb-16">
<h4>{{ $t('views.application.title') }}</h4>
<div class="flex-between">
<el-select
v-model="selectUserId"
class="mr-12"
@change="searchHandle"
style="max-width: 240px; width: 150px"
>
<el-option
v-for="item in userOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
<el-input
v-model="searchValue"
@change="searchHandle"
:placeholder="$t('views.application.searchBar.placeholder')"
prefix-icon="Search"
class="w-240"
style="min-width: 240px"
clearable
/>
</div>
</div>
<div v-loading.fullscreen.lock="paginationConfig.current_page === 1 && loading">
<InfiniteScroll
:size="applicationList.length"
:total="paginationConfig.total"
:page_size="paginationConfig.page_size"
v-model:current_page="paginationConfig.current_page"
@load="getList"
:loading="loading"
>
<el-row :gutter="15">
<el-col :xs="24" :sm="12" :md="8" :lg="6" :xl="6" class="mb-16">
<el-card shadow="hover" class="application-card-add" style="--el-card-padding: 8px">
<div class="card-add-button flex align-center cursor p-8" @click="openCreateDialog">
<AppIcon iconName="app-add-application" class="mr-8"></AppIcon>
{{ $t('views.application.createApplication') }}
</div>
<el-divider style="margin: 8px 0" />
<el-upload
ref="elUploadRef"
:file-list="[]"
action="#"
multiple
:auto-upload="false"
:show-file-list="false"
:limit="1"
:on-change="(file: any, fileList: any) => importApplication(file)"
class="card-add-button"
>
<div class="flex align-center cursor p-8">
<AppIcon iconName="app-import" class="mr-8"></AppIcon>
{{ $t('views.application.importApplication') }}
</div>
</el-upload>
</el-card>
</el-col>
<el-col
:xs="24"
:sm="12"
:md="8"
:lg="6"
:xl="6"
v-for="(item, index) in applicationList"
:key="index"
class="mb-16"
>
<CardBox
:title="item.name"
:description="item.desc"
class="application-card cursor"
@click="router.push({ path: `/application/${item.id}/${item.type}/overview` })"
>
<template #icon>
<AppAvatar
v-if="isAppIcon(item?.icon)"
shape="square"
:size="32"
style="background: none"
class="mr-8"
>
<img :src="item?.icon" alt="" />
</AppAvatar>
<AppAvatar
v-else-if="item?.name"
:name="item?.name"
pinyinColor
shape="square"
:size="32"
class="mr-8"
/>
</template>
<template #subTitle>
<el-text class="color-secondary" size="small">
<auto-tooltip :content="item.username">
{{ $t('common.creator') }}: {{ item.username }}
</auto-tooltip>
</el-text>
</template>
<div class="status-tag">
<el-tag type="warning" v-if="isWorkFlow(item.type)" style="height: 22px">
{{ $t('views.application.workflow') }}
</el-tag>
<el-tag class="blue-tag" v-else style="height: 22px">
{{ $t('views.application.simple') }}
</el-tag>
</div>
<template #footer>
<div class="footer-content">
<el-tooltip
effect="dark"
:content="$t('views.application.setting.demo')"
placement="top"
>
<el-button text @click.stop @click="getAccessToken(item.id)">
<AppIcon iconName="app-view"></AppIcon>
</el-button>
</el-tooltip>
<el-divider direction="vertical" />
<el-tooltip effect="dark" :content="$t('common.setting')" placement="top">
<el-button text @click.stop="settingApplication(item)">
<AppIcon iconName="Setting"></AppIcon>
</el-button>
</el-tooltip>
<el-divider direction="vertical" />
<span @click.stop>
<el-dropdown trigger="click">
<el-button text @click.stop>
<el-icon><MoreFilled /></el-icon>
</el-button>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item
v-if="is_show_copy_button(item)"
@click="copyApplication(item)"
>
<AppIcon iconName="app-copy"></AppIcon>
{{ $t('common.copy') }}
</el-dropdown-item>
<el-dropdown-item @click.stop="exportApplication(item)">
<AppIcon iconName="app-export"></AppIcon>
{{ $t('common.export') }}
</el-dropdown-item>
<el-dropdown-item icon="Delete" @click.stop="deleteApplication(item)">{{
$t('common.delete')
}}</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</span>
</div>
</template>
</CardBox>
</el-col>
</el-row>
</InfiniteScroll>
</div>
<CreateApplicationDialog ref="CreateApplicationDialogRef" />
<CopyApplicationDialog ref="CopyApplicationDialogRef" />
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, reactive } from 'vue'
import applicationApi from '@/api/application'
import CreateApplicationDialog from './component/CreateApplicationDialog.vue'
import CopyApplicationDialog from './component/CopyApplicationDialog.vue'
import { MsgSuccess, MsgConfirm, MsgAlert, MsgError } from '@/utils/message'
import { isAppIcon } from '@/utils/common'
import { useRouter } from 'vue-router'
import { isWorkFlow } from '@/utils/application'
import { ValidType, ValidCount } from '@/enums/common'
import { t } from '@/locales'
import useStore from '@/stores'
const elUploadRef = ref<any>()
const { application, user, common } = useStore()
const router = useRouter()
const CopyApplicationDialogRef = ref()
const CreateApplicationDialogRef = ref()
const loading = ref(false)
const applicationList = ref<any[]>([])
const paginationConfig = reactive({
current_page: 1,
page_size: 30,
total: 0
})
interface UserOption {
label: string
value: string
}
const userOptions = ref<UserOption[]>([])
const selectUserId = ref('all')
const searchValue = ref('')
const apiInputParams = ref([])
function copyApplication(row: any) {
application.asyncGetApplicationDetail(row.id, loading).then((res: any) => {
if (res?.data) {
CopyApplicationDialogRef.value.open({ ...res.data, model_id: res.data.model })
}
})
}
const is_show_copy_button = (row: any) => {
return user.userInfo ? user.userInfo.id == row.user_id : false
}
function settingApplication(row: any) {
if (isWorkFlow(row.type)) {
router.push({ path: `/application/${row.id}/workflow` })
} else {
router.push({ path: `/application/${row.id}/${row.type}/setting` })
}
}
const exportApplication = (application: any) => {
applicationApi.exportApplication(application.id, application.name, loading).catch((e) => {
if (e.response.status !== 403) {
e.response.data.text().then((res: string) => {
MsgError(`${t('views.application.tip.ExportError')}:${JSON.parse(res).message}`)
})
}
})
}
const importApplication = (file: any) => {
const formData = new FormData()
formData.append('file', file.raw, file.name)
elUploadRef.value.clearFiles()
applicationApi
.importApplication(formData, loading)
.then(async (res: any) => {
if (res?.data) {
searchHandle()
}
})
.catch((e) => {
if (e.code === 400) {
MsgConfirm(t('common.tip'), t('views.application.tip.professionalMessage'), {
cancelButtonText: t('common.confirm'),
confirmButtonText: t('common.professional')
}).then(() => {
window.open('https://maxkb.cn/pricing.html', '_blank')
})
}
})
}
function openCreateDialog() {
common
.asyncGetValid(ValidType.Application, ValidCount.Application, loading)
.then(async (res: any) => {
if (res?.data) {
CreateApplicationDialogRef.value.open()
} else if (res?.code === 400) {
MsgConfirm(t('common.tip'), t('views.application.tip.professionalMessage'), {
cancelButtonText: t('common.confirm'),
confirmButtonText: t('common.professional')
}).then(() => {
window.open('https://maxkb.cn/pricing.html', '_blank')
})
}
})
}
function searchHandle() {
if (user.userInfo) {
localStorage.setItem(user.userInfo.id + 'application', selectUserId.value)
}
applicationList.value = []
paginationConfig.current_page = 1
paginationConfig.total = 0
getList()
}
function mapToUrlParams(map: any[]) {
const params = new URLSearchParams()
map.forEach((item: any) => {
params.append(encodeURIComponent(item.name), encodeURIComponent(item.value))
})
return params.toString() // URL
}
function getAccessToken(id: string) {
applicationList.value
.filter((app) => app.id === id)[0]
?.work_flow?.nodes?.filter((v: any) => v.id === 'base-node')
.map((v: any) => {
apiInputParams.value = v.properties.api_input_field_list
? v.properties.api_input_field_list.map((v: any) => {
return {
name: v.variable,
value: v.default_value
}
})
: v.properties.input_field_list
? v.properties.input_field_list
.filter((v: any) => v.assignment_method === 'api_input')
.map((v: any) => {
return {
name: v.variable,
value: v.default_value
}
})
: []
})
const apiParams = mapToUrlParams(apiInputParams.value)
? '?' + mapToUrlParams(apiInputParams.value)
: ''
application.asyncGetAccessToken(id, loading).then((res: any) => {
window.open(application.location + res?.data?.access_token + apiParams)
})
}
function deleteApplication(row: any) {
MsgConfirm(
// @ts-ignore
`${t('views.application.delete.confirmTitle')}${row.name} ?`,
t('views.application.delete.confirmMessage'),
{
confirmButtonText: t('common.confirm'),
cancelButtonText: t('common.cancel'),
confirmButtonClass: 'danger'
}
)
.then(() => {
applicationApi.delApplication(row.id, loading).then(() => {
const index = applicationList.value.findIndex((v) => v.id === row.id)
applicationList.value.splice(index, 1)
MsgSuccess(t('common.deleteSuccess'))
})
})
.catch(() => {})
}
function getList() {
const params = {
...(searchValue.value && { name: searchValue.value }),
...(selectUserId.value &&
selectUserId.value !== 'all' && { select_user_id: selectUserId.value })
}
applicationApi.getApplication(paginationConfig, params, loading).then((res) => {
res.data.records.forEach((item: any) => {
if (user.userInfo && item.user_id === user.userInfo.id) {
item.username = user.userInfo.username
} else {
item.username = userOptions.value.find((v) => v.value === item.user_id)?.label
}
})
applicationList.value = [...applicationList.value, ...res.data.records]
paginationConfig.total = res.data.total
})
}
function getUserList() {
applicationApi.getUserList('APPLICATION', loading).then((res) => {
if (res.data) {
userOptions.value = res.data.map((item: any) => {
return {
label: item.username,
value: item.id
}
})
if (user.userInfo) {
const selectUserIdValue = localStorage.getItem(user.userInfo.id + 'application')
if (selectUserIdValue && userOptions.value.find((v) => v.value === selectUserIdValue)) {
selectUserId.value = selectUserIdValue
}
}
getList()
}
})
}
onMounted(() => {
getUserList()
})
</script>
<style lang="scss" scoped>
.application-card-add {
width: 100%;
font-size: 14px;
min-height: var(--card-min-height);
border: 1px dashed var(--el-border-color);
background: var(--el-disabled-bg-color);
border-radius: 8px;
box-sizing: border-box;
&:hover {
border: 1px solid var(--el-card-bg-color);
background-color: var(--el-card-bg-color);
}
.card-add-button {
&:hover {
border-radius: 4px;
background: var(--app-text-color-light-1);
}
:deep(.el-upload) {
display: block;
width: 100%;
color: var(--el-text-color-regular);
}
}
}
.application-card {
.status-tag {
position: absolute;
right: 16px;
top: 15px;
}
}
.dropdown-custom-switch {
padding: 5px 11px;
font-size: 14px;
font-weight: 400;
span {
margin-right: 26px;
}
}
</style>

View File

@ -0,0 +1,84 @@
<template>
<el-dialog
:modelValue="show"
modal-class="positioned-mask"
width="300"
:title="$t('chat.passwordValidator.title')"
custom-class="no-close-button"
:close-on-click-modal="false"
:close-on-press-escape="false"
:show-close="false"
top="25vh"
center
:modal="true"
>
<el-form ref="FormRef" :model="form" @submit.prevent="validator">
<el-form-item prop="value" :rules="rules.value">
<el-input show-password v-model="form.value" />
</el-form-item>
<el-button class="w-full mt-8" type="primary" @click="validator" :loading="loading">
{{ $t('common.confirm') }}</el-button
>
</el-form>
</el-dialog>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
import { useRoute } from 'vue-router'
import useStore from '@/stores'
import { t } from '@/locales'
const route = useRoute()
const FormRef = ref()
const {
params: { accessToken }
} = route as any
const { application } = useStore()
const props = defineProps<{ applicationProfile: any; modelValue: boolean }>()
const loading = ref<boolean>(false)
const show = computed(() => {
if (props.applicationProfile) {
if (props.modelValue) {
return false
}
return props.applicationProfile.authentication
}
return false
})
const emit = defineEmits(['update:modelValue'])
const auth = () => {
return application.asyncAppAuthentication(accessToken, loading, form.value).then(() => {
emit('update:modelValue', true)
})
}
const validator_auth = (rule: any, value: string, callback: any) => {
if (value === '') {
callback(new Error(t('chat.passwordValidator.errorMessage1')))
} else {
auth().catch(() => {
callback(new Error(t('chat.passwordValidator.errorMessage2')))
})
}
}
const validator = () => {
FormRef.value.validate()
}
const rules = {
value: [{ required: true, validator: validator_auth, trigger: 'manual' }]
}
const form = ref({
type: 'password',
value: ''
})
</script>
<style lang="scss">
.positioned-mask {
top: var(--app-header-height);
height: calc(100% - var(--app-header-height));
.el-overlay-dialog {
top: var(--app-header-height);
height: calc(100% - var(--app-header-height));
}
}
</style>

View File

@ -0,0 +1,66 @@
<template>
<div class="chat-pc__header" :style="customStyle">
<div class="flex align-center">
<div class="mr-12 ml-24 flex">
<AppAvatar
v-if="isAppIcon(application_profile?.icon)"
shape="square"
:size="32"
style="background: none"
>
<img :src="application_profile?.icon" alt="" />
</AppAvatar>
<AppAvatar
v-else-if="application_profile?.name"
:name="application_profile?.name"
pinyinColor
shape="square"
:size="32"
/>
</div>
<h4>{{ application_profile?.name }}</h4>
</div>
</div>
<component
:is="auth_components[`/src/views/chat/auth/component/${auth_type}.vue`].default"
v-model="is_auth"
:applicationProfile="application_profile"
/>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { isAppIcon } from '@/utils/common'
const auth_components: any = import.meta.glob('@/views/chat/auth/component/*.vue', {
eager: true
})
const emit = defineEmits(['update:modelValue'])
const props = withDefaults(
defineProps<{ modelValue: boolean; application_profile: any; auth_type?: string; style?: any }>(),
{
auth_type: 'password',
style: {}
}
)
const is_auth = computed({
get: () => {
return props.modelValue
},
set: (v) => {
emit('update:modelValue', v)
}
})
const customStyle = computed(() => {
return {
background: props.application_profile?.custom_theme?.theme_color,
color: props.application_profile?.custom_theme?.header_font_color,
border: 'none',
...props.style
}
})
</script>
<style lang="scss"></style>

View File

@ -0,0 +1,106 @@
<template>
<div class="chat layout-bg" v-loading="loading">
<div class="chat__header" :class="!isDefaultTheme ? 'custom-header' : ''">
<div class="chat-width flex align-center">
<div class="mr-12 ml-24 flex">
<AppAvatar
v-if="isAppIcon(applicationDetail?.icon)"
shape="square"
:size="32"
style="background: none"
>
<img :src="applicationDetail?.icon" alt="" />
</AppAvatar>
<AppAvatar
v-else-if="applicationDetail?.name"
:name="applicationDetail?.name"
pinyinColor
shape="square"
:size="32"
/>
</div>
<h2>{{ applicationDetail?.name }}</h2>
</div>
</div>
<div class="chat__main chat-width">
<AiChat
v-model:applicationDetails="applicationDetail"
type="ai-chat"
:available="applicationAvailable"
:appId="applicationDetail?.id"
:record="recordList"
:chatId="currentChatId"
@refresh="refresh"
>
<template #operateBefore>
<div>
<el-button type="primary" link class="new-chat-button mb-8" @click="newChat">
<el-icon><Plus /></el-icon><span class="ml-4">{{ $t('chat.createChat') }}</span>
</el-button>
</div>
</template>
</AiChat>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
import { isAppIcon } from '@/utils/common'
import useStore from '@/stores'
const { user } = useStore()
const isDefaultTheme = computed(() => {
return user.isDefaultTheme()
})
const loading = ref(false)
const props = defineProps<{
application_profile: any
applicationAvailable: boolean
}>()
const applicationDetail = computed({
get: () => {
return props.application_profile
},
set: (v) => {}
})
const recordList = ref([])
const currentChatId = ref('')
function newChat() {
currentChatId.value = 'new'
recordList.value = []
}
function refresh(id: string) {
currentChatId.value = id
}
</script>
<style lang="scss">
.chat {
overflow: hidden;
&__header {
background: var(--app-header-bg-color);
position: fixed;
width: 100%;
left: 0;
top: 0;
z-index: 100;
height: var(--app-header-height);
line-height: var(--app-header-height);
box-sizing: border-box;
border-bottom: 1px solid var(--el-border-color);
}
&__main {
padding-top: calc(var(--app-header-height) + 24px);
height: calc(100vh - var(--app-header-height) - 24px);
overflow: hidden;
}
.chat-width {
// max-width: 80%;
margin: 0 auto;
}
}
</style>

View File

@ -0,0 +1,404 @@
<template>
<div
class="chat-embed layout-bg"
:class="{ 'chat-embed--popup': isPopup }"
v-loading="loading"
:style="{
'--el-color-primary': applicationDetail?.custom_theme?.theme_color,
'--el-color-primary-light-9': hexToRgba(applicationDetail?.custom_theme?.theme_color, 0.1)
}"
>
<div class="chat-embed__header" :style="customStyle">
<div class="flex align-center">
<div class="mr-12 ml-24 flex">
<AppAvatar
v-if="isAppIcon(applicationDetail?.icon)"
shape="square"
:size="32"
style="background: none"
>
<img :src="applicationDetail?.icon" alt="" />
</AppAvatar>
<AppAvatar
v-else-if="applicationDetail?.name"
:name="applicationDetail?.name"
pinyinColor
shape="square"
:size="32"
/>
</div>
<h4>{{ applicationDetail?.name }}</h4>
</div>
</div>
<div>
<div class="chat-embed__main">
<AiChat
ref="AiChatRef"
v-model:applicationDetails="applicationDetail"
:available="applicationAvailable"
:appId="applicationDetail?.id"
:record="currentRecordList"
:chatId="currentChatId"
type="ai-chat"
@refresh="refresh"
@scroll="handleScroll"
class="AiChat-embed"
>
<template #operateBefore>
<div>
<el-button type="primary" link class="new-chat-button mb-8" @click="newChat">
<el-icon><Plus /></el-icon><span class="ml-4">{{ $t('chat.createChat') }}</span>
</el-button>
</div>
</template>
</AiChat>
</div>
<!-- 历史记录弹出层 -->
<div
v-if="applicationDetail.show_history || !user.isEnterprise()"
@click.prevent.stop="show = !show"
class="chat-popover-button cursor color-secondary"
>
<AppIcon
iconName="app-history-outlined"
:style="{
color: applicationDetail?.custom_theme?.header_font_color
}"
></AppIcon>
</div>
<el-collapse-transition>
<div v-show="show" class="chat-popover w-full" v-click-outside="clickoutside">
<div class="border-b p-16-24">
<span>{{ $t('chat.history') }}</span>
</div>
<el-scrollbar max-height="300">
<div class="p-8">
<common-list
:style="{ '--el-color-primary': applicationDetail?.custom_theme?.theme_color }"
:data="chatLogData"
v-loading="left_loading"
:defaultActive="currentChatId"
@click="clickListHandle"
@mouseenter="mouseenter"
@mouseleave="mouseId = ''"
>
<template #default="{ row }">
<div class="flex-between">
<ReadWrite
@change="editName($event, row)"
:data="row.abstract"
trigger="manual"
:write="row.writeStatus"
@close="closeWrite(row)"
:maxlength="1024"
/>
<div
@click.stop
v-if="mouseId === row.id && row.id !== 'new' && !row.writeStatus"
class="flex"
>
<el-button style="padding: 0" link @click.stop="openWrite(row)">
<el-icon><EditPen /></el-icon>
</el-button>
<el-button style="padding: 0" link @click.stop="deleteLog(row)">
<el-icon><Delete /></el-icon>
</el-button>
</div>
</div>
</template>
<template #empty>
<div class="text-center mt-24">
<el-text type="info">{{ $t('chat.noHistory') }}</el-text>
</div>
</template>
</common-list>
</div>
<div v-if="chatLogData.length" class="gradient-divider lighter mt-8">
<span>{{ $t('chat.only20history') }}</span>
</div>
</el-scrollbar>
</div>
</el-collapse-transition>
<div class="chat-popover-mask" v-show="show"></div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, reactive, nextTick, computed } from 'vue'
import { useRoute } from 'vue-router'
import { isAppIcon } from '@/utils/common'
import { hexToRgba } from '@/utils/theme'
import { MsgError } from '@/utils/message'
import useStore from '@/stores'
import { t } from '@/locales'
const { user, log } = useStore()
const route = useRoute()
const isPopup = computed(() => {
return route.query.popup !== 'no'
})
const AiChatRef = ref()
const loading = ref(false)
const left_loading = ref(false)
const chatLogData = ref<any[]>([])
const show = ref(false)
const props = defineProps<{
application_profile: any
applicationAvailable: boolean
}>()
const applicationDetail = computed({
get: () => {
return props.application_profile
},
set: (v) => {}
})
const paginationConfig = reactive({
current_page: 1,
page_size: 20,
total: 0
})
const currentRecordList = ref<any>([])
const currentChatId = ref('new') // Id 'new'
const mouseId = ref('')
const customStyle = computed(() => {
return {
background: applicationDetail.value?.custom_theme?.theme_color,
color: applicationDetail.value?.custom_theme?.header_font_color
}
})
function editName(val: string, item: any) {
if (val) {
const obj = {
abstract: val
}
log.asyncPutChatClientLog(applicationDetail.value.id, item.id, obj, loading).then(() => {
const find = chatLogData.value.find((row: any) => row.id === item.id)
if (find) {
find.abstract = val
}
item['writeStatus'] = false
})
} else {
MsgError(t('views.applicationWorkflow.tip.nameMessage'))
}
}
function openWrite(item: any) {
item['writeStatus'] = true
}
function closeWrite(item: any) {
item['writeStatus'] = false
}
function mouseenter(row: any) {
mouseId.value = row.id
}
function deleteLog(row: any) {
log.asyncDelChatClientLog(applicationDetail.value.id, row.id, left_loading).then(() => {
if (currentChatId.value === row.id) {
currentChatId.value = 'new'
paginationConfig.current_page = 1
paginationConfig.total = 0
currentRecordList.value = []
}
getChatLog(applicationDetail.value.id)
})
}
function handleScroll(event: any) {
if (
currentChatId.value !== 'new' &&
event.scrollTop === 0 &&
paginationConfig.total > currentRecordList.value.length
) {
const history_height = event.dialogScrollbar.offsetHeight
paginationConfig.current_page += 1
getChatRecord().then(() => {
event.scrollDiv.setScrollTop(event.dialogScrollbar.offsetHeight - history_height)
})
}
}
function clickoutside() {
show.value = false
}
function newChat() {
paginationConfig.current_page = 1
currentRecordList.value = []
currentChatId.value = 'new'
}
function getChatLog(id: string) {
const page = {
current_page: 1,
page_size: 20
}
log.asyncGetChatLogClient(id, page, left_loading).then((res: any) => {
chatLogData.value = res.data.records
paginationConfig.current_page = 1
paginationConfig.total = 0
currentRecordList.value = []
currentChatId.value = chatLogData.value?.[0]?.id || 'new'
if (currentChatId.value !== 'new') {
getChatRecord()
}
})
}
function getChatRecord() {
return log
.asyncChatRecordLog(
applicationDetail.value.id,
currentChatId.value,
paginationConfig,
loading,
false
)
.then((res: any) => {
paginationConfig.total = res.data.total
const list = res.data.records
list.map((v: any) => {
v['write_ed'] = true
v['record_id'] = v.id
})
currentRecordList.value = [...list, ...currentRecordList.value].sort((a, b) =>
a.create_time.localeCompare(b.create_time)
)
if (paginationConfig.current_page === 1) {
nextTick(() => {
//
AiChatRef.value.setScrollBottom()
})
}
})
}
const clickListHandle = (item: any) => {
if (item.id !== currentChatId.value) {
paginationConfig.current_page = 1
currentRecordList.value = []
currentChatId.value = item.id
if (currentChatId.value !== 'new') {
getChatRecord()
}
show.value = false
}
}
function refresh(id: string) {
getChatLog(applicationDetail.value.id)
currentChatId.value = id
}
/**
*初始化历史对话记录
*/
const init = () => {
if (applicationDetail.value.show_history || !user.isEnterprise()) {
getChatLog(applicationDetail.value.id)
}
}
onMounted(() => {
init()
})
</script>
<style lang="scss">
.chat-embed {
overflow: hidden;
&__header {
background: var(--app-header-bg-color);
position: fixed;
width: 100%;
left: 0;
top: 0;
z-index: 100;
height: var(--app-header-height);
line-height: var(--app-header-height);
box-sizing: border-box;
border-bottom: 1px solid var(--el-border-color);
}
&__main {
padding-top: calc(var(--app-header-height) + 16px);
height: calc(100vh - var(--app-header-height) - 16px);
overflow: hidden;
}
.new-chat-button {
z-index: 11;
}
//
.chat-popover {
position: absolute;
top: var(--app-header-height);
background: #ffffff;
padding-bottom: 24px;
z-index: 2009;
}
.chat-popover-button {
z-index: 2009;
position: absolute;
top: 16px;
right: 16px;
font-size: 22px;
}
&.chat-embed--popup {
.chat-popover-button {
right: 85px;
}
}
.chat-popover-mask {
background-color: var(--el-overlay-color-lighter);
bottom: 0;
height: 100%;
left: 0;
overflow: auto;
position: fixed;
right: 0;
top: var(--app-header-height);
z-index: 2008;
}
.gradient-divider {
position: relative;
text-align: center;
color: var(--el-color-info);
::before {
content: '';
width: 17%;
height: 1px;
background: linear-gradient(90deg, rgba(222, 224, 227, 0) 0%, #dee0e3 100%);
position: absolute;
left: 16px;
top: 50%;
}
::after {
content: '';
width: 17%;
height: 1px;
background: linear-gradient(90deg, #dee0e3 0%, rgba(222, 224, 227, 0) 100%);
position: absolute;
right: 16px;
top: 50%;
}
}
.AiChat-embed {
.ai-chat__operate {
padding-top: 12px;
}
}
}
</style>
<style lang="scss" scoped>
:deep(.el-overlay) {
background-color: transparent;
}
</style>

103
ui/src/views/chat/index.vue Normal file
View File

@ -0,0 +1,103 @@
<template>
<component
v-if="chat_show && init_data_end"
:applicationAvailable="applicationAvailable"
:is="currentTemplate"
:application_profile="application_profile"
:key="route.fullPath"
v-loading="loading"
/>
<Auth
v-else
:application_profile="application_profile"
:auth_type="application_profile.authentication_type"
v-model="is_auth"
:style="{
'--el-color-primary': application_profile?.custom_theme?.theme_color,
'--el-color-primary-light-9': hexToRgba(application_profile?.custom_theme?.theme_color, 0.1)
}"
></Auth>
</template>
<script setup lang="ts">
import { ref, onBeforeMount, computed } from 'vue'
import { useRoute } from 'vue-router'
import useStore from '@/stores'
import Auth from '@/views/chat/auth/index.vue'
import { hexToRgba } from '@/utils/theme'
import { useI18n } from 'vue-i18n'
import { getBrowserLang } from '@/locales/index'
const { locale } = useI18n({ useScope: 'global' })
const route = useRoute()
const { application, user } = useStore()
const components: any = import.meta.glob('@/views/chat/**/index.vue', {
eager: true
})
const {
query: { mode },
params: { accessToken }
} = route as any
const is_auth = ref<boolean>(false)
const currentTemplate = computed(() => {
let modeName = ''
if (!mode || mode === 'pc') {
modeName = show_history.value || !user.isEnterprise() ? 'pc' : 'base'
} else {
modeName = mode
}
const name = `/src/views/chat/${modeName}/index.vue`
return components[name].default
})
/**
* 是否显示对话
*/
const chat_show = computed(() => {
if (init_data_end.value) {
if (!applicationAvailable.value) {
return true
}
if (application_profile.value) {
if (application_profile.value.authentication && is_auth.value) {
return true
} else if (!application_profile.value.authentication) {
return true
}
}
}
return false
})
const loading = ref(false)
const show_history = ref(false)
const application_profile = ref<any>({})
/**
* 初始化结束
*/
const init_data_end = ref<boolean>(false)
const applicationAvailable = ref<boolean>(true)
function getAppProfile() {
return application.asyncGetAppProfile(loading).then((res: any) => {
locale.value = res.data?.language || getBrowserLang()
show_history.value = res.data?.show_history
application_profile.value = res.data
})
}
function getAccessToken(token: string) {
return application.asyncAppAuthentication(token, loading).then(() => {
return getAppProfile()
})
}
onBeforeMount(() => {
user.changeUserType(2, accessToken)
Promise.all([user.asyncGetProfile(), getAccessToken(accessToken)])
.catch(() => {
applicationAvailable.value = false
})
.finally(() => (init_data_end.value = true))
})
</script>
<style lang="scss"></style>

View File

@ -0,0 +1,401 @@
<template>
<div
class="chat-mobile layout-bg"
v-loading="loading"
:style="{
'--el-color-primary': applicationDetail?.custom_theme?.theme_color,
'--el-color-primary-light-9': hexToRgba(applicationDetail?.custom_theme?.theme_color, 0.1)
}"
>
<div class="chat-embed__header" :style="customStyle">
<div class="flex align-center">
<div class="mr-12 ml-24 flex">
<AppAvatar
v-if="isAppIcon(applicationDetail?.icon)"
shape="square"
:size="32"
style="background: none"
>
<img :src="applicationDetail?.icon" alt="" />
</AppAvatar>
<AppAvatar
v-else-if="applicationDetail?.name"
:name="applicationDetail?.name"
pinyinColor
shape="square"
:size="32"
/>
</div>
<h4>{{ applicationDetail?.name }}</h4>
</div>
</div>
<div>
<div class="chat-embed__main">
<AiChat
ref="AiChatRef"
v-model:applicationDetails="applicationDetail"
:available="applicationAvailable"
:appId="applicationDetail?.id"
:record="currentRecordList"
:chatId="currentChatId"
type="ai-chat"
@refresh="refresh"
@scroll="handleScroll"
class="AiChat-embed"
>
<template #operateBefore>
<div>
<el-button type="primary" link class="new-chat-button mb-8" @click="newChat">
<el-icon><Plus /></el-icon><span class="ml-4">{{ $t('chat.createChat') }}</span>
</el-button>
</div>
</template>
</AiChat>
</div>
<!-- 历史记录弹出层 -->
<div
v-if="applicationDetail.show_history || !user.isEnterprise()"
@click.prevent.stop="show = !show"
class="chat-popover-button cursor color-secondary"
>
<AppIcon
iconName="app-history-outlined"
:style="{
color: applicationDetail?.custom_theme?.header_font_color
}"
></AppIcon>
</div>
<el-collapse-transition>
<div v-show="show" class="chat-popover w-full" v-click-outside="clickoutside">
<div class="border-b p-16-24">
<span>{{ $t('chat.history') }}</span>
</div>
<el-scrollbar max-height="300">
<div class="p-8">
<common-list
:style="{ '--el-color-primary': applicationDetail?.custom_theme?.theme_color }"
:data="chatLogData"
v-loading="left_loading"
:defaultActive="currentChatId"
@click="clickListHandle"
@mouseenter="mouseenter"
@mouseleave="mouseId = ''"
>
<template #default="{ row }">
<div class="flex-between">
<ReadWrite
@change="editName($event, row)"
:data="row.abstract"
trigger="manual"
:write="row.writeStatus"
@close="closeWrite(row)"
:maxlength="1024"
/>
<div
@click.stop
v-if="mouseId === row.id && row.id !== 'new' && !row.writeStatus"
class="flex"
>
<el-button style="padding: 0" link @click.stop="openWrite(row)">
<el-icon><EditPen /></el-icon>
</el-button>
<el-button style="padding: 0" link @click.stop="deleteLog(row)">
<el-icon><Delete /></el-icon>
</el-button>
</div>
</div>
</template>
<template #empty>
<div class="text-center mt-24">
<el-text type="info">{{ $t('chat.noHistory') }}</el-text>
</div>
</template>
</common-list>
</div>
<div v-if="chatLogData.length" class="gradient-divider lighter mt-8">
<span>{{ $t('chat.only20history') }}</span>
</div>
</el-scrollbar>
</div>
</el-collapse-transition>
<div class="chat-popover-mask" v-show="show"></div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, reactive, nextTick, computed } from 'vue'
import { useRoute } from 'vue-router'
import { isAppIcon } from '@/utils/common'
import { hexToRgba } from '@/utils/theme'
import { MsgError } from '@/utils/message'
import useStore from '@/stores'
import { t } from '@/locales'
const { user, log } = useStore()
const AiChatRef = ref()
const loading = ref(false)
const left_loading = ref(false)
const chatLogData = ref<any[]>([])
const show = ref(false)
const props = defineProps<{
application_profile: any
applicationAvailable: boolean
}>()
const applicationDetail = computed({
get: () => {
return props.application_profile
},
set: (v) => {}
})
const paginationConfig = reactive({
current_page: 1,
page_size: 20,
total: 0
})
const currentRecordList = ref<any>([])
const currentChatId = ref('new') // Id 'new'
const mouseId = ref('')
const customStyle = computed(() => {
return {
background: applicationDetail.value?.custom_theme?.theme_color,
color: applicationDetail.value?.custom_theme?.header_font_color
}
})
function editName(val: string, item: any) {
if (val) {
const obj = {
abstract: val
}
log.asyncPutChatClientLog(applicationDetail.value.id, item.id, obj, loading).then(() => {
const find = chatLogData.value.find((row: any) => row.id === item.id)
if (find) {
find.abstract = val
}
item['writeStatus'] = false
})
} else {
MsgError(t('views.applicationWorkflow.tip.nameMessage'))
}
}
function openWrite(item: any) {
item['writeStatus'] = true
}
function closeWrite(item: any) {
item['writeStatus'] = false
}
function mouseenter(row: any) {
mouseId.value = row.id
}
function deleteLog(row: any) {
log.asyncDelChatClientLog(applicationDetail.value.id, row.id, left_loading).then(() => {
if (currentChatId.value === row.id) {
currentChatId.value = 'new'
paginationConfig.current_page = 1
paginationConfig.total = 0
currentRecordList.value = []
}
getChatLog(applicationDetail.value.id)
})
}
function handleScroll(event: any) {
if (
currentChatId.value !== 'new' &&
event.scrollTop === 0 &&
paginationConfig.total > currentRecordList.value.length
) {
const history_height = event.dialogScrollbar.offsetHeight
paginationConfig.current_page += 1
getChatRecord().then(() => {
event.scrollDiv.setScrollTop(event.dialogScrollbar.offsetHeight - history_height)
})
}
}
function clickoutside() {
show.value = false
}
function newChat() {
paginationConfig.current_page = 1
currentRecordList.value = []
currentChatId.value = 'new'
}
function getChatLog(id: string) {
const page = {
current_page: 1,
page_size: 20
}
log.asyncGetChatLogClient(id, page, left_loading).then((res: any) => {
chatLogData.value = res.data.records
paginationConfig.current_page = 1
paginationConfig.total = 0
currentRecordList.value = []
currentChatId.value = chatLogData.value?.[0]?.id || 'new'
if (currentChatId.value !== 'new') {
getChatRecord()
}
})
}
function getChatRecord() {
return log
.asyncChatRecordLog(
applicationDetail.value.id,
currentChatId.value,
paginationConfig,
loading,
false
)
.then((res: any) => {
paginationConfig.total = res.data.total
const list = res.data.records
list.map((v: any) => {
v['write_ed'] = true
v['record_id'] = v.id
})
currentRecordList.value = [...list, ...currentRecordList.value].sort((a, b) =>
a.create_time.localeCompare(b.create_time)
)
if (paginationConfig.current_page === 1) {
nextTick(() => {
//
AiChatRef.value.setScrollBottom()
})
}
})
}
const clickListHandle = (item: any) => {
if (item.id !== currentChatId.value) {
paginationConfig.current_page = 1
currentRecordList.value = []
currentChatId.value = item.id
if (currentChatId.value !== 'new') {
getChatRecord()
}
show.value = false
}
}
function refresh(id: string) {
getChatLog(applicationDetail.value.id)
currentChatId.value = id
}
/**
*初始化历史对话记录
*/
const init = () => {
if (applicationDetail.value.show_history || !user.isEnterprise()) {
getChatLog(applicationDetail.value.id)
}
}
onMounted(() => {
init()
})
</script>
<style lang="scss">
.chat-mobile {
overflow: hidden;
&__header {
background: var(--app-header-bg-color);
position: fixed;
width: 100%;
left: 0;
top: 0;
z-index: 100;
height: var(--app-header-height);
line-height: var(--app-header-height);
box-sizing: border-box;
border-bottom: 1px solid var(--el-border-color);
}
&__main {
padding-top: calc(var(--app-header-height) + 16px);
height: calc(100vh - var(--app-header-height) - 16px);
overflow: hidden;
}
.new-chat-button {
z-index: 11;
font-size: 1rem;
}
//
.chat-popover {
position: fixed;
top: var(--app-header-height);
background: #ffffff;
padding-bottom: 24px;
z-index: 2009;
}
.chat-popover-button {
z-index: 2009;
position: fixed;
top: 16px;
right: 16px;
font-size: 22px;
}
// &.chat-embed--popup {
// .chat-popover-button {
// right: 85px;
// }
// }
.chat-popover-mask {
background-color: var(--el-overlay-color-lighter);
bottom: 0;
height: 100%;
left: 0;
overflow: auto;
position: fixed;
right: 0;
top: var(--app-header-height);
z-index: 2008;
}
.gradient-divider {
position: relative;
text-align: center;
color: var(--el-color-info);
::before {
content: '';
width: 17%;
height: 1px;
background: linear-gradient(90deg, rgba(222, 224, 227, 0) 0%, #dee0e3 100%);
position: absolute;
left: 16px;
top: 50%;
}
::after {
content: '';
width: 17%;
height: 1px;
background: linear-gradient(90deg, #dee0e3 0%, rgba(222, 224, 227, 0) 100%);
position: absolute;
right: 16px;
top: 50%;
}
}
// .AiChat-embed {
// .ai-chat__operate {
// padding-top: 12px;
// }
// }
}
</style>
<style lang="scss" scoped>
:deep(.el-overlay) {
background-color: transparent;
}
</style>

View File

@ -0,0 +1,87 @@
<template>
<el-dialog
class="responsive-dialog"
:title="$t('chat.editTitle')"
v-model="dialogVisible"
:close-on-click-modal="false"
:close-on-press-escape="false"
:destroy-on-close="true"
append-to-body
>
<el-form
label-position="top"
ref="fieldFormRef"
:model="form"
require-asterisk-position="right"
>
<el-form-item
prop="abstract"
:rules="[
{
required: true,
message: $t('common.inputPlaceholder'),
trigger: 'blur'
}
]"
>
<el-input
v-model="form.abstract"
maxlength="1024"
show-word-limit
type="textarea"
@blur="form.abstract = form.abstract.trim()"
/>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click.prevent="dialogVisible = false"> {{ $t('common.cancel') }} </el-button>
<el-button type="primary" @click="submit(fieldFormRef)" :loading="loading">
{{ $t('common.save') }}
</el-button>
</span>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { reactive, ref, watch } from 'vue'
import type { FormInstance } from 'element-plus'
import useStore from '@/stores'
import { t } from '@/locales'
const { log } = useStore()
const emit = defineEmits(['refresh'])
const fieldFormRef = ref()
const loading = ref<boolean>(false)
const applicationId = ref<string>('')
const chatId = ref<string>('')
const form = ref<any>({
abstract: ''
})
const dialogVisible = ref<boolean>(false)
const open = (row: any, id: string) => {
applicationId.value = id
chatId.value = row.id
form.value.abstract = row.abstract
dialogVisible.value = true
}
const submit = async (formEl: FormInstance | undefined) => {
if (!formEl) return
await formEl.validate((valid) => {
if (valid) {
log.asyncPutChatClientLog(applicationId.value, chatId.value, form.value, loading).then(() => {
emit('refresh', chatId.value, form.value.abstract)
dialogVisible.value = false
})
}
})
}
defineExpose({ open, close })
</script>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,527 @@
<template>
<div
class="chat-pc layout-bg"
:class="classObj"
v-loading="loading"
:style="{
'--el-color-primary': applicationDetail?.custom_theme?.theme_color,
'--el-color-primary-light-9': hexToRgba(applicationDetail?.custom_theme?.theme_color, 0.1)
}"
>
<div class="chat-pc__header" :style="customStyle">
<div class="flex align-center">
<div class="mr-12 ml-24 flex">
<AppAvatar
v-if="isAppIcon(applicationDetail?.icon)"
shape="square"
:size="32"
style="background: none"
>
<img :src="applicationDetail?.icon" alt="" />
</AppAvatar>
<AppAvatar
v-else-if="applicationDetail?.name"
:name="applicationDetail?.name"
pinyinColor
shape="square"
:size="32"
/>
</div>
<h4>{{ applicationDetail?.name }}</h4>
</div>
</div>
<div>
<div class="flex">
<div class="chat-pc__left border-r">
<div class="p-24 pb-0">
<el-button class="add-button w-full primary" @click="newChat">
<el-icon>
<Plus />
</el-icon>
<span class="ml-4">{{ $t('chat.createChat') }}</span>
</el-button>
<p class="mt-20 mb-8">{{ $t('chat.history') }}</p>
</div>
<div class="left-height pt-0">
<el-scrollbar>
<div class="p-8 pt-0">
<common-list
:style="{
'--el-color-primary': applicationDetail?.custom_theme?.theme_color,
'--el-color-primary-light-9': hexToRgba(
applicationDetail?.custom_theme?.theme_color,
0.1
)
}"
:data="chatLogData"
class="mt-8"
v-loading="left_loading"
:defaultActive="currentChatId"
@click="clickListHandle"
@mouseenter="mouseenter"
@mouseleave="mouseId = ''"
>
<template #default="{ row }">
<div class="flex-between">
<auto-tooltip :content="row.abstract">
{{ row.abstract }}
</auto-tooltip>
<div @click.stop v-show="mouseId === row.id && row.id !== 'new'">
<el-dropdown trigger="click" :teleported="false">
<el-icon class="rotate-90 mt-4"><MoreFilled /></el-icon>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item @click.stop="editLogTitle(row)">
<el-icon><EditPen /></el-icon>
{{ $t('common.edit') }}
</el-dropdown-item>
<el-dropdown-item @click.stop="deleteLog(row)">
<el-icon><Delete /></el-icon>
{{ $t('common.delete') }}
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</div>
</template>
<template #empty>
<div class="text-center">
<el-text type="info">{{ $t('chat.noHistory') }}</el-text>
</div>
</template>
</common-list>
</div>
<div v-if="chatLogData?.length" class="gradient-divider lighter mt-8">
<span>{{ $t('chat.only20history') }}</span>
</div>
</el-scrollbar>
</div>
</div>
<div class="chat-pc__right">
<div class="right-header border-b mb-24 p-16-24 flex-between">
<h4 class="ellipsis-1" style="width: 66%">
{{ currentChatName }}
</h4>
<span class="flex align-center" v-if="currentRecordList.length">
<AppIcon
v-if="paginationConfig.total"
iconName="app-chat-record"
class="info mr-8"
style="font-size: 16px"
></AppIcon>
<span v-if="paginationConfig.total" class="lighter">
{{ paginationConfig.total }} {{ $t('chat.question_count') }}
</span>
<el-dropdown class="ml-8">
<AppIcon
iconName="app-export"
class="cursor"
:title="$t('chat.exportRecords')"
></AppIcon>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item @click="exportMarkdown"
>{{ $t('common.export') }} Markdown</el-dropdown-item
>
<el-dropdown-item @click="exportHTML"
>{{ $t('common.export') }} HTML</el-dropdown-item
>
</el-dropdown-menu>
</template>
</el-dropdown>
</span>
</div>
<div class="right-height chat-width">
<AiChat
ref="AiChatRef"
v-model:applicationDetails="applicationDetail"
:available="applicationAvailable"
type="ai-chat"
:appId="applicationDetail?.id"
:record="currentRecordList"
:chatId="currentChatId"
@refresh="refresh"
@scroll="handleScroll"
>
</AiChat>
</div>
</div>
</div>
<div class="collapse">
<el-button @click="isCollapse = !isCollapse">
<el-icon> <component :is="isCollapse ? 'Fold' : 'Expand'" /></el-icon>
</el-button>
</div>
</div>
<EditTitleDialog ref="EditTitleDialogRef" @refresh="refreshFieldTitle" />
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, nextTick, computed } from 'vue'
import { marked } from 'marked'
import { saveAs } from 'file-saver'
import { isAppIcon } from '@/utils/common'
import useStore from '@/stores'
import useResize from '@/layout/hooks/useResize'
import { hexToRgba } from '@/utils/theme'
import EditTitleDialog from './EditTitleDialog.vue'
import { t } from '@/locales'
useResize()
const { user, log, common } = useStore()
const EditTitleDialogRef = ref()
const isCollapse = ref(false)
const customStyle = computed(() => {
return {
background: applicationDetail.value?.custom_theme?.theme_color,
color: applicationDetail.value?.custom_theme?.header_font_color
}
})
const classObj = computed(() => {
return {
mobile: common.isMobile(),
hideLeft: !isCollapse.value,
openLeft: isCollapse.value
}
})
const newObj = {
id: 'new',
abstract: t('chat.createChat')
}
const props = defineProps<{
application_profile: any
applicationAvailable: boolean
}>()
const AiChatRef = ref()
const loading = ref(false)
const left_loading = ref(false)
const applicationDetail = computed({
get: () => {
return props.application_profile
},
set: (v) => {}
})
const chatLogData = ref<any[]>([])
const paginationConfig = ref({
current_page: 1,
page_size: 20,
total: 0
})
const currentRecordList = ref<any>([])
const currentChatId = ref('new') // Id 'new'
const currentChatName = ref(t('chat.createChat'))
const mouseId = ref('')
function mouseenter(row: any) {
mouseId.value = row.id
}
function editLogTitle(row: any) {
EditTitleDialogRef.value.open(row, applicationDetail.value.id)
}
function refreshFieldTitle(chatId: string, abstract: string) {
const find = chatLogData.value.find((item: any) => item.id == chatId)
if (find) {
find.abstract = abstract
}
}
function deleteLog(row: any) {
log.asyncDelChatClientLog(applicationDetail.value.id, row.id, left_loading).then(() => {
if (currentChatId.value === row.id) {
currentChatId.value = 'new'
currentChatName.value = t('chat.createChat')
paginationConfig.value.current_page = 1
paginationConfig.value.total = 0
currentRecordList.value = []
}
getChatLog(applicationDetail.value.id)
})
}
function handleScroll(event: any) {
if (
currentChatId.value !== 'new' &&
event.scrollTop === 0 &&
paginationConfig.value.total > currentRecordList.value.length
) {
const history_height = event.dialogScrollbar.offsetHeight
paginationConfig.value.current_page += 1
getChatRecord().then(() => {
event.scrollDiv.setScrollTop(event.dialogScrollbar.offsetHeight - history_height)
})
}
}
function newChat() {
if (!chatLogData.value.some((v) => v.id === 'new')) {
paginationConfig.value.current_page = 1
paginationConfig.value.total = 0
currentRecordList.value = []
chatLogData.value.unshift(newObj)
} else {
paginationConfig.value.current_page = 1
paginationConfig.value.total = 0
currentRecordList.value = []
}
currentChatId.value = 'new'
currentChatName.value = t('chat.createChat')
if (common.isMobile()) {
isCollapse.value = false
}
}
function getChatLog(id: string, refresh?: boolean) {
const page = {
current_page: 1,
page_size: 20
}
log.asyncGetChatLogClient(id, page, left_loading).then((res: any) => {
chatLogData.value = res.data.records
if (refresh) {
currentChatName.value = chatLogData.value?.[0]?.abstract
} else {
paginationConfig.value.current_page = 1
paginationConfig.value.total = 0
currentRecordList.value = []
currentChatId.value = chatLogData.value?.[0]?.id || 'new'
currentChatName.value = chatLogData.value?.[0]?.abstract || t('chat.createChat')
if (currentChatId.value !== 'new') {
getChatRecord()
}
}
})
}
function getChatRecord() {
return log
.asyncChatRecordLog(
applicationDetail.value.id,
currentChatId.value,
paginationConfig.value,
loading,
false
)
.then((res: any) => {
paginationConfig.value.total = res.data.total
const list = res.data.records
list.map((v: any) => {
v['write_ed'] = true
v['record_id'] = v.id
})
currentRecordList.value = [...list, ...currentRecordList.value].sort((a, b) =>
a.create_time.localeCompare(b.create_time)
)
if (paginationConfig.value.current_page === 1) {
nextTick(() => {
//
AiChatRef.value.setScrollBottom()
})
}
})
}
const clickListHandle = (item: any) => {
if (item.id !== currentChatId.value) {
paginationConfig.value.current_page = 1
paginationConfig.value.total = 0
currentRecordList.value = []
currentChatId.value = item.id
currentChatName.value = item.abstract
if (currentChatId.value !== 'new') {
getChatRecord()
//
if (window.speechSynthesis.paused && window.speechSynthesis.speaking) {
window.speechSynthesis.resume()
nextTick(() => {
window.speechSynthesis.cancel()
})
}
}
}
if (common.isMobile()) {
isCollapse.value = false
}
}
function refresh(id: string) {
getChatLog(applicationDetail.value.id, true)
currentChatId.value = id
}
async function exportMarkdown(): Promise<void> {
const suggestedName: string = `${currentChatId.value}.md`
const markdownContent: string = currentRecordList.value
.map((record: any) => `# ${record.problem_text}\n\n${record.answer_text}\n\n`)
.join('\n')
const blob: Blob = new Blob([markdownContent], { type: 'text/markdown;charset=utf-8' })
saveAs(blob, suggestedName)
}
async function exportHTML(): Promise<void> {
const suggestedName: string = `${currentChatId.value}.html`
const markdownContent: string = currentRecordList.value
.map((record: any) => `# ${record.problem_text}\n\n${record.answer_text}\n\n`)
.join('\n')
const htmlContent: any = marked(markdownContent)
const blob: Blob = new Blob([htmlContent], { type: 'text/html;charset=utf-8' })
saveAs(blob, suggestedName)
}
/**
*初始化历史对话记录
*/
const init = () => {
if (
(applicationDetail.value.show_history || !user.isEnterprise()) &&
props.applicationAvailable
) {
getChatLog(applicationDetail.value.id)
}
}
onMounted(() => {
init()
})
</script>
<style lang="scss">
.chat-pc {
overflow: hidden;
&__header {
background: var(--app-header-bg-color);
position: fixed;
width: 100%;
left: 0;
top: 0;
z-index: 100;
height: var(--app-header-height);
line-height: var(--app-header-height);
box-sizing: border-box;
border-bottom: 1px solid var(--el-border-color);
}
&__left {
padding-top: calc(var(--app-header-height) - 8px);
background: #ffffff;
width: 280px;
.add-button {
border: 1px solid var(--el-color-primary);
}
.left-height {
height: calc(100vh - var(--app-header-height) - 135px);
}
}
&__right {
width: calc(100% - 280px);
padding-top: calc(var(--app-header-height));
overflow: hidden;
position: relative;
box-sizing: border-box;
.right-header {
background: #ffffff;
box-sizing: border-box;
}
.right-height {
height: calc(100vh - var(--app-header-height) * 2 - 24px);
}
}
.gradient-divider {
position: relative;
text-align: center;
color: var(--el-color-info);
::before {
content: '';
width: 17%;
height: 1px;
background: linear-gradient(90deg, rgba(222, 224, 227, 0) 0%, #dee0e3 100%);
position: absolute;
left: 16px;
top: 50%;
}
::after {
content: '';
width: 17%;
height: 1px;
background: linear-gradient(90deg, #dee0e3 0%, rgba(222, 224, 227, 0) 100%);
position: absolute;
right: 16px;
top: 50%;
}
}
.collapse {
display: none;
}
}
//
.mobile {
.chat-pc {
&__right {
width: 100%;
}
&__left {
display: none;
width: 0;
}
}
.collapse {
display: block;
position: fixed;
bottom: 90px;
z-index: 99;
}
&.openLeft {
.chat-pc {
&__left {
display: block;
position: fixed;
width: 100%;
z-index: 99;
height: calc(100vh - var(--app-header-height) + 6px);
}
}
.collapse {
display: block;
position: absolute;
bottom: 90px;
right: 0;
z-index: 99;
}
}
}
.chat-width {
max-width: 80%;
margin: 0 auto;
}
@media only screen and (max-width: 1000px) {
.chat-width {
max-width: 100% !important;
margin: 0 auto;
}
}
</style>

View File

@ -54,7 +54,7 @@
<div class="flex-between">
<div class="flex align-center">
<AppAvatar class="mr-8 avatar-purple" shape="square" :size="32">
<img src="@/assets/icon_web.svg" style="width: 58%" alt="" />
<img src="@/assets/knowledge/icon_web.svg" style="width: 58%" alt="" />
</AppAvatar>
<div>
<p>
@ -106,7 +106,7 @@
<!-- <div class="flex-between">-->
<!-- <div class="flex align-center">-->
<!-- <AppAvatar class="mr-8" :size="32">-->
<!-- <img src="@/assets/icon_web.svg" style="width: 100%" alt="" />-->
<!-- <img src="@/assets/knowledge/icon_web.svg" style="width: 100%" alt="" />-->
<!-- </AppAvatar>-->
<!-- <div>-->
<!-- <p>-->

View File

@ -30,7 +30,7 @@
shape="square"
:size="24"
>
<img src="@/assets/icon_web.svg" style="width: 58%" alt="" />
<img src="@/assets/knowledge/icon_web.svg" style="width: 58%" alt="" />
</AppAvatar>
<AppAvatar
v-else-if="!item.dataset_id && item.type === '2'"
@ -39,7 +39,7 @@
:size="24"
style="background: none"
>
<img src="@/assets/logo_lark.svg" style="width: 100%" alt="" />
<img src="@/assets/knowledge/logo_lark.svg" style="width: 100%" alt="" />
</AppAvatar>
<AppAvatar
v-else-if="!item.dataset_id && item.type === '0'"
@ -47,7 +47,7 @@
shape="square"
:size="24"
>
<img src="@/assets/icon_document.svg" style="width: 58%" alt="" />
<img src="@/assets/knowledge/icon_document.svg" style="width: 58%" alt="" />
</AppAvatar>
{{ item.name }}
</span>

View File

@ -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
/>
<div class="p-24 pt-0">
<div class="pt-0">
<el-table :data="filterData" :max-height="tableHeight">
<el-table-column
prop="name"
:label="
isApplication
? $t('views.application.applicationForm.form.appName.label')
: $t('views.dataset.datasetForm.form.datasetName.label')
"
>
<el-table-column prop="name" :label="$t('common.name')">
<template #default="{ row }">
<div class="flex align-center">
<AppAvatar
<el-avatar
v-if="isApplication && isAppIcon(row?.icon)"
style="background: none"
class="mr-12"
@ -26,9 +19,9 @@
:size="24"
>
<img :src="row?.icon" alt="" />
</AppAvatar>
</el-avatar>
<AppAvatar
<el-avatar
v-else-if="row?.name && isApplication"
:name="row?.name"
pinyinColor
@ -36,26 +29,26 @@
:size="24"
class="mr-12"
/>
<AppAvatar
<el-avatar
v-if="row.icon === '1' && isDataset"
class="mr-8 avatar-purple"
shape="square"
:size="24"
>
<img src="@/assets/icon_web.svg" style="width: 58%" alt="" />
</AppAvatar>
<AppAvatar
<img src="@/assets/knowledge/icon_web.svg" style="width: 58%" alt="" />
</el-avatar>
<el-avatar
v-else-if="row.icon === '2' && isDataset"
class="mr-8 avatar-purple"
shape="square"
:size="24"
style="background: none"
>
<img src="@/assets/logo_lark.svg" style="width: 100%" alt="" />
</AppAvatar>
<AppAvatar v-else-if="isDataset" class="mr-8 avatar-blue" shape="square" :size="24">
<img src="@/assets/icon_document.svg" style="width: 58%" alt="" />
</AppAvatar>
<img src="@/assets/knowledge/logo_lark.svg" style="width: 100%" alt="" />
</el-avatar>
<el-avatar v-else-if="isDataset" class="mr-8 avatar-blue" shape="square" :size="24">
<img src="@/assets/knowledge/icon_document.svg" style="width: 58%" alt="" />
</el-avatar>
<auto-tooltip :content="row?.name">
{{ row?.name }}
</auto-tooltip>
@ -63,7 +56,7 @@
</template>
</el-table-column>
<el-table-column
:label="$t('views.team.setting.management')"
:label="$t('views.resourceAuthorization.setting.management')"
align="center"
width="100"
fixed="right"
@ -71,21 +64,21 @@
<template #header>
<el-checkbox
:disabled="props.manage"
v-model="allChecked[TeamEnum.MANAGE]"
:indeterminate="allIndeterminate[TeamEnum.MANAGE]"
:label="$t('views.team.setting.management')"
v-model="allChecked[AuthorizationEnum.MANAGE]"
:indeterminate="allIndeterminate[AuthorizationEnum.MANAGE]"
:label="$t('views.resourceAuthorization.setting.management')"
/>
</template>
<template #default="{ row }">
<el-checkbox
:disabled="props.manage"
v-model="row.operate[TeamEnum.MANAGE]"
@change="(e: boolean) => checkedOperateChange(TeamEnum.MANAGE, row, e)"
v-model="row.operate[AuthorizationEnum.MANAGE]"
@change="(e: boolean) => checkedOperateChange(AuthorizationEnum.MANAGE, row, e)"
/>
</template>
</el-table-column>
<el-table-column
:label="$t('views.team.setting.check')"
:label="$t('views.resourceAuthorization.setting.check')"
align="center"
width="100"
fixed="right"
@ -93,16 +86,16 @@
<template #header>
<el-checkbox
:disabled="props.manage"
v-model="allChecked[TeamEnum.USE]"
:indeterminate="allIndeterminate[TeamEnum.USE]"
:label="$t('views.team.setting.check')"
v-model="allChecked[AuthorizationEnum.USE]"
:indeterminate="allIndeterminate[AuthorizationEnum.USE]"
:label="$t('views.resourceAuthorization.setting.check')"
/>
</template>
<template #default="{ row }">
<el-checkbox
:disabled="props.manage"
v-model="row.operate[TeamEnum.USE]"
@change="(e: boolean) => checkedOperateChange(TeamEnum.USE, row, e)"
v-model="row.operate[AuthorizationEnum.USE]"
@change="(e: boolean) => checkedOperateChange(AuthorizationEnum.USE, row, e)"
/>
</template>
</el-table-column>
@ -111,92 +104,96 @@
</template>
<script setup lang="ts">
import { ref, onMounted, watch, computed } from 'vue'
import { TeamEnum } from '@/enums/team'
import { isAppIcon } from '@/utils/application'
import { AuthorizationEnum } from '@/enums/system'
import { isAppIcon } from '@/utils/common'
const props = defineProps({
data: {
type: Array,
default: () => []
default: () => [],
},
id: String,
type: String,
tableHeight: Number,
manage: Boolean
manage: Boolean,
})
const isDataset = computed(() => props.type === TeamEnum.DATASET)
const isApplication = computed(() => props.type === TeamEnum.APPLICATION)
const isDataset = computed(() => props.type === AuthorizationEnum.DATASET)
const isApplication = computed(() => props.type === AuthorizationEnum.APPLICATION)
const emit = defineEmits(['update:data'])
const allChecked: any = ref({
[TeamEnum.MANAGE]: computed({
[AuthorizationEnum.MANAGE]: computed({
get: () => {
return filterData.value.some((item: any) => item.operate[TeamEnum.MANAGE])
return filterData.value.some((item: any) => item.operate[AuthorizationEnum.MANAGE])
},
set: (val: boolean) => {
if (val) {
filterData.value.map((item: any) => {
item.operate[TeamEnum.MANAGE] = true
item.operate[TeamEnum.USE] = true
item.operate[AuthorizationEnum.MANAGE] = true
item.operate[AuthorizationEnum.USE] = true
})
} else {
filterData.value.map((item: any) => {
item.operate[TeamEnum.MANAGE] = false
item.operate[AuthorizationEnum.MANAGE] = false
})
}
}
},
}),
[TeamEnum.USE]: computed({
[AuthorizationEnum.USE]: computed({
get: () => {
return filterData.value.some((item: any) => item.operate[TeamEnum.USE])
return filterData.value.some((item: any) => item.operate[AuthorizationEnum.USE])
},
set: (val: boolean) => {
if (val) {
filterData.value.map((item: any) => {
item.operate[TeamEnum.USE] = true
item.operate[AuthorizationEnum.USE] = true
})
} else {
filterData.value.map((item: any) => {
item.operate[TeamEnum.USE] = false
item.operate[TeamEnum.MANAGE] = false
item.operate[AuthorizationEnum.USE] = false
item.operate[AuthorizationEnum.MANAGE] = false
})
}
}
})
},
}),
})
const filterText = ref('')
const filterData = computed(() =>
props.data.filter((v: any) => v.name.toLowerCase().includes(filterText.value.toLowerCase()))
props.data.filter((v: any) => v.name.toLowerCase().includes(filterText.value.toLowerCase())),
)
const allIndeterminate: any = ref({
[TeamEnum.MANAGE]: computed(() => {
const all_not_checked = filterData.value.every((item: any) => !item.operate[TeamEnum.MANAGE])
[AuthorizationEnum.MANAGE]: computed(() => {
const all_not_checked = filterData.value.every(
(item: any) => !item.operate[AuthorizationEnum.MANAGE],
)
if (all_not_checked) {
return false
}
return !filterData.value.every((item: any) => item.operate[TeamEnum.MANAGE])
return !filterData.value.every((item: any) => item.operate[AuthorizationEnum.MANAGE])
}),
[TeamEnum.USE]: computed(() => {
const all_not_checked = filterData.value.every((item: any) => !item.operate[TeamEnum.USE])
[AuthorizationEnum.USE]: computed(() => {
const all_not_checked = filterData.value.every(
(item: any) => !item.operate[AuthorizationEnum.USE],
)
if (all_not_checked) {
return false
}
return !filterData.value.every((item: any) => item.operate[TeamEnum.USE])
})
return !filterData.value.every((item: any) => item.operate[AuthorizationEnum.USE])
}),
})
function checkedOperateChange(Name: string | number, row: any, e: boolean) {
props.data.map((item: any) => {
if (item.id === row.id) {
item.operate[Name] = e
if (Name === TeamEnum.MANAGE && e) {
item.operate[TeamEnum.USE] = true
} else if (Name === TeamEnum.USE && !e) {
item.operate[TeamEnum.MANAGE] = false
if (Name === AuthorizationEnum.MANAGE && e) {
item.operate[AuthorizationEnum.USE] = true
} else if (Name === AuthorizationEnum.USE && !e) {
item.operate[AuthorizationEnum.MANAGE] = false
}
}
})

View File

@ -1,13 +1,11 @@
<template>
<div class="p-16-24">
<div class="resource-authorization p-16-24">
<h4 class="mb-16">{{ $t('views.userManage.title') }}</h4>
<el-card>
<div class="resource-authorization flex main-calc-height">
<div class="team-member p-8 border-r">
<div class="flex-between p-16">
<h4>{{ $t('views.resourceAuthorization.member') }}</h4>
</div>
<div class="team-member-input">
<el-card style="--el-card-padding: 0">
<div class="flex main-calc-height">
<div class="resource-authorization__left border-r p-8">
<div class="p-8">
<h4 class="mb-12">{{ $t('views.resourceAuthorization.member') }}</h4>
<el-input
v-model="filterText"
:placeholder="$t('common.search')"
@ -27,48 +25,34 @@
<template #default="{ row }">
<div class="flex-between">
<div>
<span class="mr-8">{{ row.username }}</span>
<span class="mr-8">{{ row.nick_name }}</span>
<el-tag v-if="isManage(row.type)" class="default-tag">{{
$t('views.resourceAuthorization.manage')
}}</el-tag>
</div>
<div @click.stop style="margin-top: 5px">
<el-dropdown trigger="click" v-if="!isManage(row.type)">
<span class="cursor">
<el-icon class="rotate-90"><MoreFilled /></el-icon>
</span>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item @click.prevent="deleteMember(row)">{{
$t('views.resourceAuthorization.delete.button')
}}</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</div>
</template>
</common-list>
</el-scrollbar>
</div>
</div>
<div class="permission-setting flex" v-loading="rLoading">
<div class="team-manage__table">
<h4 class="p-24 pb-0 mb-4">{{ $t('views.resourceAuthorization.permissionSetting') }}</h4>
<el-tabs v-model="activeName" class="team-manage__tabs">
<div class="permission-setting p-16 flex" v-loading="rLoading">
<div class="resource-authorization__table">
<h4 class="mb-4">{{ $t('views.resourceAuthorization.permissionSetting') }}</h4>
<el-tabs v-model="activeName" class="resource-authorization__tabs">
<el-tab-pane
v-for="(item, index) in settingTags"
:key="item.value"
:label="item.label"
:name="item.value"
>
<!-- <PermissionSetting
<PermissionSetting
:key="index"
:data="item.data"
:type="item.value"
:tableHeight="tableHeight"
:manage="isManage(currentType)"
></PermissionSetting> -->
></PermissionSetting>
</el-tab-pane>
</el-tabs>
</div>
@ -79,36 +63,33 @@
</div>
</div>
</el-card>
<!-- <CreateMemberDialog ref="CreateMemberRef" @refresh="refresh" /> -->
</div>
</template>
<script lang="ts" setup>
import { onMounted, ref, reactive, watch } from 'vue'
import AuthorizationApi from '@/api/user/resource-authorization'
import type { TeamMember } from '@/api/type/team'
// import CreateMemberDialog from './component/CreateMemberDialog.vue'
// import PermissionSetting from './component/PermissionSetting.vue'
import PermissionSetting from './component/PermissionSetting.vue'
import { MsgSuccess, MsgConfirm } from '@/utils/message'
import { AuthorizationEnum } from '@/enums/system'
import { t } from '@/locales'
// const CreateMemberRef = ref<InstanceType<typeof CreateMemberDialog>>()
const loading = ref(false)
const rLoading = ref(false)
const memberList = ref<TeamMember[]>([]) //
const filterMember = ref<TeamMember[]>([]) //
const memberList = ref<any[]>([]) //
const filterMember = ref<any[]>([]) //
const currentUser = ref<String>('')
const currentType = ref<String>('')
const filterText = ref('')
const activeName = ref(AuthorizationEnum.DATASET)
const activeName = ref(AuthorizationEnum.KNOWLEDGE)
const tableHeight = ref(0)
const settingTags = reactive([
{
label: t('views.knowledge.title'),
value: AuthorizationEnum.DATASET,
value: AuthorizationEnum.KNOWLEDGE,
data: [] as any,
},
{
@ -156,10 +137,40 @@ function submitPermissions() {
})
}
function clickMemberHandle(item: any) {
currentUser.value = item.id
currentType.value = item.type
ResourcePermissions(item.id)
}
function getMember(id?: string) {
loading.value = true
AuthorizationApi.getUserList()
.then((res) => {
memberList.value = res.data
filterMember.value = res.data
const user = (id && memberList.value.find((p) => p.user_id === id)) || null
currentUser.value = user ? user.id : memberList.value[0].id
currentType.value = user ? user.type : memberList.value[0].type
ResourcePermissions(currentUser.value)
loading.value = false
})
.catch(() => {
loading.value = false
})
}
function ResourcePermissions() {
rLoading.value = true
AuthorizationApi.getResourceAuthorization('default')
.then((res) => {
if (!res.data || Object.keys(res.data).length > 0) {
settingTags.map((item) => {
if (Object.keys(res.data).indexOf(item.value) !== -1) {
item.data = res.data[item.value]
}
})
}
rLoading.value = false
})
.catch(() => {
@ -176,19 +187,13 @@ onMounted(() => {
tableHeight.value = window.innerHeight - 330
})()
}
ResourcePermissions()
getMember()
})
</script>
<style lang="scss" scoped>
.resource-authorization {
.add-user-icon {
font-size: 17px;
}
.team-member-input {
padding: 0 calc(var(--app-base-px) * 2);
}
.team-member {
.resource-authorization__left {
box-sizing: border-box;
width: var(--setting-left-width);
min-width: var(--setting-left-width);
@ -196,28 +201,17 @@ onMounted(() => {
.permission-setting {
box-sizing: border-box;
width: calc(100% - var(--setting-left-width));
width: 100%;
flex-direction: column;
position: relative;
.submit-button {
position: absolute;
top: 54px;
top: 16px;
right: 24px;
}
}
.list-height-left {
height: calc(var(--create-dataset-height) - 60px);
}
&__tabs {
margin-top: 10px;
:deep(.el-tabs__nav-scroll) {
padding: 0 24px;
}
}
&__table {
flex: 1;
height: calc(100vh - 240px);
}
}
</style>

View File

@ -0,0 +1,53 @@
<template>
<el-dialog
:title="title"
v-model="dialogVisible"
:close-on-click-modal="false"
:close-on-press-escape="false"
:destroy-on-close="true"
:before-close="close"
append-to-body
>
<DynamicsFormConstructor
v-model="dynamicsFormData"
label-position="top"
require-asterisk-position="right"
ref="dynamicsFormConstructorRef"
></DynamicsFormConstructor>
<template #footer>
<span class="dialog-footer">
<el-button @click.prevent="dialogVisible = false"> {{$t('common.cancel')}} </el-button>
<el-button type="primary" @click="submit()" :loading="loading"> {{$t('common.add')}} </el-button>
</span>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import DynamicsFormConstructor from '@/components/dynamics-form/constructor/index.vue'
import { t } from '@/locales'
const props = withDefaults(
defineProps<{ title?: string; addFormField: (form_data: any) => void }>(),
{ title: t('views.template.templateForm.title.addParam') }
)
const dialogVisible = ref<boolean>(false)
const dynamicsFormConstructorRef = ref<InstanceType<typeof DynamicsFormConstructor>>()
const emit = defineEmits(['submit'])
const dynamicsFormData = ref<any>({})
const loading = ref<boolean>(false)
const open = () => {
dialogVisible.value = true
}
const close = () => {
dialogVisible.value = false
dynamicsFormData.value = {}
}
const submit = () => {
dynamicsFormConstructorRef.value?.validate().then(() => {
props.addFormField(dynamicsFormConstructorRef.value?.getData())
close()
})
}
defineExpose({ close, open })
</script>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,37 @@
<template>
<div class="custom-edge cursor" @mouseup.stop @click.stop v-show="props.model.isHovered">
<svg
@click="deleteEdge"
width="22"
height="22"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 23.0001C5.925 23.0001 1 18.0751 1 12.0001C1 5.92512 5.925 1.00012 12 1.00012C18.075 1.00012 23 5.92512 23 12.0001C23 18.0751 18.075 23.0001 12 23.0001Z"
fill="#3370FF"
/>
<path
d="M9.02524 7.61124L12.0002 10.5862L14.9752 7.61124C15.069 7.5175 15.1962 7.46484 15.3287 7.46484C15.4613 7.46484 15.5885 7.5175 15.6822 7.61124L16.3892 8.31824C16.483 8.412 16.5356 8.53915 16.5356 8.67174C16.5356 8.80432 16.483 8.93147 16.3892 9.02524L13.4142 12.0002L16.3892 14.9752C16.483 15.069 16.5356 15.1962 16.5356 15.3287C16.5356 15.4613 16.483 15.5885 16.3892 15.6822L15.6822 16.3892C15.5885 16.483 15.4613 16.5356 15.3287 16.5356C15.1962 16.5356 15.069 16.483 14.9752 16.3892L12.0002 13.4142L9.02524 16.3892C8.93147 16.483 8.80432 16.5356 8.67174 16.5356C8.53916 16.5356 8.412 16.483 8.31824 16.3892L7.61124 15.6822C7.5175 15.5885 7.46484 15.4613 7.46484 15.3287C7.46484 15.1962 7.5175 15.069 7.61124 14.9752L10.5862 12.0002L7.61124 9.02524C7.5175 8.93147 7.46484 8.80432 7.46484 8.67174C7.46484 8.53915 7.5175 8.412 7.61124 8.31824L8.31824 7.61124C8.412 7.5175 8.53916 7.46484 8.67174 7.46484C8.80432 7.46484 8.93147 7.5175 9.02524 7.61124Z"
fill="white"
/>
</svg>
</div>
</template>
<script setup lang="ts">
const props = defineProps<{ model: any }>()
const deleteEdge = () => {
props.model.graphModel.deleteEdgeById(props.model.id)
}
</script>
<style lang="scss">
.custom-edge {
color: var(--el-color-primary);
stroke: none;
z-index: 100000;
position: absolute;
pointer-events: all;
}
</style>

View File

@ -0,0 +1,58 @@
<template>
<el-dialog
:title="title"
v-model="dialogVisible"
:close-on-click-modal="false"
:close-on-press-escape="false"
:destroy-on-close="true"
:before-close="close"
append-to-body
>
<DynamicsFormConstructor
v-model="dynamicsFormData"
label-position="top"
require-asterisk-position="right"
ref="dynamicsFormConstructorRef"
></DynamicsFormConstructor>
<template #footer>
<span class="dialog-footer">
<el-button @click.prevent="dialogVisible = false"> {{ $t('common.cancel') }} </el-button>
<el-button type="primary" @click="submit()" :loading="loading">
{{ $t('common.modify') }}
</el-button>
</span>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import DynamicsFormConstructor from '@/components/dynamics-form/constructor/index.vue'
import { t } from '@/locales'
const props = withDefaults(
defineProps<{ title?: string; editFormField: (form_data: any, index: number) => void }>(),
{ title: t('views.template.templateForm.title.editParam') }
)
const dialogVisible = ref<boolean>(false)
const dynamicsFormConstructorRef = ref<InstanceType<typeof DynamicsFormConstructor>>()
const emit = defineEmits(['submit'])
const dynamicsFormData = ref<any>({})
const currentIndex = ref<number>(0)
const loading = ref<boolean>(false)
const open = (form_data: any, index: number) => {
dialogVisible.value = true
dynamicsFormData.value = form_data
currentIndex.value = index
}
const close = () => {
dialogVisible.value = false
dynamicsFormData.value = {}
}
const submit = () => {
dynamicsFormConstructorRef.value?.validate().then(() => {
props.editFormField(dynamicsFormConstructorRef.value?.getData(), currentIndex.value)
close()
})
}
defineExpose({ close, open })
</script>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,87 @@
<template>
<el-cascader
@wheel="wheel"
:teleported="false"
:options="options"
@visible-change="visibleChange"
v-bind="$attrs"
v-model="data"
separator=" > "
>
<template #default="{ node, data }">
<span class="flex align-center" @wheel="wheel">
<component :is="iconComponent(`${data.type}-icon`)" class="mr-8" :size="18" />{{
data.label
}}</span
>
</template>
</el-cascader>
</template>
<script setup lang="ts">
import { ref, onMounted, computed } from 'vue'
import { iconComponent } from '../icons/utils'
import { t } from '@/locales'
const props = defineProps<{
nodeModel: any
modelValue: Array<any>
global?: Boolean
}>()
const emit = defineEmits(['update:modelValue'])
const data = computed({
set: (value) => {
emit('update:modelValue', value)
},
get: () => {
return props.modelValue
}
})
const options = ref<Array<any>>([])
const wheel = (e: any) => {
if (e.ctrlKey === true) {
e.preventDefault()
return true
} else {
e.stopPropagation()
return true
}
}
function visibleChange(bool: boolean) {
if (bool) {
options.value = props.global
? props.nodeModel.get_up_node_field_list(false, true).filter((v: any) => v.value === 'global')
: props.nodeModel.get_up_node_field_list(false, true)
}
}
const validate = () => {
const incomingNodeValue = props.nodeModel.get_up_node_field_list(false, true)
if (!data.value || data.value.length === 0) {
return Promise.reject(t('views.applicationWorkflow.variable.ReferencingRequired'))
}
if (data.value.length < 2) {
return Promise.reject(t('views.applicationWorkflow.variable.ReferencingError'))
}
const node_id = data.value[0]
const node_field = data.value[1]
const nodeParent = incomingNodeValue.find((item: any) => item.value === node_id)
if (!nodeParent) {
data.value = []
return Promise.reject(t('views.applicationWorkflow.variable.NoReferencing'))
}
if (!nodeParent.children.some((item: any) => item.value === node_field)) {
data.value = []
return Promise.reject(t('views.applicationWorkflow.variable.NoReferencing'))
}
return Promise.resolve('')
}
defineExpose({ validate })
onMounted(() => {
options.value = props.global
? props.nodeModel.get_up_node_field_list(false, true).filter((v: any) => v.value === 'global')
: props.nodeModel.get_up_node_field_list(false, true)
})
</script>
<style scoped></style>

View File

@ -0,0 +1,367 @@
<template>
<div @mousedown="mousedown" class="workflow-node-container p-16" style="overflow: visible">
<div
class="step-container app-card p-16"
:class="{ isSelected: props.nodeModel.isSelected, error: node_status !== 200 }"
style="overflow: visible"
>
<div v-resize="resizeStepContainer">
<div class="flex-between">
<div class="flex align-center" style="width: 70%">
<component
:is="iconComponent(`${nodeModel.type}-icon`)"
class="mr-8"
:size="24"
:item="nodeModel?.properties.node_data"
/>
<h4 class="ellipsis-1 break-all">{{ nodeModel.properties.stepName }}</h4>
</div>
<div @mousemove.stop @mousedown.stop @keydown.stop @click.stop>
<el-button text @click="showNode = !showNode">
<el-icon class="arrow-icon color-secondary" :class="showNode ? 'rotate-180' : ''"
><ArrowDownBold />
</el-icon>
</el-button>
<el-dropdown
v-if="showOperate(nodeModel.type)"
:teleported="false"
trigger="click"
placement="bottom-start"
>
<el-button text>
<img src="@/assets/icon_or.svg" alt="" v-if="condition === 'OR'" />
<img src="@/assets/icon_and.svg" alt="" v-if="condition === 'AND'" />
</el-button>
<template #dropdown>
<div style="width: 280px" class="p-12-16">
<h5>{{ $t('views.applicationWorkflow.condition.title') }}</h5>
<p class="mt-8 lighter">
<span>{{ $t('views.applicationWorkflow.condition.front') }}</span>
<el-select v-model="condition" size="small" style="width: 60px; margin: 0 8px">
<el-option
:label="$t('views.applicationWorkflow.condition.AND')"
value="AND"
/>
<el-option :label="$t('views.applicationWorkflow.condition.OR')" value="OR" />
</el-select>
<span>{{ $t('views.applicationWorkflow.condition.text') }}</span>
</p>
</div>
</template>
</el-dropdown>
<el-dropdown v-if="showOperate(nodeModel.type)" :teleported="false" trigger="click">
<el-button text>
<el-icon class="color-secondary"><MoreFilled /></el-icon>
</el-button>
<template #dropdown>
<el-dropdown-menu style="min-width: 80px">
<el-dropdown-item @click="renameNode" class="p-8">{{
$t('common.rename')
}}</el-dropdown-item>
<el-dropdown-item @click="copyNode" class="p-8">{{
$t('common.copy')
}}</el-dropdown-item>
<el-dropdown-item @click="deleteNode" class="border-t p-8">{{
$t('common.delete')
}}</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</div>
<el-collapse-transition>
<div @mousedown.stop @keydown.stop @click.stop v-show="showNode" class="mt-16">
<el-alert
v-if="node_status != 200"
class="mb-16"
:title="
props.nodeModel.type === 'application-node'
? $t('views.applicationWorkflow.tip.applicationNodeError')
: $t('views.applicationWorkflow.tip.functionNodeError')
"
type="error"
show-icon
:closable="false"
/>
<slot></slot>
<template v-if="nodeFields.length > 0">
<h5 class="title-decoration-1 mb-8 mt-8">
{{ $t('common.param.outputParam') }}
</h5>
<template v-for="(item, index) in nodeFields" :key="index">
<div
class="flex-between border-r-4 p-8-12 mb-8 layout-bg lighter"
@mouseenter="showicon = index"
@mouseleave="showicon = null"
>
<span style="max-width: 92%">{{ item.label }} {{ '{' + item.value + '}' }}</span>
<el-tooltip
effect="dark"
:content="$t('views.applicationWorkflow.setting.copyParam')"
placement="top"
v-if="showicon === index"
>
<el-button link @click="copyClick(item.globeLabel)" style="padding: 0">
<AppIcon iconName="app-copy"></AppIcon>
</el-button>
</el-tooltip>
</div>
</template>
</template>
</div>
</el-collapse-transition>
</div>
</div>
<el-collapse-transition>
<DropdownMenu
v-if="showAnchor"
@mousemove.stop
@mousedown.stop
@click.stop
@wheel="handleWheel"
:show="showAnchor"
:id="id"
style="left: 100%; top: 50%; transform: translate(0, -50%)"
@clickNodes="clickNodes"
/>
</el-collapse-transition>
<el-dialog
:title="$t('views.applicationWorkflow.nodeName')"
v-model="nodeNameDialogVisible"
:close-on-click-modal="false"
:close-on-press-escape="false"
:destroy-on-close="true"
append-to-body
@submit.prevent
>
<el-form label-position="top" ref="titleFormRef" :model="form">
<el-form-item
prop="title"
:rules="[
{
required: true,
message: $t('common.inputPlaceholder'),
trigger: 'blur'
}
]"
>
<el-input v-model="form.title" @blur="form.title = form.title.trim()" />
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click.prevent="nodeNameDialogVisible = false">
{{ $t('common.cancel') }}
</el-button>
<el-button type="primary" @click="editName(titleFormRef)">
{{ $t('common.save') }}
</el-button>
</span>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { app } from '@/main'
import DropdownMenu from '@/views/application-workflow/component/DropdownMenu.vue'
import { set } from 'lodash'
import { iconComponent } from '../icons/utils'
import { copyClick } from '@/utils/clipboard'
import { WorkflowType } from '@/enums/workflow'
import { MsgError, MsgConfirm } from '@/utils/message'
import type { FormInstance } from 'element-plus'
import { t } from '@/locales'
const {
params: { id }
} = app.config.globalProperties.$route as any
const height = ref<{
stepContainerHeight: number
inputContainerHeight: number
outputContainerHeight: number
}>({
stepContainerHeight: 0,
inputContainerHeight: 0,
outputContainerHeight: 0
})
const showAnchor = ref<boolean>(false)
const anchorData = ref<any>()
const titleFormRef = ref()
const nodeNameDialogVisible = ref<boolean>(false)
const form = ref<any>({
title: ''
})
const condition = computed({
set: (v) => {
set(props.nodeModel.properties, 'condition', v)
},
get: () => {
if (props.nodeModel.properties.condition) {
return props.nodeModel.properties.condition
}
set(props.nodeModel.properties, 'condition', 'AND')
return true
}
})
const showNode = computed({
set: (v) => {
set(props.nodeModel.properties, 'showNode', v)
},
get: () => {
if (props.nodeModel.properties.showNode !== undefined) {
return props.nodeModel.properties.showNode
}
set(props.nodeModel.properties, 'showNode', true)
return true
}
})
const handleWheel = (event: any) => {
const isCombinationKeyPressed = event.ctrlKey || event.metaKey
if (!isCombinationKeyPressed) {
event.stopPropagation()
}
}
const node_status = computed(() => {
if (props.nodeModel.properties.status) {
return props.nodeModel.properties.status
}
return 200
})
function renameNode() {
form.value.title = props.nodeModel.properties.stepName
nodeNameDialogVisible.value = true
}
const editName = async (formEl: FormInstance | undefined) => {
if (!formEl) return
await formEl.validate((valid) => {
if (valid) {
if (
!props.nodeModel.graphModel.nodes?.some(
(node: any) => node.properties.stepName === form.value.title
)
) {
set(props.nodeModel.properties, 'stepName', form.value.title)
nodeNameDialogVisible.value = false
formEl.resetFields()
} else {
MsgError(t('views.applicationWorkflow.tip.repeatedNodeError'))
}
}
})
}
const mousedown = () => {
props.nodeModel.graphModel.clearSelectElements()
set(props.nodeModel, 'isSelected', true)
set(props.nodeModel, 'isHovered', true)
props.nodeModel.graphModel.toFront(props.nodeModel.id)
}
const showicon = ref<number | null>(null)
const copyNode = () => {
props.nodeModel.graphModel.clearSelectElements()
const cloneNode = props.nodeModel.graphModel.cloneNode(props.nodeModel.id)
set(cloneNode, 'isSelected', true)
set(cloneNode, 'isHovered', true)
props.nodeModel.graphModel.toFront(cloneNode.id)
}
const deleteNode = () => {
MsgConfirm(t('common.tip'), t('views.applicationWorkflow.delete.confirmTitle'), {
confirmButtonText: t('common.confirm'),
confirmButtonClass: 'danger'
}).then(() => {
props.nodeModel.graphModel.deleteNode(props.nodeModel.id)
})
props.nodeModel.graphModel.eventCenter.emit('delete_node')
}
const resizeStepContainer = (wh: any) => {
if (wh.height) {
if (!props.nodeModel.virtual) {
height.value.stepContainerHeight = wh.height
props.nodeModel.setHeight(height.value.stepContainerHeight)
}
}
}
function clickNodes(item: any) {
const width = item.properties.width ? item.properties.width : 214
const nodeModel = props.nodeModel.graphModel.addNode({
type: item.type,
properties: item.properties,
x: anchorData.value?.x + width / 2 + 200,
y: anchorData.value?.y - item.height
})
props.nodeModel.graphModel.addEdge({
type: 'app-edge',
sourceNodeId: props.nodeModel.id,
sourceAnchorId: anchorData.value?.id,
targetNodeId: nodeModel.id
})
closeNodeMenu()
}
const props = defineProps<{
nodeModel: any
}>()
const nodeFields = computed(() => {
if (props.nodeModel.properties.config.fields) {
const fields = props.nodeModel.properties.config.fields?.map((field: any) => {
return {
label: field.label,
value: field.value,
globeLabel: `{{${props.nodeModel.properties.stepName}.${field.value}}}`,
globeValue: `{{context['${props.nodeModel.id}'].${field.value}}}`
}
})
return fields
}
return []
})
function showOperate(type: string) {
return type !== WorkflowType.Base && type !== WorkflowType.Start
}
const openNodeMenu = (anchorValue: any) => {
showAnchor.value = true
anchorData.value = anchorValue
}
const closeNodeMenu = () => {
showAnchor.value = false
anchorData.value = undefined
}
onMounted(() => {
set(props.nodeModel, 'openNodeMenu', (anchorData: any) => {
showAnchor.value ? closeNodeMenu() : openNodeMenu(anchorData)
})
})
</script>
<style lang="scss" scoped>
.workflow-node-container {
.step-container {
border: 2px solid #ffffff !important;
box-sizing: border-box;
&:hover {
box-shadow: 0px 6px 24px 0px rgba(31, 35, 41, 0.08);
}
&.isSelected {
border: 2px solid var(--el-color-primary) !important;
}
&.error {
border: 1px solid #f54a45 !important;
}
}
.arrow-icon {
transition: 0.2s;
}
}
:deep(.el-card) {
overflow: visible;
}
</style>

View File

@ -0,0 +1,110 @@
<template>
<el-card shadow="always" style="--el-card-padding: 8px 12px; --el-card-border-radius: 8px">
<el-button link @click="zoomOut">
<el-tooltip
effect="dark"
:content="$t('views.applicationWorkflow.control.zoomOut')"
placement="top"
>
<el-icon :size="16" :title="$t('views.applicationWorkflow.control.zoomOut')"
><ZoomOut
/></el-icon>
</el-tooltip>
</el-button>
<el-button link @click="zoomIn">
<el-tooltip
effect="dark"
:content="$t('views.applicationWorkflow.control.zoomIn')"
placement="top"
>
<el-icon :size="16" :title="$t('views.applicationWorkflow.control.zoomIn')"
><ZoomIn
/></el-icon>
</el-tooltip>
</el-button>
<el-button link @click="fitView">
<el-tooltip
effect="dark"
:content="$t('views.applicationWorkflow.control.fitView')"
placement="top"
>
<AppIcon
iconName="app-fitview"
:title="$t('views.applicationWorkflow.control.fitView')"
></AppIcon>
</el-tooltip>
</el-button>
<el-divider direction="vertical" />
<el-button link @click="retract">
<el-tooltip
effect="dark"
:content="$t('views.applicationWorkflow.control.retract')"
placement="top"
>
<AppIcon
style="font-size: 16px"
iconName="app-retract"
:title="$t('views.applicationWorkflow.control.retract')"
></AppIcon>
</el-tooltip>
</el-button>
<el-button link @click="extend">
<el-tooltip
effect="dark"
:content="$t('views.applicationWorkflow.control.extend')"
placement="top"
>
<AppIcon
style="font-size: 16px"
iconName="app-extend"
:title="$t('views.applicationWorkflow.control.extend')"
></AppIcon>
</el-tooltip>
</el-button>
<el-button link @click="layout">
<el-tooltip
effect="dark"
:content="$t('views.applicationWorkflow.control.beautify')"
placement="top"
>
<AppIcon
style="font-size: 16px"
iconName="app-beautify"
:title="$t('views.applicationWorkflow.control.beautify')"
></AppIcon>
</el-tooltip>
</el-button>
</el-card>
</template>
<script setup lang="ts">
const props = defineProps({
lf: Object || String || null
})
function zoomIn() {
props.lf?.zoom(true, [0, 0])
}
function zoomOut() {
props.lf?.zoom(false, [0, 0])
}
function fitView() {
props.lf?.resetZoom()
props.lf?.resetTranslate()
props.lf?.fitView()
}
const layout = () => {
props.lf?.extension.dagre.layout()
}
const retract = () => {
props.lf?.graphModel.nodes.forEach((element: any) => {
element.properties.showNode = false
})
}
const extend = () => {
props.lf?.graphModel.nodes.forEach((element: any) => {
element.properties.showNode = true
})
}
</script>
<style scoped></style>

View File

@ -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<Array<any>>
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
? `<svg width="100%" height="100%" viewBox="0 0 42 42" fill="none" xmlns="http://www.w3.org/2000/svg">
<g filter="url(#filter0_d_5119_232585)">
<path d="M20.9998 29.8333C28.0875 29.8333 33.8332 24.0876 33.8332 17C33.8332 9.91231 28.0875 4.16663 20.9998 4.16663C13.9122 4.16663 8.1665 9.91231 8.1665 17C8.1665 24.0876 13.9122 29.8333 20.9998 29.8333Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M20.9998 27.5C26.7988 27.5 31.4998 22.799 31.4998 17C31.4998 11.201 26.7988 6.49996 20.9998 6.49996C15.2008 6.49996 10.4998 11.201 10.4998 17C10.4998 22.799 15.2008 27.5 20.9998 27.5ZM33.8332 17C33.8332 24.0876 28.0875 29.8333 20.9998 29.8333C13.9122 29.8333 8.1665 24.0876 8.1665 17C8.1665 9.91231 13.9122 4.16663 20.9998 4.16663C28.0875 4.16663 33.8332 9.91231 33.8332 17Z" fill="#3370FF"/>
</g>
<defs>
<filter id="filter0_d_5119_232585" x="-1" y="-1" width="44" height="44" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dy="4"/>
<feGaussianBlur stdDeviation="4"/>
<feComposite in2="hardAlpha" operator="out"/>
<feColorMatrix type="matrix" values="0 0 0 0 0.2 0 0 0 0 0.439216 0 0 0 0 1 0 0 0 0.1 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_5119_232585"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_5119_232585" result="shape"/>
</filter>
</defs>
</svg>
`
: `<svg width="100%" height="100%" viewBox="0 0 42 42" fill="none" xmlns="http://www.w3.org/2000/svg">
<g filter="url(#filter0_d_5199_166905)">
<path d="M20.9998 29.8333C28.0875 29.8333 33.8332 24.0876 33.8332 17C33.8332 9.91231 28.0875 4.16663 20.9998 4.16663C13.9122 4.16663 8.1665 9.91231 8.1665 17C8.1665 24.0876 13.9122 29.8333 20.9998 29.8333Z" fill="#3370FF"/>
<path d="M19.8332 11.75C19.8332 11.4278 20.0943 11.1666 20.4165 11.1666H21.5832C21.9053 11.1666 22.1665 11.4278 22.1665 11.75V15.8333H26.2498C26.572 15.8333 26.8332 16.0945 26.8332 16.4166V17.5833C26.8332 17.9055 26.572 18.1666 26.2498 18.1666H22.1665V22.25C22.1665 22.5721 21.9053 22.8333 21.5832 22.8333H20.4165C20.0943 22.8333 19.8332 22.5721 19.8332 22.25V18.1666H15.7498C15.4277 18.1666 15.1665 17.9055 15.1665 17.5833V16.4166C15.1665 16.0945 15.4277 15.8333 15.7498 15.8333H19.8332V11.75Z" fill="white"/>
</g>
<defs>
<filter id="filter0_d_5199_166905" x="-1" y="-1" width="44" height="44" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dy="4"/>
<feGaussianBlur stdDeviation="4"/>
<feComposite in2="hardAlpha" operator="out"/>
<feColorMatrix type="matrix" values="0 0 0 0 0.2 0 0 0 0 0.439216 0 0 0 0 1 0 0 0 0.1 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_5199_166905"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_5199_166905" result="shape"/>
</filter>
</defs>
</svg>`
}
})
]
)
}
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 }

View File

@ -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
}
}

View File

@ -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
}

View File

@ -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)
}

View File

@ -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<string, any>[] = []
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))
)
}
}
})
}

View File

@ -0,0 +1,150 @@
import { WorkflowType } from '@/enums/workflow'
import { t } from '@/locales'
const end_nodes: Array<string> = [
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<any>
constructor(workflow: { nodes: Array<any>; edges: Array<any> }) {
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')}`
}
}
}

View File

@ -0,0 +1,6 @@
<template>
<AppAvatar class="avatar-gradient" shape="square">
<img src="@/assets/icon_robot.svg" style="width: 75%" alt="" />
</AppAvatar>
</template>
<script setup lang="ts"></script>

View File

@ -0,0 +1,28 @@
<template>
<AppAvatar
v-if="isAppIcon(item?.icon)"
shape="square"
:size="32"
style="background: none"
class="mr-8"
>
<img :src="item?.icon" alt="" />
</AppAvatar>
<AppAvatar
v-else-if="item?.name"
:name="item?.name"
pinyinColor
shape="square"
:size="32"
class="mr-8"
/>
</template>
<script setup lang="ts">
import { isAppIcon } from '@/utils/common'
const props = defineProps<{
item: {
name: string
icon: string
}
}>()
</script>

View File

@ -0,0 +1,6 @@
<template>
<AppAvatar shape="square" style="background: #FF8800;">
<img src="@/assets/icon_hi.svg" style="width: 75%" alt="" />
</AppAvatar>
</template>
<script setup lang="ts"></script>

View File

@ -0,0 +1,6 @@
<template>
<AppAvatar shape="square" style="background: #14C0FF;">
<img src="@/assets/icon_condition.svg" style="width: 75%" alt="" />
</AppAvatar>
</template>
<script setup lang="ts"></script>

View File

@ -0,0 +1,6 @@
<template>
<AppAvatar shape="square" class="avatar-blue">
<img src="@/assets/icon_docs.svg" style="width: 65%" alt="" />
</AppAvatar>
</template>
<script setup lang="ts"></script>

View File

@ -0,0 +1,6 @@
<template>
<AppAvatar shape="square" style="background: #34c724">
<img src="@/assets/icon_form.svg" style="width: 75%" alt="" />
</AppAvatar>
</template>
<script setup lang="ts"></script>

View File

@ -0,0 +1,25 @@
<template>
<AppAvatar
v-if="isAppIcon(item?.icon)"
shape="square"
:size="32"
style="background: none"
class="mr-8"
>
<img :src="item?.icon" alt="" />
</AppAvatar>
<AppAvatar v-else shape="square" style="background: #34c724">
<img src="@/assets/icon_function_outlined.svg" style="width: 75%" alt="" />
</AppAvatar>
</template>
<script setup lang="ts">
import { isAppIcon } from '@/utils/common'
const props = defineProps<{
item?: {
name: string
icon: string
}
}>()
</script>
<script setup lang="ts"></script>

View File

@ -0,0 +1,6 @@
<template>
<AppAvatar shape="square" style="background: #34c724">
<img src="@/assets/icon_function_outlined.svg" style="width: 75%" alt="" />
</AppAvatar>
</template>
<script setup lang="ts"></script>

View File

@ -0,0 +1,4 @@
<template>
<img src="@/assets/icon_globe_color.svg" style="width: 18px" alt="" />
</template>
<script setup lang="ts"></script>

View File

@ -0,0 +1,6 @@
<template>
<AppAvatar shape="square" style="background: #FF8800;">
<img src="@/assets/icon_text-image.svg" style="width: 65%" alt="" />
</AppAvatar>
</template>
<script setup lang="ts"></script>

View File

@ -0,0 +1,6 @@
<template>
<AppAvatar shape="square" style="background: #14C0FF;">
<img src="@/assets/icon_image.svg" style="width: 65%" alt="" />
</AppAvatar>
</template>
<script setup lang="ts"></script>

View File

@ -0,0 +1,6 @@
<template>
<AppAvatar shape="square" style="background: #34c724">
<img src="@/assets/icon_mcp.svg" style="width: 65%" alt="" />
</AppAvatar>
</template>
<script setup lang="ts"></script>

View File

@ -0,0 +1,6 @@
<template>
<AppAvatar shape="square" style="background: #34C724">
<img src="@/assets/icon_setting.svg" style="width: 65%" alt="" />
</AppAvatar>
</template>
<script setup lang="ts"></script>

View File

@ -0,0 +1,6 @@
<template>
<AppAvatar shape="square" style="background: #FF8800">
<img src="@/assets/icon_reply.svg" style="width: 65%" alt="" />
</AppAvatar>
</template>
<script setup lang="ts"></script>

View File

@ -0,0 +1,6 @@
<template>
<AppAvatar shape="square" style="background: #7F3BF5">
<img src="@/assets/icon_reranker.svg" style="width: 65%" alt="" />
</AppAvatar>
</template>
<script setup lang="ts"></script>

View File

@ -0,0 +1,6 @@
<template>
<AppAvatar shape="square" class="avatar-blue">
<img src="@/assets/icon_document.svg" style="width: 58%" alt="" />
</AppAvatar>
</template>
<script setup lang="ts"></script>

View File

@ -0,0 +1,6 @@
<template>
<AppAvatar shape="square" style="background: #ff8800">
<img src="@/assets/icon_speech_to_text.svg" style="width: 100%" alt="" />
</AppAvatar>
</template>
<script setup lang="ts"></script>

View File

@ -0,0 +1,6 @@
<template>
<AppAvatar shape="square" style="background: #D136D1;">
<img src="@/assets/icon_start.svg" style="width: 75%" alt="" />
</AppAvatar>
</template>
<script setup lang="ts"></script>

View File

@ -0,0 +1,6 @@
<template>
<AppAvatar shape="square" style="background: #14c0ff">
<img src="@/assets/icon_text_to_speech.svg" style="width: 100%" alt="" />
</AppAvatar>
</template>
<script setup lang="ts"></script>

View File

@ -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
}

View File

@ -0,0 +1,6 @@
<template>
<AppAvatar shape="square" class="avatar-blue">
<img src="@/assets/icon_assigner.svg" style="width: 65%" alt="" />
</AppAvatar>
</template>
<script setup lang="ts"></script>

178
ui/src/workflow/index.vue Normal file
View File

@ -0,0 +1,178 @@
<template>
<div className="workflow-app" id="container"></div>
<!-- 辅助工具栏 -->
<Control class="workflow-control" v-if="lf" :lf="lf"></Control>
<TeleportContainer :flow-id="flowId" />
</template>
<script setup lang="ts">
import LogicFlow from '@logicflow/core'
import { ref, onMounted, computed } from 'vue'
import AppEdge from './common/edge'
import Control from './common/NodeControl.vue'
import { baseNodes } from '@/workflow/common/data'
import '@logicflow/extension/lib/style/index.css'
import '@logicflow/core/dist/style/index.css'
import { initDefaultShortcut } from '@/workflow/common/shortcut'
import Dagre from '@/workflow/plugins/dagre'
import { getTeleport } from '@/workflow/common/teleport'
const nodes: any = import.meta.glob('./nodes/**/index.ts', { eager: true })
defineOptions({ name: 'WorkFlow' })
const TeleportContainer = getTeleport()
const flowId = ref('')
type ShapeItem = {
type?: string
text?: string
icon?: string
label?: string
className?: string
disabled?: boolean
properties?: Record<string, any>
callback?: (lf: LogicFlow, container?: HTMLElement) => void
}
const props = defineProps({
data: Object || null
})
const defaultData = {
nodes: [...baseNodes]
}
const graphData = computed({
get: () => {
if (props.data) {
return props.data
} else {
return defaultData
}
},
set: (value) => {
return value
}
})
const lf = ref()
onMounted(() => {
renderGraphData()
})
const render = (data: any) => {
lf.value.render(data)
}
const renderGraphData = (data?: any) => {
const container: any = document.querySelector('#container')
if (container) {
lf.value = new LogicFlow({
plugins: [Dagre],
textEdit: false,
adjustEdge: false,
adjustEdgeStartAndEnd: false,
background: {
backgroundColor: '#f5f6f7'
},
grid: {
size: 10,
type: 'dot',
config: {
color: '#DEE0E3',
thickness: 1
}
},
keyboard: {
enabled: true
},
isSilentMode: false,
container: container
})
lf.value.setTheme({
bezier: {
stroke: '#afafaf',
strokeWidth: 1
}
})
lf.value.on('graph:rendered', () => {
flowId.value = lf.value.graphModel.flowId
})
initDefaultShortcut(lf.value, lf.value.graphModel)
lf.value.batchRegister([...Object.keys(nodes).map((key) => nodes[key].default), AppEdge])
lf.value.setDefaultEdgeType('app-edge')
lf.value.render(data ? data : {})
lf.value.graphModel.eventCenter.on('delete_edge', (id_list: Array<string>) => {
id_list.forEach((id: string) => {
lf.value.deleteEdge(id)
})
})
lf.value.graphModel.eventCenter.on('anchor:drop', (data: any) => {
//
data.nodeModel.clear_next_node_field(false)
})
setTimeout(() => {
lf.value?.fitView()
}, 500)
}
}
const validate = () => {
return Promise.all(lf.value.graphModel.nodes.map((element: any) => element?.validate?.()))
}
const getGraphData = () => {
return lf.value.getGraphData()
}
const onmousedown = (shapeItem: ShapeItem) => {
if (shapeItem.type) {
lf.value.dnd.startDrag({
type: shapeItem.type,
properties: { ...shapeItem.properties }
})
}
if (shapeItem.callback) {
shapeItem.callback(lf.value)
}
}
const addNode = (shapeItem: ShapeItem) => {
lf.value.clearSelectElements()
const { virtualRectCenterPositionX, virtualRectCenterPositionY } =
lf.value.graphModel.getVirtualRectSize()
const newNode = lf.value.graphModel.addNode({
type: shapeItem.type,
properties: shapeItem.properties,
x: virtualRectCenterPositionX,
y: virtualRectCenterPositionY - lf.value.graphModel.height / 2
})
newNode.isSelected = true
newNode.isHovered = true
lf.value.toFront(newNode.id)
}
const clearGraphData = () => {
return lf.value.clearData()
}
defineExpose({
onmousedown,
validate,
getGraphData,
addNode,
clearGraphData,
renderGraphData,
render
})
</script>
<style lang="scss">
.workflow-app {
width: 100%;
height: 100%;
position: relative;
}
.workflow-control {
position: absolute;
bottom: 24px;
left: 24px;
z-index: 2;
}
.lf-drag-able {
cursor: pointer;
}
</style>

View File

@ -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
}

View File

@ -0,0 +1,338 @@
<template>
<NodeContainer :nodeModel="nodeModel">
<h5 class="title-decoration-1 mb-8">{{ $t('views.applicationWorkflow.nodeSetting') }}</h5>
<el-card shadow="never" class="card-never" style="--el-card-padding: 12px">
<el-form
@submit.prevent
:model="chat_data"
label-position="top"
require-asterisk-position="right"
label-width="auto"
ref="aiChatNodeFormRef"
hide-required-asterisk
>
<el-form-item
:label="$t('views.application.applicationForm.form.aiModel.label')"
prop="model_id"
:rules="{
required: true,
message: $t('views.application.applicationForm.form.aiModel.placeholder'),
trigger: 'change'
}"
>
<template #label>
<div class="flex-between w-full">
<div>
<span
>{{ $t('views.application.applicationForm.form.aiModel.label')
}}<span class="danger">*</span></span
>
</div>
<el-button
:disabled="!chat_data.model_id"
type="primary"
link
@click="openAIParamSettingDialog(chat_data.model_id)"
@refreshForm="refreshParam"
>
<el-icon><Setting /></el-icon>
</el-button>
</div>
</template>
<ModelSelect
@change="model_change"
@wheel="wheel"
:teleported="false"
v-model="chat_data.model_id"
:placeholder="$t('views.application.applicationForm.form.aiModel.placeholder')"
:options="modelOptions"
@submitModel="getModel"
showFooter
:model-type="'LLM'"
></ModelSelect>
</el-form-item>
<el-form-item :label="$t('views.application.applicationForm.form.roleSettings.label')">
<MdEditorMagnify
:title="$t('views.application.applicationForm.form.roleSettings.label')"
v-model="chat_data.system"
style="height: 100px"
@submitDialog="submitSystemDialog"
:placeholder="$t('views.application.applicationForm.form.roleSettings.label')"
/>
</el-form-item>
<el-form-item
:label="$t('views.application.applicationForm.form.prompt.label')"
prop="prompt"
:rules="{
required: true,
message: $t('views.application.applicationForm.form.prompt.requiredMessage'),
trigger: 'blur'
}"
>
<template #label>
<div class="flex align-center">
<div class="mr-4">
<span
>{{ $t('views.application.applicationForm.form.prompt.label')
}}<span class="danger">*</span></span
>
</div>
<el-tooltip effect="dark" placement="right" popper-class="max-w-200">
<template #content
>{{ $t('views.application.applicationForm.form.prompt.tooltip') }}
</template>
<AppIcon iconName="app-warning" class="app-warning-icon"></AppIcon>
</el-tooltip>
</div>
</template>
<MdEditorMagnify
@wheel="wheel"
:title="$t('views.application.applicationForm.form.prompt.label')"
v-model="chat_data.prompt"
style="height: 150px"
@submitDialog="submitDialog"
/>
</el-form-item>
<el-form-item :label="$t('views.application.applicationForm.form.historyRecord.label')">
<template #label>
<div class="flex-between">
<div>{{ $t('views.application.applicationForm.form.historyRecord.label') }}</div>
<el-select v-model="chat_data.dialogue_type" type="small" style="width: 100px">
<el-option :label="$t('views.applicationWorkflow.node')" value="NODE" />
<el-option :label="$t('views.applicationWorkflow.workflow')" value="WORKFLOW" />
</el-select>
</div>
</template>
<el-input-number
v-model="chat_data.dialogue_number"
:min="0"
:value-on-clear="0"
controls-position="right"
class="w-full"
:step="1"
:step-strictly="true"
/>
</el-form-item>
<div class="flex-between mb-16">
<div class="lighter">{{ $t('views.applicationWorkflow.nodes.mcpNode.tool') }}</div>
<el-button type="primary" link @click="openMcpServersDialog" @refreshForm="refreshParam">
<el-icon><Setting /></el-icon>
</el-button>
</div>
<el-form-item @click.prevent>
<template #label>
<div class="flex-between w-full">
<div>
<span>{{
$t('views.application.applicationForm.form.reasoningContent.label')
}}</span>
</div>
<el-button
type="primary"
link
@click="openReasoningParamSettingDialog"
@refreshForm="refreshParam"
>
<el-icon><Setting /></el-icon>
</el-button>
</div>
</template>
<el-switch size="small" v-model="chat_data.model_setting.reasoning_content_enable" />
</el-form-item>
<el-form-item @click.prevent>
<template #label>
<div class="flex align-center">
<div class="mr-4">
<span>{{
$t('views.applicationWorkflow.nodes.aiChatNode.returnContent.label')
}}</span>
</div>
<el-tooltip effect="dark" placement="right" popper-class="max-w-200">
<template #content>
{{ $t('views.applicationWorkflow.nodes.aiChatNode.returnContent.tooltip') }}
</template>
<AppIcon iconName="app-warning" class="app-warning-icon"></AppIcon>
</el-tooltip>
</div>
</template>
<el-switch size="small" v-model="chat_data.is_result" />
</el-form-item>
</el-form>
</el-card>
<AIModeParamSettingDialog ref="AIModeParamSettingDialogRef" @refresh="refreshParam" />
<ReasoningParamSettingDialog
ref="ReasoningParamSettingDialogRef"
@refresh="submitReasoningDialog"
/>
<McpServersDialog ref="mcpServersDialogRef" @refresh="submitMcpServersDialog" />
</NodeContainer>
</template>
<script setup lang="ts">
import { cloneDeep, set, groupBy } from 'lodash'
import { app } from '@/main'
import NodeContainer from '@/workflow/common/NodeContainer.vue'
import type { FormInstance } from 'element-plus'
import { ref, computed, onMounted } from 'vue'
import applicationApi from '@/api/application'
import useStore from '@/stores'
import { isLastNode } from '@/workflow/common/data'
import AIModeParamSettingDialog from '@/views/application/component/AIModeParamSettingDialog.vue'
import { t } from '@/locales'
import ReasoningParamSettingDialog from '@/views/application/component/ReasoningParamSettingDialog.vue'
import McpServersDialog from '@/views/application/component/McpServersDialog.vue'
const { model } = useStore()
const wheel = (e: any) => {
if (e.ctrlKey === true) {
e.preventDefault()
return true
} else {
e.stopPropagation()
return true
}
}
function submitSystemDialog(val: string) {
set(props.nodeModel.properties.node_data, 'system', val)
}
function submitDialog(val: string) {
set(props.nodeModel.properties.node_data, 'prompt', val)
}
const model_change = (model_id?: string) => {
if (model_id) {
AIModeParamSettingDialogRef.value?.reset_default(model_id, id)
} else {
refreshParam({})
}
}
const {
params: { id }
} = app.config.globalProperties.$route as any
// @ts-ignore
const defaultPrompt = `${t('views.applicationWorkflow.nodes.aiChatNode.defaultPrompt')}
{{${t('views.applicationWorkflow.nodes.searchDatasetNode.label')}.data}}
${t('views.problem.title')}
{{${t('views.applicationWorkflow.nodes.startNode.label')}.question}}`
const form = {
model_id: '',
system: '',
prompt: defaultPrompt,
dialogue_number: 1,
is_result: false,
temperature: null,
max_tokens: null,
dialogue_type: 'WORKFLOW',
model_setting: {
reasoning_content_start: '<think>',
reasoning_content_end: '</think>',
reasoning_content_enable: false
}
}
const chat_data = computed({
get: () => {
if (props.nodeModel.properties.node_data) {
if (!props.nodeModel.properties.node_data.model_setting) {
set(props.nodeModel.properties.node_data, 'model_setting', {
reasoning_content_start: '<think>',
reasoning_content_end: '</think>',
reasoning_content_enable: false
})
}
return props.nodeModel.properties.node_data
} else {
set(props.nodeModel.properties, 'node_data', form)
}
return props.nodeModel.properties.node_data
},
set: (value) => {
set(props.nodeModel.properties, 'node_data', value)
}
})
const props = defineProps<{ nodeModel: any }>()
const aiChatNodeFormRef = ref<FormInstance>()
const modelOptions = ref<any>(null)
const AIModeParamSettingDialogRef = ref<InstanceType<typeof AIModeParamSettingDialog>>()
const ReasoningParamSettingDialogRef = ref<InstanceType<typeof ReasoningParamSettingDialog>>()
const validate = () => {
return aiChatNodeFormRef.value?.validate().catch((err) => {
return Promise.reject({ node: props.nodeModel, errMessage: err })
})
}
function getModel() {
if (id) {
applicationApi.getApplicationModel(id).then((res: any) => {
modelOptions.value = groupBy(res?.data, 'provider')
})
} else {
model.asyncGetModel().then((res: any) => {
modelOptions.value = groupBy(res?.data, 'provider')
})
}
}
const openAIParamSettingDialog = (modelId: string) => {
if (modelId) {
AIModeParamSettingDialogRef.value?.open(modelId, id, chat_data.value.model_params_setting)
}
}
const openReasoningParamSettingDialog = () => {
ReasoningParamSettingDialogRef.value?.open(chat_data.value.model_setting)
}
function refreshParam(data: any) {
set(props.nodeModel.properties.node_data, 'model_params_setting', data)
}
function submitReasoningDialog(val: any) {
let model_setting = cloneDeep(props.nodeModel.properties.node_data.model_setting)
model_setting = {
...model_setting,
...val
}
set(props.nodeModel.properties.node_data, 'model_setting', model_setting)
}
const mcpServersDialogRef = ref()
function openMcpServersDialog() {
const config = {
mcp_servers: chat_data.value.mcp_servers,
mcp_enable: chat_data.value.mcp_enable
}
mcpServersDialogRef.value.open(config)
}
function submitMcpServersDialog(config: any) {
set(props.nodeModel.properties.node_data, 'mcp_servers', config.mcp_servers)
set(props.nodeModel.properties.node_data, 'mcp_enable', config.mcp_enable)
}
onMounted(() => {
getModel()
if (typeof props.nodeModel.properties.node_data?.is_result === 'undefined') {
if (isLastNode(props.nodeModel)) {
set(props.nodeModel.properties.node_data, 'is_result', true)
}
}
set(props.nodeModel, 'validate', validate)
if (!chat_data.value.dialogue_type) {
chat_data.value.dialogue_type = 'WORKFLOW'
}
})
</script>
<style lang="scss" scoped></style>

View File

@ -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
}

View File

@ -0,0 +1,317 @@
<template>
<NodeContainer :nodeModel="nodeModel">
<h5 class="title-decoration-1 mb-8">{{ $t('views.applicationWorkflow.nodeSetting') }}</h5>
<el-card shadow="never" class="card-never">
<el-form
@submit.prevent
:model="form_data"
label-position="top"
require-asterisk-position="right"
label-width="auto"
ref="applicationNodeFormRef"
>
<el-form-item
:label="$t('views.applicationWorkflow.nodes.startNode.question')"
prop="question_reference_address"
:rules="{
message: $t(
'views.applicationWorkflow.nodes.searchDatasetNode.searchQuestion.requiredMessage'
),
trigger: 'blur',
required: true
}"
>
<NodeCascader
ref="applicationNodeFormRef"
:nodeModel="nodeModel"
class="w-full"
:placeholder="
$t('views.applicationWorkflow.nodes.searchDatasetNode.searchQuestion.placeholder')
"
v-model="form_data.question_reference_address"
/>
</el-form-item>
<el-form-item
v-if="form_data.hasOwnProperty('document_list') || 'document_list' in form_data"
:label="$t('views.problem.relateParagraph.selectDocument')"
prop="document_list"
:rules="{
message: $t('views.log.documentPlaceholder'),
trigger: 'blur',
required: false
}"
>
<NodeCascader
ref="nodeCascaderRef"
:nodeModel="nodeModel"
class="w-full"
:placeholder="$t('views.log.documentPlaceholder')"
v-model="form_data.document_list"
/>
</el-form-item>
<el-form-item
v-if="form_data.hasOwnProperty('image_list') || 'image_list' in form_data"
:label="$t('views.applicationWorkflow.nodes.imageUnderstandNode.image.label')"
prop="image_list"
:rules="{
message: $t(
'views.applicationWorkflow.nodes.imageUnderstandNode.image.requiredMessage'
),
trigger: 'blur',
required: false
}"
>
<NodeCascader
ref="nodeCascaderRef"
:nodeModel="nodeModel"
class="w-full"
:placeholder="
$t('views.applicationWorkflow.nodes.imageUnderstandNode.image.requiredMessage')
"
v-model="form_data.image_list"
/>
</el-form-item>
<el-form-item
v-if="form_data.hasOwnProperty('audio_list') || 'audio_list' in form_data"
:label="$t('views.applicationWorkflow.nodes.speechToTextNode.audio.label')"
prop="audio_list"
:rules="{
message: $t('views.applicationWorkflow.nodes.speechToTextNode.audio.placeholder'),
trigger: 'blur',
required: false
}"
>
<NodeCascader
ref="nodeCascaderRef"
:nodeModel="nodeModel"
class="w-full"
:placeholder="$t('views.applicationWorkflow.nodes.speechToTextNode.audio.placeholder')"
v-model="form_data.audio_list"
/>
</el-form-item>
<div v-for="(field, index) in form_data.api_input_field_list" :key="'api-input-' + index">
<el-form-item
:label="field.variable"
:prop="'api_input_field_list.' + index + '.value'"
:rules="[
{
required: field.is_required,
message: `${$t('common.inputPlaceholder')}${field.variable}`,
trigger: 'blur'
}
]"
>
<NodeCascader
ref="nodeCascaderRef"
:nodeModel="nodeModel"
class="w-full"
:placeholder="
$t('views.applicationWorkflow.nodes.searchDatasetNode.searchQuestion.placeholder')
"
v-model="form_data.api_input_field_list[index].value"
/>
</el-form-item>
</div>
<div v-for="(field, index) in form_data.user_input_field_list" :key="'user-input-' + index">
<el-form-item
:label="field.label"
:prop="'user_input_field_list.' + index + '.value'"
:rules="[
{
required: field.required,
message: `${$t('common.inputPlaceholder')}${field.label}`,
trigger: 'blur'
}
]"
>
<NodeCascader
ref="nodeCascaderRef"
:nodeModel="nodeModel"
class="w-full"
:placeholder="
$t('views.applicationWorkflow.nodes.searchDatasetNode.searchQuestion.placeholder')
"
v-model="form_data.user_input_field_list[index].value"
/>
</el-form-item>
</div>
<el-form-item
:label="$t('views.applicationWorkflow.nodes.aiChatNode.returnContent.label')"
@click.prevent
>
<template #label>
<div class="flex align-center">
<div class="mr-4">
<span>{{
$t('views.applicationWorkflow.nodes.aiChatNode.returnContent.label')
}}</span>
</div>
<el-tooltip effect="dark" placement="right" popper-class="max-w-200">
<template #content>
{{ $t('views.applicationWorkflow.nodes.aiChatNode.returnContent.tooltip') }}
</template>
<AppIcon iconName="app-warning" class="app-warning-icon"></AppIcon>
</el-tooltip>
</div>
</template>
<el-switch size="small" v-model="form_data.is_result" />
</el-form-item>
</el-form>
</el-card>
</NodeContainer>
</template>
<script setup lang="ts">
import { set, groupBy, create, cloneDeep } from 'lodash'
import { app } from '@/main'
import NodeContainer from '@/workflow/common/NodeContainer.vue'
import { ref, computed, onMounted, onActivated } from 'vue'
import NodeCascader from '@/workflow/common/NodeCascader.vue'
import type { FormInstance } from 'element-plus'
import applicationApi from '@/api/application'
import { isWorkFlow } from '@/utils/application'
const form = {
question_reference_address: ['start-node', 'question'],
api_input_field_list: [],
user_input_field_list: [],
document_list: ['start-node', 'document'],
image_list: ['start-node', 'image'],
audio_list: ['start-node', 'audio']
}
const {
params: { id }
} = app.config.globalProperties.$route as any
const applicationNodeFormRef = ref<FormInstance>()
const form_data = computed({
get: () => {
if (props.nodeModel.properties.node_data) {
return props.nodeModel.properties.node_data
} else {
set(props.nodeModel.properties, 'node_data', form)
}
return props.nodeModel.properties.node_data
},
set: (value) => {
set(props.nodeModel.properties, 'node_data', value)
}
})
function handleFileUpload(type: string, isEnabled: boolean) {
const listKey = `${type}_list`
if (isEnabled) {
if (!props.nodeModel.properties.node_data[listKey]) {
set(props.nodeModel.properties.node_data, listKey, [])
}
} else {
// eslint-disable-next-line vue/no-mutating-props
delete props.nodeModel.properties.node_data[listKey]
}
}
const update_field = () => {
if (!props.nodeModel.properties.node_data.application_id) {
set(props.nodeModel.properties, 'status', 500)
return
}
applicationApi
.getApplicationById(id, props.nodeModel.properties.node_data.application_id)
.then((ok) => {
const old_api_input_field_list = cloneDeep(
props.nodeModel.properties.node_data.api_input_field_list
)
const old_user_input_field_list = cloneDeep(
props.nodeModel.properties.node_data.user_input_field_list
)
if (isWorkFlow(ok.data.type)) {
const nodeData = ok.data.work_flow.nodes[0].properties.node_data
const new_api_input_field_list = cloneDeep(
ok.data.work_flow.nodes[0].properties.api_input_field_list
)
const new_user_input_field_list = cloneDeep(
ok.data.work_flow.nodes[0].properties.user_input_field_list
)
const merge_api_input_field_list = (new_api_input_field_list || []).map((item: any) => {
const find_field = old_api_input_field_list.find(
(old_item: any) => old_item.variable == item.variable
)
if (find_field) {
return {
...item,
value: find_field.value,
label:
typeof item.label === 'object' && item.label != null ? item.label.label : item.label
}
} else {
return item
}
})
set(
props.nodeModel.properties.node_data,
'api_input_field_list',
merge_api_input_field_list
)
const merge_user_input_field_list = (new_user_input_field_list || []).map((item: any) => {
const find_field = old_user_input_field_list.find(
(old_item: any) => old_item.field == item.field
)
if (find_field) {
return {
...item,
value: find_field.value,
label:
typeof item.label === 'object' && item.label != null ? item.label.label : item.label
}
} else {
return item
}
})
set(
props.nodeModel.properties.node_data,
'user_input_field_list',
merge_user_input_field_list
)
const fileEnable = nodeData.file_upload_enable
const fileUploadSetting = nodeData.file_upload_setting
if (fileEnable) {
handleFileUpload('document', fileUploadSetting.document)
handleFileUpload('image', fileUploadSetting.image)
handleFileUpload('audio', fileUploadSetting.audio)
} else {
;['document_list', 'image_list', 'audio_list'].forEach((list) => {
// eslint-disable-next-line vue/no-mutating-props
delete props.nodeModel.properties.node_data[list]
})
}
set(props.nodeModel.properties, 'status', ok.data.id ? 200 : 500)
}
})
.catch((err) => {
console.log(err)
set(props.nodeModel.properties, 'status', 500)
})
}
const props = defineProps<{ nodeModel: any }>()
const validate = () => {
return applicationNodeFormRef.value?.validate().catch((err) => {
return Promise.reject({ node: props.nodeModel, errMessage: err })
})
}
onMounted(() => {
update_field()
set(props.nodeModel, 'validate', validate)
})
</script>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,132 @@
<template>
<el-dialog
:title="
isEdit
? $t('views.template.templateForm.title.editParam')
: $t('views.template.templateForm.title.addParam')
"
v-model="dialogVisible"
:close-on-click-modal="false"
:close-on-press-escape="false"
:destroy-on-close="true"
:before-close="close"
append-to-body
>
<el-form
label-position="top"
ref="fieldFormRef"
:rules="rules"
:model="form"
require-asterisk-position="right"
>
<el-form-item :label="$t('dynamicsForm.paramForm.field.label')" prop="variable">
<el-input
v-model="form.variable"
:placeholder="$t('dynamicsForm.paramForm.field.placeholder')"
maxlength="64"
show-word-limit
@blur="form.variable = form.variable.trim()"
/>
</el-form-item>
<el-form-item :label="$t('dynamicsForm.paramForm.required.label')" @click.prevent>
<el-switch size="small" v-model="form.is_required"></el-switch>
</el-form-item>
<el-form-item
:label="$t('dynamicsForm.default.label')"
prop="default_value"
:rules="{
required: form.is_required,
message: $t('dynamicsForm.default.placeholder'),
trigger: 'blur'
}"
>
<el-input
v-model="form.default_value"
:placeholder="$t('dynamicsForm.default.placeholder')"
@blur="form.name = form.name.trim()"
/>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click.prevent="dialogVisible = false"> {{ $t('common.cancel') }} </el-button>
<el-button type="primary" @click="submit(fieldFormRef)" :loading="loading">
{{ isEdit ? $t('common.save') : $t('common.add') }}
</el-button>
</span>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { reactive, ref, watch } from 'vue'
import type { FormInstance } from 'element-plus'
import { cloneDeep } from 'lodash'
import { t } from '@/locales'
const emit = defineEmits(['refresh'])
const fieldFormRef = ref()
const loading = ref<boolean>(false)
const isEdit = ref(false)
const form = ref<any>({
name: '',
variable: '',
type: 'input',
is_required: true,
assignment_method: 'api_input',
optionList: [''],
default_value: ''
})
const rules = reactive({
name: [{ required: true, message: t('dynamicsForm.paramForm.name.requiredMessage'), trigger: 'blur' }],
variable: [
{ required: true, message: t('dynamicsForm.paramForm.field.requiredMessage'), trigger: 'blur' },
{ pattern: /^[a-zA-Z0-9_]+$/, message: t('dynamicsForm.paramForm.field.requiredMessage2'), trigger: 'blur' }
]
})
const dialogVisible = ref<boolean>(false)
watch(dialogVisible, (bool) => {
if (!bool) {
form.value = {
name: '',
variable: '',
type: 'input',
is_required: true,
assignment_method: 'api_input',
optionList: [''],
default_value: ''
}
isEdit.value = false
}
})
const open = (row: any) => {
if (row) {
form.value = cloneDeep(row)
isEdit.value = true
}
dialogVisible.value = true
}
const close = () => {
dialogVisible.value = false
isEdit.value = false
}
const submit = async (formEl: FormInstance | undefined) => {
if (!formEl) return
await formEl.validate((valid) => {
if (valid) {
emit('refresh', form.value)
}
})
}
defineExpose({ open, close })
</script>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,157 @@
<template>
<div class="flex-between mb-16">
<h5 class="lighter">{{ $t('views.template.templateForm.title.apiParamPassing') }}</h5>
<el-button link type="primary" @click="openAddDialog()">
<el-icon class="mr-4">
<Plus />
</el-icon>
{{ $t('common.add') }}
</el-button>
</div>
<el-table
v-if="props.nodeModel.properties.api_input_field_list?.length > 0"
:data="props.nodeModel.properties.api_input_field_list"
class="mb-16"
ref="tableRef"
row-key="field"
>
<el-table-column prop="variable" :label="$t('dynamicsForm.paramForm.field.label')">
<template #default="{ row }">
<span class="ellipsis-1" :title="row.variable">
{{ row.variable }}
</span>
</template>
</el-table-column>
<el-table-column prop="default_value" :label="$t('dynamicsForm.default.label')">
<template #default="{ row }">
<span class="ellipsis-1" :title="row.default_value">
{{ row.default_value }}
</span>
</template>
</el-table-column>
<el-table-column :label="$t('common.required')">
<template #default="{ row }">
<div @click.stop>
<el-switch disabled size="small" v-model="row.is_required" />
</div>
</template>
</el-table-column>
<el-table-column :label="$t('common.operation')" align="left" width="90">
<template #default="{ row, $index }">
<span class="mr-4">
<el-tooltip effect="dark" :content="$t('common.modify')" placement="top">
<el-button type="primary" text @click.stop="openAddDialog(row, $index)">
<el-icon><EditPen /></el-icon>
</el-button>
</el-tooltip>
</span>
<el-tooltip effect="dark" :content="$t('common.delete')" placement="top">
<el-button type="primary" text @click="deleteField($index)">
<el-icon>
<Delete />
</el-icon>
</el-button>
</el-tooltip>
</template>
</el-table-column>
</el-table>
<ApiFieldFormDialog ref="ApiFieldFormDialogRef" @refresh="refreshFieldList" />
</template>
<script setup lang="ts">
import { onMounted, ref } from 'vue'
import { set } from 'lodash'
import Sortable from 'sortablejs'
import ApiFieldFormDialog from './ApiFieldFormDialog.vue'
import { MsgError } from '@/utils/message'
import { t } from '@/locales'
const props = defineProps<{ nodeModel: any }>()
const tableRef = ref()
const currentIndex = ref(null)
const ApiFieldFormDialogRef = ref()
const inputFieldList = ref<any[]>([])
function openAddDialog(data?: any, index?: any) {
if (typeof index !== 'undefined') {
currentIndex.value = index
}
ApiFieldFormDialogRef.value.open(data)
}
function deleteField(index: any) {
inputFieldList.value.splice(index, 1)
props.nodeModel.graphModel.eventCenter.emit('refreshFieldList')
onDragHandle()
}
function refreshFieldList(data: any) {
for (let i = 0; i < inputFieldList.value.length; i++) {
if (inputFieldList.value[i].variable === data.variable && currentIndex.value !== i) {
MsgError(t('views.applicationWorkflow.tip.paramErrorMessage') + data.variable)
return
}
}
// list
let arr = props.nodeModel.properties.user_input_field_list
for (let i = 0; i < arr.length; i++) {
if (arr[i].field === data.variable) {
MsgError(t('views.applicationWorkflow.tip.paramErrorMessage') + data.variable)
return
}
}
if (currentIndex.value !== null) {
inputFieldList.value.splice(currentIndex.value, 1, data)
} else {
inputFieldList.value.push(data)
}
currentIndex.value = null
ApiFieldFormDialogRef.value.close()
props.nodeModel.graphModel.eventCenter.emit('refreshFieldList')
onDragHandle()
}
//
function onDragHandle() {
if (!tableRef.value) return
// tbody DOM
const wrapper = tableRef.value.$el as HTMLElement
const tbody = wrapper.querySelector('.el-table__body-wrapper tbody')
if (!tbody) return
// Sortable
Sortable.create(tbody as HTMLElement, {
animation: 150,
ghostClass: 'ghost-row',
onEnd: (evt) => {
if (evt.oldIndex === undefined || evt.newIndex === undefined) return
//
const items = [...inputFieldList.value]
const [movedItem] = items.splice(evt.oldIndex, 1)
items.splice(evt.newIndex, 0, movedItem)
inputFieldList.value = items
props.nodeModel.graphModel.eventCenter.emit('refreshFieldList')
}
})
}
onMounted(() => {
if (!props.nodeModel.properties.api_input_field_list) {
if (props.nodeModel.properties.input_field_list) {
props.nodeModel.properties.input_field_list
.filter((item: any) => {
return item.assignment_method === 'api_input'
})
.forEach((item: any) => {
inputFieldList.value.push(item)
})
}
} else {
inputFieldList.value.push(...props.nodeModel.properties.api_input_field_list)
}
set(props.nodeModel.properties, 'api_input_field_list', inputFieldList)
})
</script>
<style scoped lang="scss"></style>

View File

@ -0,0 +1,288 @@
<template>
<el-dialog
:title="$t('views.applicationWorkflow.nodes.baseNode.FileUploadSetting.title')"
v-model="dialogVisible"
:close-on-click-modal="false"
:close-on-press-escape="false"
:destroy-on-close="true"
:before-close="close"
append-to-body
width="800"
>
<el-form
label-position="top"
ref="fieldFormRef"
:model="form_data"
require-asterisk-position="right"
>
<el-form-item
:label="$t('views.applicationWorkflow.nodes.baseNode.FileUploadSetting.maxFiles')"
>
<el-slider
v-model="form_data.maxFiles"
show-input
:show-input-controls="false"
:min="1"
:max="10"
/>
</el-form-item>
<el-form-item
:label="$t('views.applicationWorkflow.nodes.baseNode.FileUploadSetting.fileLimit')"
>
<el-slider
v-model="form_data.fileLimit"
show-input
:show-input-controls="false"
:min="1"
:max="100"
/>
</el-form-item>
<el-form-item
:label="
$t('views.applicationWorkflow.nodes.baseNode.FileUploadSetting.fileUploadType.label')
"
>
<el-card
shadow="hover"
class="card-checkbox cursor w-full mb-8"
:class="form_data.document ? 'active' : ''"
style="--el-card-padding: 8px 16px"
@click.stop="form_data.document = !form_data.document"
>
<div class="flex-between">
<div class="flex align-center">
<img class="mr-12" src="@/assets/icon_file-doc.svg" alt="" />
<div>
<p class="line-height-22 mt-4">
{{ $t('common.fileUpload.document') }}
<el-text class="color-secondary"
>{{
$t(
'views.applicationWorkflow.nodes.baseNode.FileUploadSetting.fileUploadType.documentText'
)
}}
</el-text>
</p>
<p>{{ documentExtensions.join('、') }}</p>
</div>
</div>
<el-checkbox
v-model="form_data.document"
@change="form_data.document = !form_data.document"
/>
</div>
</el-card>
<el-card
shadow="hover"
class="card-checkbox cursor w-full mb-8"
:class="form_data.image ? 'active' : ''"
style="--el-card-padding: 8px 16px"
@click.stop="form_data.image = !form_data.image"
>
<div class="flex-between">
<div class="flex align-center">
<img class="mr-12" src="@/assets/icon_file-image.svg" alt="" />
<div>
<p class="line-height-22 mt-4">
{{ $t('common.fileUpload.image') }}
<el-text class="color-secondary"
>{{
$t(
'views.applicationWorkflow.nodes.baseNode.FileUploadSetting.fileUploadType.imageText'
)
}}
</el-text>
</p>
<p>{{ imageExtensions.join('、') }}</p>
</div>
</div>
<el-checkbox v-model="form_data.image" @change="form_data.image = !form_data.image" />
</div>
</el-card>
<el-card
shadow="hover"
class="card-checkbox cursor w-full mb-8"
:class="form_data.audio ? 'active' : ''"
style="--el-card-padding: 8px 16px"
@click.stop="form_data.audio = !form_data.audio"
>
<div class="flex-between">
<div class="flex align-center">
<img class="mr-12" src="@/assets/icon_file-audio.svg" alt="" />
<div>
<p class="line-height-22 mt-4">
{{ $t('common.fileUpload.audio') }}
<el-text class="color-secondary"
>{{
$t(
'views.applicationWorkflow.nodes.baseNode.FileUploadSetting.fileUploadType.audioText'
)
}}
</el-text>
</p>
<p>{{ audioExtensions.join('、') }}</p>
</div>
</div>
<el-checkbox v-model="form_data.audio" @change="form_data.audio = !form_data.audio" />
</div>
</el-card>
<el-card
shadow="hover"
class="card-checkbox cursor w-full mb-8"
:class="form_data.other ? 'active' : ''"
style="--el-card-padding: 8px 16px"
@click.stop="form_data.other = !form_data.other"
>
<div class="flex-between">
<div class="flex align-center">
<img class="mr-12" :width="32" src="@/assets/fileType/unknown-icon.svg" alt="" />
<div>
<p class="line-height-22 mt-4">
{{ $t('common.fileUpload.other') }}
<el-text class="color-secondary"
>{{
$t(
'views.applicationWorkflow.nodes.baseNode.FileUploadSetting.fileUploadType.otherText'
)
}}
</el-text>
</p>
<el-space wrap :size="2" class="mt-4">
<el-tag
v-for="tag in form_data.otherExtensions"
:key="tag"
closable
:disable-transitions="false"
@close="handleClose(tag)"
type="info"
class="mr-4"
effect="plain"
style="
--el-tag-border-radius: 4px;
--el-tag-border-color: var(--el-border-color);
"
>
{{ tag }}
</el-tag>
<el-input
v-if="inputVisible"
ref="InputRef"
v-model="inputValue"
size="small"
@keyup.enter="handleInputConfirm"
@blur="handleInputConfirm"
/>
<el-button v-else class="button-new-tag" size="small" @click.stop="showInput">
+ {{ $t('common.fileUpload.addExtensions') }}
</el-button>
</el-space>
</div>
</div>
<el-checkbox v-model="form_data.other" @change="form_data.other = !form_data.other" />
</div>
</el-card>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click.prevent="close"> {{ $t('common.cancel') }} </el-button>
<el-button type="primary" @click="submit()" :loading="loading">
{{ $t('common.confirm') }}
</el-button>
</span>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { nextTick, ref } from 'vue'
import type { InputInstance } from 'element-plus'
import { cloneDeep } from 'lodash'
import { MsgWarning } from '@/utils/message'
import { t } from '@/locales'
const emit = defineEmits(['refresh'])
const props = defineProps<{ nodeModel: any }>()
const dialogVisible = ref(false)
const inputVisible = ref(false)
const inputValue = ref('')
const loading = ref(false)
const fieldFormRef = ref()
const InputRef = ref<InputInstance>()
const documentExtensions = ['TXT', 'MD', 'DOCX', 'HTML', 'CSV', 'XLSX', 'XLS', 'PDF']
const imageExtensions = ['JPG', 'JPEG', 'PNG', 'GIF']
const audioExtensions = ['MP3', 'WAV', 'OGG', 'ACC', 'M4A']
const form_data = ref({
maxFiles: 3,
fileLimit: 50,
document: true,
image: false,
audio: false,
video: false,
other: false,
otherExtensions: ['PPT', 'DOC']
})
function open(data: any) {
dialogVisible.value = true
nextTick(() => {
form_data.value = { ...form_data.value, ...data }
})
}
function close() {
dialogVisible.value = false
}
const handleClose = (tag: string) => {
form_data.value.otherExtensions = form_data.value.otherExtensions.filter((item) => item !== tag)
}
const showInput = () => {
inputVisible.value = true
nextTick(() => {
InputRef.value!.input!.focus()
})
}
const handleInputConfirm = () => {
if (inputValue.value) {
inputValue.value = inputValue.value.toUpperCase()
if (
form_data.value.otherExtensions.includes(inputValue.value) ||
documentExtensions.includes(inputValue.value) ||
imageExtensions.includes(inputValue.value) ||
audioExtensions.includes(inputValue.value)
) {
inputVisible.value = false
inputValue.value = ''
MsgWarning(t('common.fileUpload.existingExtensionsTip'))
return
}
form_data.value.otherExtensions.push(inputValue.value)
}
inputVisible.value = false
inputValue.value = ''
}
async function submit() {
const formEl = fieldFormRef.value
if (!formEl) return
await formEl.validate().then(() => {
const formattedData = cloneDeep(form_data.value)
emit('refresh', formattedData)
// emit('refresh', form_data.value)
props.nodeModel.graphModel.eventCenter.emit('refreshFileUploadConfig')
dialogVisible.value = false
})
}
defineExpose({
open
})
</script>
<style scoped lang="scss"></style>

View File

@ -0,0 +1,168 @@
<template>
<el-dialog
:title="
isEdit
? $t('views.template.templateForm.title.editParam')
: $t('views.template.templateForm.title.addParam')
"
v-model="dialogVisible"
:close-on-click-modal="false"
:close-on-press-escape="false"
:destroy-on-close="true"
:before-close="close"
append-to-body
>
<DynamicsFormConstructor
v-model="currentRow"
label-position="top"
require-asterisk-position="right"
:input_type_list="inputTypeList"
ref="DynamicsFormConstructorRef"
></DynamicsFormConstructor>
<template #footer>
<span class="dialog-footer">
<el-button @click.prevent="close"> {{ $t('common.cancel') }} </el-button>
<el-button type="primary" @click="submit()" :loading="loading">
{{ isEdit ? $t('common.save') : $t('common.add') }}
</el-button>
</span>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
import { cloneDeep } from 'lodash'
import DynamicsFormConstructor from '@/components/dynamics-form/constructor/index.vue'
import type { FormField } from '@/components/dynamics-form/type'
import _ from 'lodash'
import { t } from '@/locales'
const emit = defineEmits(['refresh'])
const DynamicsFormConstructorRef = ref()
const loading = ref<boolean>(false)
const isEdit = ref(false)
const currentItem = ref<FormField | any>()
const check_field = (field_list: Array<string>, obj: any) => {
return field_list.every((field) => _.get(obj, field, undefined) !== undefined)
}
const currentRow = computed(() => {
if (currentItem.value) {
const row = currentItem.value
switch (row.type) {
case 'input':
if (check_field(['field', 'input_type', 'label', 'required', 'attrs'], currentItem.value)) {
return currentItem.value
}
return {
attrs: row.attrs || { maxlength: 200, minlength: 0 },
field: row.field || row.variable,
input_type: 'TextInput',
label: row.label || row.name,
default_value: row.default_value,
required: row.required != undefined ? row.required : row.is_required
}
case 'select':
if (
check_field(
['field', 'input_type', 'label', 'required', 'option_list'],
currentItem.value
)
) {
return currentItem.value
}
return {
attrs: row.attrs || {},
field: row.field || row.variable,
input_type: 'SingleSelect',
label: row.label || row.name,
default_value: row.default_value,
required: row.required != undefined ? row.required : row.is_required,
option_list: row.option_list
? row.option_list
: row.optionList.map((o: any) => {
return { key: o, value: o }
})
}
case 'date':
if (
check_field(
[
'field',
'input_type',
'label',
'required',
'attrs.format',
'attrs.value-format',
'attrs.type'
],
currentItem.value
)
) {
return currentItem.value
}
return {
field: row.field || row.variable,
input_type: 'DatePicker',
label: row.label || row.name,
default_value: row.default_value,
required: row.required != undefined ? row.required : row.is_required,
attrs: {
format: 'YYYY-MM-DD HH:mm:ss',
'value-format': 'YYYY-MM-DD HH:mm:ss',
type: 'datetime'
}
}
default:
return currentItem.value
}
} else {
return { input_type: 'TextInput', required: false, attrs: { maxlength: 200, minlength: 0 }, show_default_value: true }
}
})
const currentIndex = ref(null)
const inputTypeList = ref([
{ label: t('dynamicsForm.input_type_list.TextInput'), value: 'TextInputConstructor' },
{ label: t('dynamicsForm.input_type_list.PasswordInput'), value: 'PasswordInputConstructor' },
{ label: t('dynamicsForm.input_type_list.SingleSelect'), value: 'SingleSelectConstructor' },
{ label: t('dynamicsForm.input_type_list.MultiSelect'), value: 'MultiSelectConstructor' },
{ label: t('dynamicsForm.input_type_list.RadioCard'), value: 'RadioCardConstructor' },
{ label: t('dynamicsForm.input_type_list.DatePicker'), value: 'DatePickerConstructor' },
{ label: t('dynamicsForm.input_type_list.SwitchInput'), value: 'SwitchInputConstructor' },
])
const dialogVisible = ref<boolean>(false)
const open = (row: any, index: any) => {
dialogVisible.value = true
if (row) {
isEdit.value = true
currentItem.value = cloneDeep(row)
currentIndex.value = index
} else {
currentItem.value = null
}
}
const close = () => {
dialogVisible.value = false
isEdit.value = false
currentIndex.value = null
currentItem.value = null as any
}
const submit = async () => {
const formEl = DynamicsFormConstructorRef.value
if (!formEl) return
await formEl.validate().then(() => {
emit('refresh', formEl?.getData(), currentIndex.value)
isEdit.value = false
currentItem.value = null as any
currentIndex.value = null
})
}
defineExpose({ open, close })
</script>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,258 @@
<template>
<div class="flex-between mb-16">
<h5 class="break-all ellipsis lighter" style="max-width:80%" :title="inputFieldConfig.title">
{{ inputFieldConfig.title }}
</h5>
<div>
<el-button type="primary" link @click="openChangeTitleDialog">
<el-icon>
<Setting />
</el-icon>
</el-button>
<span class="ml-4">
<el-button link type="primary" @click="openAddDialog()">
<el-icon class="mr-4">
<Plus />
</el-icon>
{{ $t('common.add') }}
</el-button>
</span>
</div>
</div>
<el-table
v-if="props.nodeModel.properties.user_input_field_list?.length > 0"
:data="props.nodeModel.properties.user_input_field_list"
class="mb-16"
ref="tableRef"
row-key="field"
>
<el-table-column prop="field" :label="$t('dynamicsForm.paramForm.field.label')" width="95">
<template #default="{ row }">
<span :title="row.field" class="ellipsis-1">{{ row.field }}</span>
</template>
</el-table-column>
<el-table-column prop="label" :label="$t('dynamicsForm.paramForm.name.label')">
<template #default="{ row }">
<span v-if="row.label && row.label.input_type === 'TooltipLabel'">
<span :title="row.label.label" class="ellipsis-1">
{{ row.label.label }}
</span>
</span>
<span v-else>
<span :title="row.label" class="ellipsis-1">
{{ row.label }}
</span></span
>
</template>
</el-table-column>
<el-table-column :label="$t('dynamicsForm.paramForm.input_type.label')" width="95">
<template #default="{ row }">
<el-tag type="info" class="info-tag" v-if="row.input_type === 'TextInput'">{{
$t('dynamicsForm.input_type_list.TextInput')
}}</el-tag>
<el-tag type="info" class="info-tag" v-if="row.input_type === 'PasswordInput'">{{
$t('dynamicsForm.input_type_list.PasswordInput')
}}</el-tag>
<el-tag type="info" class="info-tag" v-if="row.input_type === 'Slider'">{{
$t('dynamicsForm.input_type_list.Slider')
}}</el-tag>
<el-tag type="info" class="info-tag" v-if="row.input_type === 'SwitchInput'">{{
$t('dynamicsForm.input_type_list.SwitchInput')
}}</el-tag>
<el-tag type="info" class="info-tag" v-if="row.input_type === 'SingleSelect'">{{
$t('dynamicsForm.input_type_list.SingleSelect')
}}</el-tag>
<el-tag type="info" class="info-tag" v-if="row.input_type === 'MultiSelect'">{{
$t('dynamicsForm.input_type_list.MultiSelect')
}}</el-tag>
<el-tag type="info" class="info-tag" v-if="row.input_type === 'RadioCard'">{{
$t('dynamicsForm.input_type_list.RadioCard')
}}</el-tag>
<el-tag type="info" class="info-tag" v-if="row.input_type === 'DatePicker'">{{
$t('dynamicsForm.input_type_list.DatePicker')
}}</el-tag>
</template>
</el-table-column>
<el-table-column prop="default_value" :label="$t('dynamicsForm.default.label')">
<template #default="{ row }">
<span :title="row.default_value" class="ellipsis-1">{{ getDefaultValue(row) }}</span>
</template>
</el-table-column>
<el-table-column :label="$t('common.required')">
<template #default="{ row }">
<div @click.stop>
<el-switch disabled size="small" v-model="row.required" />
</div>
</template>
</el-table-column>
<el-table-column :label="$t('common.operation')" align="left" width="90">
<template #default="{ row, $index }">
<span class="mr-4">
<el-tooltip effect="dark" :content="$t('common.modify')" placement="top">
<el-button type="primary" text @click.stop="openAddDialog(row, $index)">
<el-icon><EditPen /></el-icon>
</el-button>
</el-tooltip>
</span>
<el-tooltip effect="dark" :content="$t('common.delete')" placement="top">
<el-button type="primary" text @click="deleteField($index)">
<el-icon>
<Delete />
</el-icon>
</el-button>
</el-tooltip>
</template>
</el-table-column>
</el-table>
<UserFieldFormDialog ref="UserFieldFormDialogRef" @refresh="refreshFieldList" />
<UserInputTitleDialog ref="UserInputTitleDialogRef" @refresh="refreshFieldTitle" />
</template>
<script setup lang="ts">
import { onMounted, ref } from 'vue'
import { set } from 'lodash'
import Sortable from 'sortablejs'
import UserFieldFormDialog from './UserFieldFormDialog.vue'
import { MsgError } from '@/utils/message'
import { t } from '@/locales'
import UserInputTitleDialog from '@/workflow/nodes/base-node/component/UserInputTitleDialog.vue'
const props = defineProps<{ nodeModel: any }>()
const tableRef = ref()
const UserFieldFormDialogRef = ref()
const UserInputTitleDialogRef = ref()
const inputFieldList = ref<any[]>([])
const inputFieldConfig = ref({ title: t('chat.userInput') })
function openAddDialog(data?: any, index?: any) {
UserFieldFormDialogRef.value.open(data, index)
}
function openChangeTitleDialog() {
UserInputTitleDialogRef.value.open(inputFieldConfig.value)
}
function deleteField(index: any) {
inputFieldList.value.splice(index, 1)
props.nodeModel.graphModel.eventCenter.emit('refreshFieldList')
onDragHandle()
}
function refreshFieldList(data: any, index: any) {
for (let i = 0; i < inputFieldList.value.length; i++) {
if (inputFieldList.value[i].field === data.field && index !== i) {
MsgError(t('views.applicationWorkflow.tip.paramErrorMessage') + data.field)
return
}
}
// list
let arr = props.nodeModel.properties.api_input_field_list
for (let i = 0; i < arr.length; i++) {
if (arr[i].variable === data.field) {
MsgError(t('views.applicationWorkflow.tip.paramErrorMessage') + data.field)
return
}
}
if (index !== null) {
inputFieldList.value.splice(index, 1, data)
} else {
inputFieldList.value.push(data)
}
UserFieldFormDialogRef.value.close()
props.nodeModel.graphModel.eventCenter.emit('refreshFieldList')
onDragHandle()
}
function refreshFieldTitle(data: any) {
inputFieldConfig.value = data
UserInputTitleDialogRef.value.close()
// console.log('inputFieldConfig', inputFieldConfig.value)
}
const getDefaultValue = (row: any) => {
if (row.input_type === 'PasswordInput') {
return '******'
}
if (row.default_value) {
const default_value = row.option_list
?.filter((v: any) => row.default_value.indexOf(v.value) > -1)
.map((v: any) => v.label)
.join(',')
if (default_value) {
return default_value
}
return row.default_value
}
if (row.default_value !== undefined) {
return row.default_value
}
}
function onDragHandle() {
if (!tableRef.value) return
// tbody DOM
const wrapper = tableRef.value.$el as HTMLElement
const tbody = wrapper.querySelector('.el-table__body-wrapper tbody')
if (!tbody) return
// Sortable
Sortable.create(tbody as HTMLElement, {
animation: 150,
ghostClass: 'ghost-row',
onEnd: (evt) => {
if (evt.oldIndex === undefined || evt.newIndex === undefined) return
//
const items = [...inputFieldList.value]
const [movedItem] = items.splice(evt.oldIndex, 1)
items.splice(evt.newIndex, 0, movedItem)
inputFieldList.value = items
props.nodeModel.graphModel.eventCenter.emit('refreshFieldList')
}
})
}
onMounted(() => {
if (!props.nodeModel.properties.user_input_field_list) {
if (props.nodeModel.properties.input_field_list) {
props.nodeModel.properties.input_field_list
.filter((item: any) => {
return item.assignment_method === 'user_input'
})
.forEach((item: any) => {
inputFieldList.value.push(item)
})
}
} else {
inputFieldList.value.push(...props.nodeModel.properties.user_input_field_list)
}
//
inputFieldList.value.forEach((item, index) => {
item.label = item.label || item.name
item.field = item.field || item.variable
item.required = item.required || item.is_required
switch (item.type) {
case 'input':
item.input_type = 'TextInput'
break
case 'select':
item.input_type = 'SingleSelect'
break
case 'date':
item.input_type = 'DatePicker'
break
}
})
set(props.nodeModel.properties, 'user_input_field_list', inputFieldList)
if (props.nodeModel.properties.user_input_config) {
inputFieldConfig.value = props.nodeModel.properties.user_input_config
}
set(props.nodeModel.properties, 'user_input_config', inputFieldConfig)
onDragHandle()
})
</script>
<style scoped lang="scss"></style>

View File

@ -0,0 +1,82 @@
<template>
<el-dialog
:title="$t('common.setting')"
v-model="dialogVisible"
:close-on-click-modal="false"
:close-on-press-escape="false"
:destroy-on-close="true"
:before-close="close"
append-to-body
>
<el-form
label-position="top"
ref="fieldFormRef"
:rules="rules"
:model="form"
require-asterisk-position="right"
>
<el-form-item :label="$t('common.title')" prop="title">
<el-input
v-model="form.title"
maxlength="64"
show-word-limit
@blur="form.title = form.title.trim()"
/>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click.prevent="dialogVisible = false"> {{ $t('common.cancel') }} </el-button>
<el-button type="primary" @click="submit(fieldFormRef)" :loading="loading">
{{ $t('common.save') }}
</el-button>
</span>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { reactive, ref, watch } from 'vue'
import type { FormInstance } from 'element-plus'
import { cloneDeep } from 'lodash'
import { t } from '@/locales'
const emit = defineEmits(['refresh'])
const fieldFormRef = ref()
const loading = ref<boolean>(false)
const form = ref<any>({
title: t('chat.userInput')
})
const rules = reactive({
title: [
{ required: true, message: t('dynamicsForm.paramForm.name.requiredMessage'), trigger: 'blur' }
]
})
const dialogVisible = ref<boolean>(false)
const open = (row: any) => {
if (row) {
form.value = cloneDeep(row)
}
dialogVisible.value = true
}
const close = () => {
dialogVisible.value = false
}
const submit = async (formEl: FormInstance | undefined) => {
if (!formEl) return
await formEl.validate((valid) => {
if (valid) {
emit('refresh', form.value)
}
})
}
defineExpose({ open, close })
</script>
<style lang="scss" scoped></style>

View File

@ -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
}

View File

@ -0,0 +1,349 @@
<template>
<NodeContainer :nodeModel="nodeModel">
<el-form
@submit.prevent
:model="form_data"
label-position="top"
require-asterisk-position="right"
class="mb-24"
label-width="auto"
ref="baseNodeFormRef"
>
<el-form-item
:label="$t('views.applicationWorkflow.nodes.baseNode.appName.label')"
prop="name"
:rules="{
message: t('views.application.applicationForm.form.appName.requiredMessage'),
trigger: 'blur',
required: true
}"
>
<el-input
v-model="form_data.name"
maxlength="64"
:placeholder="t('views.application.applicationForm.form.appName.placeholder')"
show-word-limit
@blur="form_data.name = form_data.name?.trim()"
/>
</el-form-item>
<el-form-item :label="$t('views.applicationWorkflow.nodes.baseNode.appDescription.label')">
<el-input
v-model="form_data.desc"
:placeholder="$t('views.application.applicationForm.form.appDescription.placeholder')"
:rows="3"
type="textarea"
maxlength="256"
show-word-limit
/>
</el-form-item>
<el-form-item :label="$t('views.application.applicationForm.form.prologue')">
<MdEditorMagnify
@wheel="wheel"
:title="$t('views.application.applicationForm.form.prologue')"
v-model="form_data.prologue"
style="height: 150px"
@submitDialog="submitDialog"
/>
</el-form-item>
<el-form-item>
<template #label>
<div class="flex-between">
<div class="flex align-center">
<span class="mr-4">{{
$t('views.applicationWorkflow.nodes.baseNode.fileUpload.label')
}}</span>
<el-tooltip
effect="dark"
:content="$t('views.applicationWorkflow.nodes.baseNode.fileUpload.tooltip')"
placement="right"
>
<AppIcon iconName="app-warning" class="app-warning-icon"></AppIcon>
</el-tooltip>
</div>
<div>
<el-button
v-if="form_data.file_upload_enable"
type="primary"
link
@click="openFileUploadSettingDialog"
class="mr-4"
>
<el-icon class="mr-4">
<Setting />
</el-icon>
</el-button>
<el-switch
size="small"
v-model="form_data.file_upload_enable"
@change="switchFileUpload"
/>
</div>
</div>
</template>
</el-form-item>
<UserInputFieldTable ref="UserInputFieldTableFef" :node-model="nodeModel" />
<ApiInputFieldTable ref="ApiInputFieldTableFef" :node-model="nodeModel" />
<el-form-item>
<template #label>
<div class="flex-between">
<span class="mr-4">{{
$t('views.application.applicationForm.form.voiceInput.label')
}}</span>
<div class="flex">
<el-checkbox v-if="form_data.stt_model_enable" v-model="form_data.stt_autosend">{{
$t('views.application.applicationForm.form.voiceInput.autoSend')
}}</el-checkbox>
<el-switch
class="ml-8"
size="small"
v-model="form_data.stt_model_enable"
@change="sttModelEnableChange"
/>
</div>
</div>
</template>
<ModelSelect
@wheel="wheel"
v-show="form_data.stt_model_enable"
v-model="form_data.stt_model_id"
:placeholder="$t('views.application.applicationForm.form.voiceInput.placeholder')"
:options="sttModelOptions"
showFooter
:model-type="'STT'"
></ModelSelect>
</el-form-item>
<el-form-item>
<template #label>
<div class="flex-between">
<span class="mr-4">{{
$t('views.application.applicationForm.form.voicePlay.label')
}}</span>
<div class="flex">
<el-checkbox v-if="form_data.tts_model_enable" v-model="form_data.tts_autoplay">{{
$t('views.application.applicationForm.form.voicePlay.autoPlay')
}}</el-checkbox>
<el-switch
class="ml-8"
size="small"
v-model="form_data.tts_model_enable"
@change="ttsModelEnableChange"
/>
</div>
</div>
</template>
<div class="w-full">
<el-radio-group v-model="form_data.tts_type" v-show="form_data.tts_model_enable">
<el-radio
:label="$t('views.application.applicationForm.form.voicePlay.browser')"
value="BROWSER"
/>
<el-radio
:label="$t('views.application.applicationForm.form.voicePlay.tts')"
value="TTS"
/>
</el-radio-group>
</div>
<div class="flex-between w-full">
<ModelSelect
@wheel="wheel"
v-if="form_data.tts_type === 'TTS' && form_data.tts_model_enable"
v-model="form_data.tts_model_id"
:placeholder="$t('views.application.applicationForm.form.voicePlay.placeholder')"
:options="ttsModelOptions"
@change="ttsModelChange()"
showFooter
:model-type="'TTS'"
></ModelSelect>
<el-button
v-if="form_data.tts_type === 'TTS' && form_data.tts_model_enable"
@click="openTTSParamSettingDialog"
:disabled="!form_data.tts_model_id"
class="ml-8"
>
<el-icon>
<el-icon><Operation /></el-icon>
</el-icon>
</el-button>
</div>
</el-form-item>
</el-form>
<TTSModeParamSettingDialog ref="TTSModeParamSettingDialogRef" @refresh="refreshTTSForm" />
<FileUploadSettingDialog
ref="FileUploadSettingDialogRef"
:node-model="nodeModel"
@refresh="refreshFileUploadForm"
/>
</NodeContainer>
</template>
<script setup lang="ts">
import { app } from '@/main'
import { groupBy, set } from 'lodash'
import NodeContainer from '@/workflow/common/NodeContainer.vue'
import type { FormInstance } from 'element-plus'
import { ref, computed, onMounted } from 'vue'
import applicationApi from '@/api/application'
import { MsgError, MsgSuccess, MsgWarning } from '@/utils/message'
import { t } from '@/locales'
import TTSModeParamSettingDialog from '@/views/application/component/TTSModeParamSettingDialog.vue'
import ApiInputFieldTable from './component/ApiInputFieldTable.vue'
import UserInputFieldTable from './component/UserInputFieldTable.vue'
import FileUploadSettingDialog from '@/workflow/nodes/base-node/component/FileUploadSettingDialog.vue'
const {
params: { id }
} = app.config.globalProperties.$route as any
const props = defineProps<{ nodeModel: any }>()
const sttModelOptions = ref<any>(null)
const ttsModelOptions = ref<any>(null)
const TTSModeParamSettingDialogRef = ref<InstanceType<typeof TTSModeParamSettingDialog>>()
const UserInputFieldTableFef = ref()
const ApiInputFieldTableFef = ref()
const FileUploadSettingDialogRef = ref<InstanceType<typeof FileUploadSettingDialog>>()
const form = {
name: '',
desc: '',
prologue: t('views.application.applicationForm.form.defaultPrologue')
}
const wheel = (e: any) => {
if (e.ctrlKey === true) {
e.preventDefault()
return true
} else {
e.stopPropagation()
return true
}
}
function submitDialog(val: string) {
set(props.nodeModel.properties.node_data, 'prologue', val)
}
const form_data = computed({
get: () => {
if (props.nodeModel.properties.node_data) {
return props.nodeModel.properties.node_data
} else {
set(props.nodeModel.properties, 'node_data', form)
}
return props.nodeModel.properties.node_data
},
set: (value) => {
set(props.nodeModel.properties, 'node_data', value)
}
})
const baseNodeFormRef = ref<FormInstance>()
const validate = () => {
if (
form_data.value.tts_model_enable &&
!form_data.value.tts_model_id &&
form_data.value.tts_type === 'TTS'
) {
return Promise.reject({
node: props.nodeModel,
errMessage: t('views.application.applicationForm.form.voicePlay.requiredMessage')
})
}
if (form_data.value.stt_model_enable && !form_data.value.stt_model_id) {
return Promise.reject({
node: props.nodeModel,
errMessage: t('views.application.applicationForm.form.voiceInput.requiredMessage')
})
}
return baseNodeFormRef.value?.validate().catch((err) => {
return Promise.reject({ node: props.nodeModel, errMessage: err })
})
}
function getSTTModel() {
applicationApi.getApplicationSTTModel(id).then((res: any) => {
sttModelOptions.value = groupBy(res?.data, 'provider')
})
}
function getTTSModel() {
applicationApi.getApplicationTTSModel(id).then((res: any) => {
ttsModelOptions.value = groupBy(res?.data, 'provider')
})
}
function ttsModelChange() {
if (form_data.value.tts_model_id) {
TTSModeParamSettingDialogRef.value?.reset_default(form_data.value.tts_model_id, id)
} else {
refreshTTSForm({})
}
}
function ttsModelEnableChange() {
if (!form_data.value.tts_model_enable) {
form_data.value.tts_model_id = ''
form_data.value.tts_type = 'BROWSER'
}
}
function sttModelEnableChange() {
if (!form_data.value.stt_model_enable) {
form_data.value.stt_model_id = ''
}
}
const openTTSParamSettingDialog = () => {
const model_id = form_data.value.tts_model_id
if (!model_id) {
MsgSuccess(t('views.application.applicationForm.form.voicePlay.requiredMessage'))
return
}
TTSModeParamSettingDialogRef.value?.open(model_id, id, form_data.value.tts_model_params_setting)
}
const refreshTTSForm = (data: any) => {
form_data.value.tts_model_params_setting = data
}
const switchFileUpload = () => {
const default_upload_setting = {
maxFiles: 3,
fileLimit: 50,
document: true,
image: false,
audio: false,
video: false,
other: false,
otherExtensions: ['ppt', 'doc']
}
if (form_data.value.file_upload_enable) {
form_data.value.file_upload_setting =
form_data.value.file_upload_setting || default_upload_setting
}
props.nodeModel.graphModel.eventCenter.emit('refreshFileUploadConfig')
}
const openFileUploadSettingDialog = () => {
FileUploadSettingDialogRef.value?.open(form_data.value.file_upload_setting)
}
const refreshFileUploadForm = (data: any) => {
form_data.value.file_upload_setting = data
}
onMounted(() => {
set(props.nodeModel, 'validate', validate)
if (!props.nodeModel.properties.node_data.tts_type) {
set(props.nodeModel.properties.node_data, 'tts_type', 'BROWSER')
}
getTTSModel()
getSTTModel()
})
</script>
<style lang="scss" scoped>
:deep(.el-form-item__label) {
display: block;
}
</style>

View File

@ -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<any>, 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
}

View File

@ -0,0 +1,370 @@
<template>
<NodeContainer :nodeModel="nodeModel">
<el-form
:model="form_data"
label-position="top"
require-asterisk-position="right"
label-width="auto"
ref="ConditionNodeFormRef"
@submit.prevent
>
<VueDraggable
ref="el"
v-bind:modelValue="form_data.branch"
:disabled="form_data.branch === 2"
:filter="'.no-drag'"
handle=".handle"
:animation="150"
ghostClass="ghost"
@end="onEnd"
>
<template v-for="(item, index) in form_data.branch" :key="item.id">
<el-card
v-resize="(wh: any) => resizeCondition(wh, item, index)"
shadow="never"
class="drag-card card-never mb-8"
:class="{
'no-drag': index === form_data.branch.length - 1 || form_data.branch.length === 2
}"
style="--el-card-padding: 12px"
>
<div class="handle flex-between lighter">
<span class="flex align-center">
<img src="@/assets/sort.svg" alt="" height="15" class="handle-img mr-4" />
{{ item.type }}
</span>
<div class="info" v-if="item.conditions.length > 1">
<span>{{
$t('views.applicationWorkflow.nodes.conditionNode.conditions.info')
}}</span>
<el-select
:teleported="false"
v-model="item.condition"
size="small"
style="width: 60px; margin: 0 8px"
>
<el-option :label="$t('views.applicationWorkflow.condition.AND')" value="and" />
<el-option :label="$t('views.applicationWorkflow.condition.OR')" value="or" />
</el-select>
<span>{{
$t('views.applicationWorkflow.nodes.conditionNode.conditions.label')
}}</span>
</div>
</div>
<div v-if="index !== form_data.branch.length - 1" class="mt-8">
<template v-for="(condition, cIndex) in item.conditions" :key="cIndex">
<el-row :gutter="8">
<el-col :span="11">
<el-form-item
:prop="'branch.' + index + '.conditions.' + cIndex + '.field'"
:rules="{
type: 'array',
required: true,
message: $t('views.applicationWorkflow.variable.placeholder'),
trigger: 'change'
}"
>
<NodeCascader
ref="nodeCascaderRef"
:nodeModel="nodeModel"
class="w-full"
:placeholder="$t('views.applicationWorkflow.variable.placeholder')"
v-model="condition.field"
/>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item
:prop="'branch.' + index + '.conditions.' + cIndex + '.compare'"
:rules="{
required: true,
message: $t(
'views.applicationWorkflow.nodes.conditionNode.conditions.requiredMessage'
),
trigger: 'change'
}"
>
<el-select
@wheel="wheel"
:teleported="false"
v-model="condition.compare"
:placeholder="
$t(
'views.applicationWorkflow.nodes.conditionNode.conditions.requiredMessage'
)
"
clearable
@change="changeCondition($event, index, cIndex)"
>
<template v-for="(item, index) in compareList" :key="index">
<el-option :label="item.label" :value="item.value" />
</template>
</el-select>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item
v-if="
!['is_null', 'is_not_null', 'is_true', 'is_not_true'].includes(
condition.compare
)
"
:prop="'branch.' + index + '.conditions.' + cIndex + '.value'"
:rules="{
required: true,
message: $t('views.applicationWorkflow.nodes.conditionNode.valueMessage'),
trigger: 'blur'
}"
>
<el-input
v-model="condition.value"
:placeholder="
$t('views.applicationWorkflow.nodes.conditionNode.valueMessage')
"
/>
</el-form-item>
</el-col>
<el-col :span="1">
<el-button
:disabled="index === 0 && cIndex === 0"
link
type="info"
class="mt-4"
@click="deleteCondition(index, cIndex)"
>
<el-icon><Delete /></el-icon>
</el-button>
</el-col>
</el-row>
</template>
</div>
<el-button
link
type="primary"
@click="addCondition(index)"
v-if="index !== form_data.branch.length - 1"
>
<el-icon class="mr-4"><Plus /></el-icon>
{{ $t('views.applicationWorkflow.nodes.conditionNode.addCondition') }}
</el-button>
</el-card>
</template>
</VueDraggable>
<el-button link type="primary" @click="addBranch">
<el-icon class="mr-4"><Plus /></el-icon>
{{ $t('views.applicationWorkflow.nodes.conditionNode.addBranch') }}
</el-button>
</el-form>
</NodeContainer>
</template>
<script setup lang="ts">
import { cloneDeep, set } from 'lodash'
import NodeContainer from '@/workflow/common/NodeContainer.vue'
import NodeCascader from '@/workflow/common/NodeCascader.vue'
import type { FormInstance } from 'element-plus'
import { ref, computed, onMounted, nextTick } from 'vue'
import { randomId } from '@/utils/utils'
import { compareList } from '@/workflow/common/data'
import { VueDraggable } from 'vue-draggable-plus'
const props = defineProps<{ nodeModel: any }>()
const form = {
branch: [
{
conditions: [
{
field: [],
compare: '',
value: ''
}
],
id: randomId(),
type: 'IF',
condition: 'and'
},
{
conditions: [],
id: randomId(),
type: 'ELSE',
condition: 'and'
}
]
}
const wheel = (e: any) => {
if (e.ctrlKey === true) {
e.preventDefault()
return true
} else {
e.stopPropagation()
return true
}
}
const resizeCondition = (wh: any, row: any, index: number) => {
const branch_condition_list = cloneDeep(
props.nodeModel.properties.branch_condition_list
? props.nodeModel.properties.branch_condition_list
: []
)
const new_branch_condition_list = branch_condition_list.map((item: any) => {
if (item.id === row.id) {
return { ...item, height: wh.height, index: index }
}
return item
})
set(props.nodeModel.properties, 'branch_condition_list', new_branch_condition_list)
refreshBranchAnchor(props.nodeModel.properties.node_data.branch, true)
}
const form_data = computed({
get: () => {
if (props.nodeModel.properties.node_data) {
return props.nodeModel.properties.node_data
} else {
set(props.nodeModel.properties, 'node_data', form)
refreshBranchAnchor(form.branch, true)
}
return props.nodeModel.properties.node_data
},
set: (value) => {
set(props.nodeModel.properties, 'node_data', value)
}
})
const ConditionNodeFormRef = ref<FormInstance>()
const nodeCascaderRef = ref()
const validate = () => {
const v_list = [
ConditionNodeFormRef.value?.validate(),
...nodeCascaderRef.value.map((item: any) => item.validate())
]
return Promise.all(v_list).catch((err) => {
return Promise.reject({ node: props.nodeModel, errMessage: err })
})
}
function onEnd(event?: any) {
const { oldIndex, newIndex } = event
if (oldIndex === undefined || newIndex === undefined) return
const list = cloneDeep(props.nodeModel.properties.node_data.branch)
if (oldIndex === list.length - 1 || newIndex === list.length - 1) {
return
}
const newInstance = { ...list[oldIndex], type: list[newIndex].type, id: list[newIndex].id }
const oldInstance = { ...list[newIndex], type: list[oldIndex].type, id: list[oldIndex].id }
list[newIndex] = newInstance
list[oldIndex] = oldInstance
set(props.nodeModel.properties.node_data, 'branch', list)
}
function addBranch() {
const list = cloneDeep(props.nodeModel.properties.node_data.branch)
const obj = {
conditions: [
{
field: [],
compare: '',
value: ''
}
],
type: 'ELSE IF ' + (list.length - 1),
id: randomId(),
condition: 'and'
}
list.splice(list.length - 1, 0, obj)
refreshBranchAnchor(list, true)
set(props.nodeModel.properties.node_data, 'branch', list)
}
function refreshBranchAnchor(list: Array<any>, is_add: boolean) {
const branch_condition_list = cloneDeep(
props.nodeModel.properties.branch_condition_list
? props.nodeModel.properties.branch_condition_list
: []
)
const new_branch_condition_list = list
.map((item, index) => {
const find = branch_condition_list.find((b: any) => b.id === item.id)
if (find) {
return { index: index, height: find.height, id: item.id }
} else {
if (is_add) {
return { index: index, height: 12, id: item.id }
}
}
})
.filter((item) => item)
set(props.nodeModel.properties, 'branch_condition_list', new_branch_condition_list)
props.nodeModel.refreshBranch()
}
function addCondition(index: number) {
const list = cloneDeep(props.nodeModel.properties.node_data.branch)
list[index]['conditions'].push({
field: [],
compare: '',
value: ''
})
set(props.nodeModel.properties.node_data, 'branch', list)
}
function deleteCondition(index: number, cIndex: number) {
const list = cloneDeep(props.nodeModel.properties.node_data.branch)
list[index]['conditions'].splice(cIndex, 1)
if (list[index]['conditions'].length === 0) {
const delete_edge = list.splice(index, 1)
const delete_target_anchor_id_list = delete_edge.map(
(item: any) => props.nodeModel.id + '_' + item.id + '_right'
)
props.nodeModel.graphModel.eventCenter.emit(
'delete_edge',
props.nodeModel.outgoing.edges
.filter((item: any) => delete_target_anchor_id_list.includes(item.sourceAnchorId))
.map((item: any) => item.id)
)
refreshBranchAnchor(list, false)
list.forEach((item: any, index: number) => {
if (item.type === 'ELSE IF ' + (index + 1)) {
item.type = 'ELSE IF ' + index
}
})
}
set(props.nodeModel.properties.node_data, 'branch', list)
}
function changeCondition(val: string, index: number, cIndex: number) {
if (['is_null', 'is_not_null', 'is_true', 'is_not_true'].includes(val)) {
const list = cloneDeep(props.nodeModel.properties.node_data.branch)
list[index]['conditions'][cIndex].value = 1
set(props.nodeModel.properties.node_data, 'branch', list)
}
}
onMounted(() => {
set(props.nodeModel, 'validate', validate)
})
</script>
<style lang="scss" scoped>
.drag-card.no-drag {
.handle {
.handle-img {
display: none;
}
}
}
.drag-card:not(.no-drag) {
.handle {
.handle-img {
display: none;
}
&:hover {
.handle-img {
display: block;
}
}
}
}
</style>

View File

@ -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
}

View File

@ -0,0 +1,64 @@
<template>
<NodeContainer :nodeModel="nodeModel">
<h5 class="title-decoration-1 mb-8">{{ $t('views.applicationWorkflow.nodeSetting') }}</h5>
<el-card shadow="never" class="card-never">
<el-form
@submit.prevent
:model="form_data"
label-position="top"
require-asterisk-position="right"
label-width="auto"
ref="DatasetNodeFormRef"
>
<el-form-item :label="$t('views.problem.relateParagraph.selectDocument')" :rules="{
type: 'array',
required: true,
message: $t('views.log.documentPlaceholder'),
trigger: 'change'
}"
>
<NodeCascader
ref="nodeCascaderRef"
:nodeModel="nodeModel"
class="w-full"
:placeholder="$t('views.log.documentPlaceholder')"
v-model="form_data.document_list"
/>
</el-form-item>
</el-form>
</el-card>
</NodeContainer>
</template>
<script setup lang="ts">
import NodeContainer from '@/workflow/common/NodeContainer.vue'
import { computed } from 'vue'
import { set } from 'lodash'
import NodeCascader from '@/workflow/common/NodeCascader.vue'
const props = defineProps<{ nodeModel: any }>()
const form = {
document_list: ["start-node", "document"]
}
const form_data = computed({
get: () => {
if (props.nodeModel.properties.node_data) {
return props.nodeModel.properties.node_data
} else {
set(props.nodeModel.properties, 'node_data', form)
}
return props.nodeModel.properties.node_data
},
set: (value) => {
set(props.nodeModel.properties, 'node_data', value)
}
})
</script>
<style lang="scss" scoped>
</style>

View File

@ -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
}

View File

@ -0,0 +1,281 @@
<template>
<NodeContainer :nodeModel="nodeModel">
<h5 class="title-decoration-1 mb-8">{{ $t('views.applicationWorkflow.nodeSetting') }}</h5>
<el-card shadow="never" class="card-never" style="--el-card-padding: 12px">
<el-form
@submit.prevent
:model="form_data"
label-position="top"
require-asterisk-position="right"
label-width="auto"
ref="formNodeFormRef"
hide-required-asterisk
>
<el-form-item
:label="$t('views.applicationWorkflow.nodes.formNode.formContent.label')"
prop="form_content_format"
:rules="{
required: true,
message: $t('views.applicationWorkflow.nodes.formNode.formContent.requiredMessage'),
trigger: 'blur'
}"
>
<template #label>
<div class="flex align-center">
<div class="mr-4">
<span
>{{ $t('views.applicationWorkflow.nodes.formNode.formContent.label')
}}<span class="danger">*</span></span
>
</div>
<el-tooltip effect="dark" placement="right" popper-class="max-w-200">
<template #content>
{{
$t('views.applicationWorkflow.nodes.formNode.formContent.tooltip', {
form: '{ form }'
})
}}
</template>
<AppIcon iconName="app-warning" class="app-warning-icon"></AppIcon>
</el-tooltip>
</div>
</template>
<MdEditorMagnify
:title="$t('views.applicationWorkflow.nodes.formNode.formContent.label')"
v-model="form_data.form_content_format"
style="height: 150px"
@submitDialog="submitDialog"
/>
</el-form-item>
<el-form-item
:label="$t('views.applicationWorkflow.nodes.formNode.formSetting')"
@click.prevent
>
<template #label>
<div class="flex-between">
<h5 class="lighter">
{{ $t('views.applicationWorkflow.nodes.formNode.formSetting') }}
</h5>
<el-button link type="primary" @click="openAddFormCollect()">
<el-icon class="mr-4">
<Plus />
</el-icon>
{{ $t('common.add') }}
</el-button>
</div></template
>
<el-table
class="border"
v-if="form_data.form_field_list.length > 0"
:data="form_data.form_field_list"
ref="tableRef"
row-key="field"
>
<el-table-column
prop="field"
:label="$t('dynamicsForm.paramForm.field.label')"
width="95"
>
<template #default="{ row }">
<span :title="row.field" class="ellipsis-1">{{ row.field }}</span>
</template>
</el-table-column>
<el-table-column prop="label" :label="$t('dynamicsForm.paramForm.name.label')">
<template #default="{ row }">
<span v-if="row.label && row.label.input_type === 'TooltipLabel'">
<span :title="row.label.label" class="ellipsis-1">
{{ row.label.label }}
</span>
</span>
<span v-else>
<span :title="row.label" class="ellipsis-1">
{{ row.label }}
</span></span
>
</template>
</el-table-column>
<el-table-column :label="$t('dynamicsForm.paramForm.input_type.label')" width="110px">
<template #default="{ row }">
<el-tag type="info" class="info-tag">{{
input_type_list.find((item) => item.value === row.input_type)?.label
}}</el-tag>
</template>
</el-table-column>
<el-table-column prop="default_value" :label="$t('dynamicsForm.default.label')">
<template #default="{ row }">
<span :title="row.default_value" class="ellipsis-1">{{
getDefaultValue(row)
}}</span>
</template>
</el-table-column>
<el-table-column :label="$t('common.required')" width="85">
<template #default="{ row }">
<div @click.stop>
<el-switch disabled size="small" v-model="row.required" />
</div>
</template>
</el-table-column>
<el-table-column :label="$t('common.operation')" align="left" width="90">
<template #default="{ row, $index }">
<span class="mr-4">
<el-tooltip effect="dark" :content="$t('common.modify')" placement="top">
<el-button type="primary" text @click.stop="openEditFormCollect(row, $index)">
<el-icon><EditPen /></el-icon>
</el-button>
</el-tooltip>
</span>
<el-tooltip effect="dark" :content="$t('common.delete')" placement="top">
<el-button type="primary" text @click="deleteField(row)">
<el-icon>
<Delete />
</el-icon>
</el-button>
</el-tooltip>
</template>
</el-table-column>
</el-table>
</el-form-item>
</el-form>
</el-card>
<AddFormCollect ref="addFormCollectRef" :addFormField="addFormField"></AddFormCollect>
<EditFormCollect ref="editFormCollectRef" :editFormField="editFormField"></EditFormCollect>
</NodeContainer>
</template>
<script setup lang="ts">
import NodeContainer from '@/workflow/common/NodeContainer.vue'
import AddFormCollect from '@/workflow/common/AddFormCollect.vue'
import EditFormCollect from '@/workflow/common/EditFormCollect.vue'
import { type FormInstance } from 'element-plus'
import { ref, onMounted, computed } from 'vue'
import { input_type_list } from '@/components/dynamics-form/constructor/data'
import { MsgError } from '@/utils/message'
import { set, cloneDeep } from 'lodash'
import Sortable from 'sortablejs'
import { t } from '@/locales'
const props = defineProps<{ nodeModel: any }>()
const formNodeFormRef = ref<FormInstance>()
const tableRef = ref()
const editFormField = (form_field_data: any, field_index: number) => {
const _value = form_data.value.form_field_list.map((item: any, index: number) => {
if (field_index === index) {
return cloneDeep(form_field_data)
}
return cloneDeep(item)
})
form_data.value.form_field_list = _value
sync_form_field_list()
}
const addFormField = (form_field_data: any) => {
if (form_data.value.form_field_list.some((field: any) => field.field === form_field_data.field)) {
MsgError(t('views.applicationWorkflow.tip.paramErrorMessage') + form_field_data.field)
return
}
form_data.value.form_field_list = cloneDeep([...form_data.value.form_field_list, form_field_data])
sync_form_field_list()
}
const sync_form_field_list = () => {
const fields = [
{
label: t('views.applicationWorkflow.nodes.formNode.formAllContent'),
value: 'form_data'
},
...form_data.value.form_field_list.map((item: any) => ({
value: item.field,
label: typeof item.label == 'string' ? item.label : item.label.label
}))
]
set(props.nodeModel.properties.config, 'fields', fields)
props.nodeModel.clear_next_node_field(false)
onDragHandle()
}
const addFormCollectRef = ref<InstanceType<typeof AddFormCollect>>()
const editFormCollectRef = ref<InstanceType<typeof EditFormCollect>>()
const openAddFormCollect = () => {
addFormCollectRef.value?.open()
}
const openEditFormCollect = (form_field_data: any, index: number) => {
editFormCollectRef.value?.open(cloneDeep(form_field_data), index)
}
const deleteField = (form_field_data: any) => {
form_data.value.form_field_list = form_data.value.form_field_list.filter(
(field: any) => field.field !== form_field_data.field
)
sync_form_field_list()
}
const form = ref<any>({
is_result: true,
form_content_format: `${t('views.applicationWorkflow.nodes.formNode.form_content_format1')}
{{form}}
${t('views.applicationWorkflow.nodes.formNode.form_content_format2')}`,
form_field_list: []
})
const form_data = computed({
get: () => {
if (props.nodeModel.properties.node_data) {
return props.nodeModel.properties.node_data
} else {
set(props.nodeModel.properties, 'node_data', form.value)
}
return props.nodeModel.properties.node_data
},
set: (value) => {
set(props.nodeModel.properties, 'node_data', value)
}
})
const getDefaultValue = (row: any) => {
if (row.default_value) {
const default_value = row.option_list
?.filter((v: any) => row.default_value.indexOf(v.value) > -1)
.map((v: any) => v.label)
.join(',')
if (default_value) {
return default_value
}
return row.default_value
}
if (row.default_value !== undefined) {
return row.default_value
}
}
const validate = () => {
return formNodeFormRef.value?.validate()
}
function submitDialog(val: string) {
set(props.nodeModel.properties.node_data, 'form_content_format', val)
}
//
function onDragHandle() {
if (!tableRef.value) return
// tbody DOM
const wrapper = tableRef.value.$el as HTMLElement
const tbody = wrapper.querySelector('.el-table__body-wrapper tbody')
if (!tbody) return
// Sortable
Sortable.create(tbody as HTMLElement, {
animation: 150,
ghostClass: 'ghost-row',
onEnd: (evt) => {
if (evt.oldIndex === undefined || evt.newIndex === undefined) return
//
const items = [...form_data.value.form_field_list]
const [movedItem] = items.splice(evt.oldIndex, 1)
items.splice(evt.newIndex, 0, movedItem)
form_data.value.form_field_list = items
sync_form_field_list()
}
})
}
onMounted(() => {
set(props.nodeModel, 'validate', validate)
sync_form_field_list()
props.nodeModel.graphModel.eventCenter.emit('refresh_incoming_node_field')
})
</script>
<style lang="scss" scoped></style>

View File

@ -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
}

Some files were not shown because too many files have changed in this diff Show More