maxkb/ui/src/workflow/nodes/ai-chat-node/index.vue

475 lines
16 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<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.form.aiModel.label')"
prop="model_id"
:rules="{
required: true,
message: $t('views.application.form.aiModel.placeholder'),
trigger: 'change',
}"
>
<template #label>
<div class="flex-between w-full">
<div>
<span
>{{ $t('views.application.form.aiModel.label')
}}<span class="color-danger">*</span></span
>
</div>
<el-button
:disabled="!chat_data.model_id"
type="primary"
link
@click="openAIParamSettingDialog(chat_data.model_id)"
@refreshForm="refreshParam"
>
<AppIcon iconName="app-setting"></AppIcon>
</el-button>
</div>
</template>
<ModelSelect
@change="model_change"
@wheel="wheel"
:teleported="false"
v-model="chat_data.model_id"
:placeholder="$t('views.application.form.aiModel.placeholder')"
:options="modelOptions"
@submitModel="getSelectModel"
showFooter
:model-type="'LLM'"
></ModelSelect>
</el-form-item>
<el-form-item :label="$t('views.application.form.roleSettings.label')">
<MdEditorMagnify
:title="$t('views.application.form.roleSettings.label')"
v-model="chat_data.system"
style="height: 100px"
@submitDialog="submitSystemDialog"
:placeholder="$t('views.application.form.roleSettings.label')"
/>
</el-form-item>
<el-form-item
:label="$t('views.application.form.prompt.label')"
prop="prompt"
:rules="{
required: true,
message: $t('views.application.form.prompt.requiredMessage'),
trigger: 'blur',
}"
>
<template #label>
<div class="flex align-center">
<div class="mr-4">
<span
>{{ $t('views.application.form.prompt.label')
}}<span class="color-danger">*</span></span
>
</div>
<el-tooltip effect="dark" placement="right" popper-class="max-w-200">
<template #content>{{ $t('views.application.form.prompt.tooltip') }} </template>
<AppIcon iconName="app-warning" class="app-warning-icon"></AppIcon>
</el-tooltip>
</div>
</template>
<MdEditorMagnify
@wheel="wheel"
:title="$t('views.application.form.prompt.label')"
v-model="chat_data.prompt"
style="height: 150px"
@submitDialog="submitDialog"
/>
</el-form-item>
<el-form-item :label="$t('views.application.form.historyRecord.label')">
<template #label>
<div class="flex-between">
<div>{{ $t('views.application.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>
<!-- MCP-->
<div class="flex-between mb-16">
<div class="lighter">MCP</div>
<div>
<el-button
type="primary"
class="mr-4"
link
@click="openMcpServersDialog"
@refreshForm="refreshParam"
>
<AppIcon iconName="app-setting"></AppIcon>
</el-button>
<el-switch size="small" v-model="chat_data.mcp_enable" />
</div>
</div>
<div class="w-full" v-if="
(chat_data.mcp_tool_id) ||
(chat_data.mcp_servers && chat_data.mcp_servers.length > 0)"
>
<div class="flex-between border border-r-6 white-bg mb-4" style="padding: 5px 8px">
<div class="flex align-center" style="line-height: 20px">
<ToolIcon type="MCP" class="mr-8" :size="20" />
<div class="ellipsis" :title="relatedObject(toolSelectOptions, chat_data.mcp_tool_id, 'id')?.name">
{{ relatedObject(mcpToolSelectOptions, chat_data.mcp_tool_id, 'id')?.name || $t('common.custom') + ' MCP' }}
</div>
</div>
<el-button text @click="chat_data.mcp_tool_id = ''">
<el-icon><Close /></el-icon>
</el-button>
</div>
</div>
<!-- 工具 -->
<div class="flex-between mb-16">
<div class="lighter">{{ $t('views.applicationWorkflow.nodes.mcpNode.tool') }}</div>
<div>
<el-button
type="primary"
class="mr-4"
link
@click="openToolDialog"
@refreshForm="refreshParam"
>
<AppIcon iconName="app-setting"></AppIcon>
</el-button>
<el-switch size="small" v-model="chat_data.tool_enable" />
</div>
</div>
<div class="w-full" v-if="chat_data.tool_ids?.length > 0">
<template v-for="(item, index) in chat_data.tool_ids" :key="index">
<div class="flex-between border border-r-6 white-bg mb-4" style="padding: 5px 8px">
<div class="flex align-center" style="line-height: 20px">
<ToolIcon type="CUSTOM" class="mr-8" :size="20" />
<div class="ellipsis" :title="relatedObject(toolSelectOptions, item, 'id')?.name">
{{ relatedObject(toolSelectOptions, item, 'id')?.name }}
</div>
</div>
<el-button text @click="removeTool(item)">
<el-icon><Close /></el-icon>
</el-button>
</div>
</template>
</div>
<el-form-item @click.prevent>
<template #label>
<div class="flex-between w-full">
<div>
<span>{{ $t('views.application.form.reasoningContent.label') }}</span>
</div>
<el-button
type="primary"
link
@click="openReasoningParamSettingDialog"
@refreshForm="refreshParam"
>
<AppIcon iconName="app-setting"></AppIcon>
</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" />
<ToolDialog ref="toolDialogRef" @refresh="submitToolDialog" />
</NodeContainer>
</template>
<script setup lang="ts">
import { cloneDeep, set, groupBy } from 'lodash'
import NodeContainer from '@/workflow/common/NodeContainer.vue'
import type { FormInstance } from 'element-plus'
import { ref, computed, onMounted, inject } from 'vue'
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'
import { loadSharedApi } from '@/utils/dynamics-api/shared-api'
import { useRoute } from 'vue-router'
import ToolDialog from '@/views/application/component/ToolDialog.vue'
import {relatedObject} from "@/utils/array.ts";
const getApplicationDetail = inject('getApplicationDetail') as any
const route = useRoute()
const {
params: { id },
} = route as any
const apiType = computed(() => {
if (route.path.includes('resource-management')) {
return 'systemManage'
} else {
return 'workspace'
}
})
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 defaultPrompt = `${t('views.applicationWorkflow.nodes.aiChatNode.defaultPrompt')}
{{${t('views.applicationWorkflow.nodes.searchKnowledgeNode.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: true,
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 })
})
}
const application = getApplicationDetail()
function getSelectModel() {
const obj =
apiType.value === 'systemManage'
? {
model_type: 'LLM',
workspace_id: application.value?.workspace_id,
}
: {
model_type: 'LLM',
}
loadSharedApi({ type: 'model', systemType: apiType.value })
.getSelectModelList(obj)
.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_tool_id: chat_data.value.mcp_tool_id,
mcp_source: chat_data.value.mcp_source,
}
mcpServersDialogRef.value.open(config, mcpToolSelectOptions.value)
}
function submitMcpServersDialog(config: any) {
set(props.nodeModel.properties.node_data, 'mcp_servers', config.mcp_servers)
set(props.nodeModel.properties.node_data, 'mcp_tool_id', config.mcp_tool_id)
set(props.nodeModel.properties.node_data, 'mcp_source', config.mcp_source)
}
const toolDialogRef = ref()
function openToolDialog() {
toolDialogRef.value.open(chat_data.value.tool_ids)
}
function submitToolDialog(config: any) {
set(props.nodeModel.properties.node_data, 'tool_ids', config.tool_ids)
}
function removeTool(id: any) {
const list = props.nodeModel.properties.node_data.tool_ids.filter((v: any) => v !== id)
set(props.nodeModel.properties.node_data, 'tool_ids', list)
}
const toolSelectOptions = ref<any[]>([])
function getToolSelectOptions() {
const obj =
apiType.value === 'systemManage'
? {
scope: 'WORKSPACE',
tool_type: 'CUSTOM',
workspace_id: application.value?.workspace_id,
}
: {
scope: 'WORKSPACE',
tool_type: 'CUSTOM',
}
loadSharedApi({ type: 'tool', systemType: apiType.value })
.getAllToolList(obj)
.then((res: any) => {
toolSelectOptions.value = [...res.data.shared_tools, ...res.data.tools].filter(
(item: any) => item.is_active,
)
})
}
const mcpToolSelectOptions = ref<any[]>([])
function getMcpToolSelectOptions() {
const obj =
apiType.value === 'systemManage'
? {
scope: 'WORKSPACE',
tool_type: 'MCP',
workspace_id: application.value?.workspace_id,
}
: {
scope: 'WORKSPACE',
tool_type: 'MCP',
}
loadSharedApi({ type: 'tool', systemType: apiType.value })
.getAllToolList(obj)
.then((res: any) => {
mcpToolSelectOptions.value = [...res.data.shared_tools, ...res.data.tools].filter(
(item: any) => item.is_active,
)
})
}
onMounted(() => {
getSelectModel()
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'
}
getToolSelectOptions()
getMcpToolSelectOptions()
})
</script>
<style lang="scss" scoped></style>