Compare commits

...

21 Commits
main ... v1.7.2

Author SHA1 Message Date
wangdan-fit2cloud 906ec28f1f fix: 统一头像尺寸 2024-11-14 15:26:49 +08:00
wangdan-fit2cloud 5f02b56006 fix: 修改AI问答头像问题 2024-11-14 14:27:48 +08:00
liqiang-fit2cloud ef2bcdcd17 fix: 修复swagger接口文档中schema不正确的问题
(cherry picked from commit 6f84239e8b)
2024-11-14 13:15:51 +08:00
wangdan-fit2cloud 3cd7b29611 fix: 【应用】简单配置中对过长名称知识库进行关联时有概率导致无法显示知识库全部名称(#1596) 2024-11-14 12:07:40 +08:00
wangdan-fit2cloud 20fa075c1b fix: 修复AI回复头像尺寸(#1592) 2024-11-14 11:59:10 +08:00
shaohuzhang1 33fe7ab8e6 fix: 修复对话时,模型参数删除后依然进行校验
(cherry picked from commit a0cfcb73a9)
2024-11-14 11:48:51 +08:00
shaohuzhang1 328b9a095a fix: 修复工作流节点收缩后无法发布 #1582
(cherry picked from commit 8cf23fe990)
2024-11-14 11:48:44 +08:00
shaohuzhang1 7adfdf73d9 feat: 去除工作流直接返回内容的标题数据
Some checks failed
sync2gitee / repo-sync (push) Has been cancelled
2024-11-07 17:54:43 +08:00
wxg0103 22cf23e593 refactor: 优化文案
(cherry picked from commit 7bf7df5e27)
2024-11-07 16:07:10 +08:00
shaohuzhang1 a4b4db70bd fix: 添加对话stream流式结束标记 2024-11-07 16:06:52 +08:00
shaohuzhang1 a529bc3510 fix: 修复函数库,非必填引用类型,不传值 执行报错 #1564
(cherry picked from commit 739a20bc38)
2024-11-07 16:06:45 +08:00
shaohuzhang1 766f9fa745 fix: 修复对话中表格中图片时出现错位 #1561
(cherry picked from commit 8a0c204961)
2024-11-07 16:06:41 +08:00
wxg0103 cf66e33c58 fix: 修复xinference向量模型添加失败的缺陷
(cherry picked from commit fc015d17bc)
2024-11-07 16:06:29 +08:00
wangdan-fit2cloud cb83ed8688 fix: 【团队成员】修复全选的问题 (#1558)
(cherry picked from commit f30d3d736b)
2024-11-07 16:06:22 +08:00
shaohuzhang1 4229192fc1 feat: 工作流编排应用记住节点的收起/展开状态 #1543
(cherry picked from commit b2ac658c4f)
2024-11-07 16:06:18 +08:00
shaohuzhang1 4076988374 fix: 修复旧word文档图片无法正常识别 #1533
(cherry picked from commit 22d9fdc42f)
2024-11-07 16:06:15 +08:00
shaohuzhang1 e3de5e7a26 fix: 修复工作流节点未输出数据无法获取执行详情问题 #1550
(cherry picked from commit af509a9f58)
2024-11-07 16:06:10 +08:00
shaohuzhang1 d6c86d96ee fix: 修复工作流知识库检索结果标题不存在的时候显示错误 #1535
(cherry picked from commit 6a226c9539)
2024-11-07 16:06:03 +08:00
shaohuzhang1 84b0998055 fix: 修复应用发布后无法使用最新的应用设置
(cherry picked from commit 5285745321)
2024-11-07 16:05:56 +08:00
wxg0103 110d8fd398 refactor: 修改依赖包的方法
(cherry picked from commit 3cee9bc495)
2024-11-07 16:03:49 +08:00
wxg0103 f5af097e6d style: 修复样式问题
(cherry picked from commit c0dd4e8034)
2024-11-07 16:03:36 +08:00
38 changed files with 324 additions and 734 deletions

View File

@ -62,6 +62,8 @@ def valid_reference_value(_type, value, name):
def convert_value(name: str, value, _type, is_required, source, node):
if not is_required and value is None:
return None
if not is_required and source == 'reference' and (value is None or len(value) == 0):
return None
if source == 'reference':
value = node.workflow_manage.get_reference_field(
value[0],

View File

@ -37,6 +37,13 @@ def get_none_result(question):
'directly_return': ''}, {})
def reset_title(title):
if title is None or len(title.strip()) == 0:
return ""
else:
return f"#### {title}\n"
class BaseSearchDatasetNode(ISearchDatasetStepNode):
def execute(self, dataset_id_list, dataset_setting, question,
exclude_paragraph_id_list=None,
@ -63,10 +70,11 @@ class BaseSearchDatasetNode(ISearchDatasetStepNode):
return NodeResult({'paragraph_list': result,
'is_hit_handling_method_list': [row for row in result if row.get('is_hit_handling_method')],
'data': '\n'.join(
[f"{paragraph.get('title', '')}:{paragraph.get('content')}" for paragraph in
[f"{reset_title(paragraph.get('title', ''))}{paragraph.get('content')}" for paragraph in
paragraph_list])[0:dataset_setting.get('max_paragraph_char_number', 5000)],
'directly_return': '\n'.join(
[f"{paragraph.get('title', '')}:{paragraph.get('content')}" for paragraph in result if
[paragraph.get('content') for paragraph in
result if
paragraph.get('is_hit_handling_method')]),
'question': question},

View File

@ -144,7 +144,6 @@ class Flow:
if model_params_setting is None:
model_params_setting = model_params_setting_form.get_default_form_data()
node.properties.get('node_data', {})['model_params_setting'] = model_params_setting
model_params_setting_form.valid_form(model_params_setting)
if node.properties.get('status', 200) != 200:
raise ValidationError(ErrorDetail(f'节点{node.properties.get("stepName")} 不可用'))
node_list = [node for node in self.nodes if (node.type == 'function-lib-node')]
@ -308,6 +307,7 @@ class WorkflowManage:
self.work_flow_post_handler.handler(self.params['chat_id'], self.params['chat_record_id'],
self.answer,
self)
yield self.get_chunk_content('', True)
def run_chain_async(self, current_node):
future = executor.submit(self.run_chain, current_node)

View File

@ -831,8 +831,6 @@ class ApplicationSerializer(serializers.Serializer):
ApplicationSerializer.Edit(data=instance).is_valid(
raise_exception=True)
application_id = self.data.get("application_id")
valid_model_params_setting(instance.get('model_id'),
instance.get('model_params_setting'))
application = QuerySet(Application).get(id=application_id)
if instance.get('model_id') is None or len(instance.get('model_id')) == 0:

View File

@ -294,7 +294,6 @@ class ChatSerializers(serializers.Serializer):
return chat_id
def open_simple(self, application):
valid_model_params_setting(application.model_id, application.model_params_setting)
application_id = self.data.get('application_id')
dataset_id_list = [str(row.dataset_id) for row in
QuerySet(ApplicationDatasetMapping).filter(
@ -376,7 +375,6 @@ class ChatSerializers(serializers.Serializer):
model_id = self.data.get('model_id')
dataset_id_list = self.data.get('dataset_id_list')
dialogue_number = 3 if self.data.get('multiple_rounds_dialogue', False) else 0
valid_model_params_setting(model_id, self.data.get('model_params_setting'))
application = Application(id=None, dialogue_number=dialogue_number, model_id=model_id,
dataset_setting=self.data.get('dataset_setting'),
model_setting=self.data.get('model_setting'),

View File

@ -79,7 +79,7 @@ class FileCache(BaseCache):
value.application.id) == application_id):
delete_keys.append(key)
for key in delete_keys:
self.delete(key)
self.cache.delete(key)
def clear_timeout_data(self):
for key in self.cache.iterkeys():

View File

@ -20,3 +20,10 @@ class CustomSwaggerAutoSchema(SwaggerAutoSchema):
if "api" in tags and operation_keys:
return [tags_dict.get(operation_keys[1]) if operation_keys[1] in tags_dict else operation_keys[1]]
return tags
def get_schema(self, request=None, public=False):
schema = super().get_schema(request, public)
if request.is_secure():
schema.schemes = ['https']
else:
schema.schemes = ['http']
return schema

View File

@ -14,9 +14,9 @@ from functools import reduce
from typing import List
from docx import Document, ImagePart
from docx.oxml import ns
from docx.table import Table
from docx.text.paragraph import Paragraph
from docx.oxml import ns
from common.handle.base_split_handle import BaseSplitHandle
from common.util.split_model import SplitModel
@ -33,11 +33,8 @@ old_docx_nsmap = {'v': 'urn:schemas-microsoft-com:vml'}
combine_nsmap = {**ns.nsmap, **old_docx_nsmap}
def image_to_mode(image, doc: Document, images_list, get_image_id, is_new_docx=True):
if is_new_docx:
image_ids = image.xpath('.//a:blip/@r:embed')
else:
image_ids = image.xpath('.//v:imagedata/@r:id', namespaces=combine_nsmap)
def image_to_mode(image, doc: Document, images_list, get_image_id):
image_ids = image['get_image_id_handle'](image.get('image'))
for img_id in image_ids: # 获取图片id
part = doc.part.related_parts[img_id] # 根据图片id获取对应的图片
if isinstance(part, ImagePart):
@ -49,14 +46,15 @@ def image_to_mode(image, doc: Document, images_list, get_image_id, is_new_docx=T
def get_paragraph_element_images(paragraph_element, doc: Document, images_list, get_image_id):
images_xpath_list = [".//pic:pic", ".//w:pict"]
images_xpath_list = [(".//pic:pic", lambda img: img.xpath('.//a:blip/@r:embed')),
(".//w:pict", lambda img: img.xpath('.//v:imagedata/@r:id', namespaces=combine_nsmap))]
images = []
for images_xpath in images_xpath_list:
for images_xpath, get_image_id_handle in images_xpath_list:
try:
_images = paragraph_element.xpath(images_xpath)
if _images is not None and len(_images) > 0:
for image in _images:
images.append(image)
images.append({'image': image, 'get_image_id_handle': get_image_id_handle})
except Exception as e:
pass
return images

View File

@ -9,7 +9,7 @@
from typing import List, Optional, Any, Iterator, Dict
from langchain_community.chat_models.sparkllm import \
ChatSparkLLM, _convert_message_to_dict, _convert_delta_to_message_chunk
ChatSparkLLM, convert_message_to_dict, _convert_delta_to_message_chunk
from langchain_core.callbacks import CallbackManagerForLLMRun
from langchain_core.messages import BaseMessage, AIMessageChunk
from langchain_core.outputs import ChatGenerationChunk
@ -56,7 +56,7 @@ class XFChatSparkLLM(MaxKBBaseModel, ChatSparkLLM):
default_chunk_class = AIMessageChunk
self.client.arun(
[_convert_message_to_dict(m) for m in messages],
[convert_message_to_dict(m) for m in messages],
self.spark_user_id,
self.model_kwargs,
True,

View File

@ -15,7 +15,8 @@ class XinferenceEmbeddingModelCredential(BaseForm, BaseModelCredential):
if not any(list(filter(lambda mt: mt.get('value') == model_type, model_type_list))):
raise AppApiException(ValidCode.valid_error.value, f'{model_type} 模型类型不支持')
try:
model_list = provider.get_base_model_list(model_credential.get('api_base'), 'embedding')
model_list = provider.get_base_model_list(model_credential.get('api_base'), model_credential.get('api_key'),
'embedding')
except Exception as e:
raise AppApiException(ValidCode.valid_error.value, "API 域名无效")
exist = provider.get_model_info_by_name(model_list, model_name)
@ -36,3 +37,4 @@ class XinferenceEmbeddingModelCredential(BaseForm, BaseModelCredential):
return self
api_base = forms.TextInputField('API 域名', required=True)
api_key = forms.PasswordInputField('API Key', required=True)

View File

@ -1,18 +1,26 @@
# coding=utf-8
import threading
from typing import Dict
from typing import Dict, Optional, List, Any
from langchain_community.embeddings import XinferenceEmbeddings
from langchain_core.embeddings import Embeddings
from setting.models_provider.base_model_provider import MaxKBBaseModel
class XinferenceEmbedding(MaxKBBaseModel, XinferenceEmbeddings):
class XinferenceEmbedding(MaxKBBaseModel, Embeddings):
client: Any
server_url: Optional[str]
"""URL of the xinference server"""
model_uid: Optional[str]
"""UID of the launched model"""
@staticmethod
def new_instance(model_type, model_name, model_credential: Dict[str, object], **model_kwargs):
return XinferenceEmbedding(
model_uid=model_name,
server_url=model_credential.get('api_base'),
api_key=model_credential.get('api_key'),
)
def down_model(self):
@ -22,3 +30,63 @@ class XinferenceEmbedding(MaxKBBaseModel, XinferenceEmbeddings):
thread = threading.Thread(target=self.down_model)
thread.daemon = True
thread.start()
def __init__(
self, server_url: Optional[str] = None, model_uid: Optional[str] = None,
api_key: Optional[str] = None
):
try:
from xinference.client import RESTfulClient
except ImportError:
try:
from xinference_client import RESTfulClient
except ImportError as e:
raise ImportError(
"Could not import RESTfulClient from xinference. Please install it"
" with `pip install xinference` or `pip install xinference_client`."
) from e
if server_url is None:
raise ValueError("Please provide server URL")
if model_uid is None:
raise ValueError("Please provide the model UID")
self.server_url = server_url
self.model_uid = model_uid
self.api_key = api_key
self.client = RESTfulClient(server_url, api_key)
def embed_documents(self, texts: List[str]) -> List[List[float]]:
"""Embed a list of documents using Xinference.
Args:
texts: The list of texts to embed.
Returns:
List of embeddings, one for each text.
"""
model = self.client.get_model(self.model_uid)
embeddings = [
model.create_embedding(text)["data"][0]["embedding"] for text in texts
]
return [list(map(float, e)) for e in embeddings]
def embed_query(self, text: str) -> List[float]:
"""Embed a query of documents using Xinference.
Args:
text: The text to embed.
Returns:
Embeddings for the text.
"""
model = self.client.get_model(self.model_uid)
embedding_res = model.create_embedding(text)
embedding = embedding_res["data"][0]["embedding"]
return list(map(float, embedding))

View File

@ -26,6 +26,8 @@ DATABASES = {
'default': CONFIG.get_db_setting()
}
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
# Application definition
INSTALLED_APPS = [

View File

@ -12,7 +12,7 @@ djangorestframework = "^3.15.2"
drf-yasg = "1.21.7"
django-filter = "23.2"
langchain = "0.2.16"
langchain_community = "0.2.4"
langchain_community = "0.2.17"
langchain-huggingface = "^0.0.3"
psycopg2-binary = "2.9.7"
jieba = "^0.42.1"

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View File

@ -40,8 +40,14 @@
<div ref="dialogScrollbar" class="ai-chat__content p-24 chat-width">
<div class="item-content mb-16" v-if="!props.available || (props.data?.prologue && !log)">
<div class="avatar">
<img v-if="data.avatar" :src="data.avatar" height="30px" />
<LogoIcon v-else height="30px" />
<el-image
v-if="data.avatar"
:src="data.avatar"
alt=""
fit="cover"
style="width: 32px; height: 32px; display: block"
/>
<LogoIcon v-else height="32px" width="32px" />
</div>
<div class="content">
@ -96,8 +102,14 @@
<!-- 回答 -->
<div class="item-content mb-16 lighter">
<div class="avatar">
<img v-if="data.avatar" :src="data.avatar" height="30px" />
<LogoIcon v-else height="30px" />
<el-image
v-if="data.avatar"
:src="data.avatar"
alt=""
fit="cover"
style="width: 30px; height: 30px; display: block"
/>
<LogoIcon v-else height="32px" width="32px" />
</div>
<div class="content">

View File

@ -12,7 +12,7 @@
</div>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
import { ref, computed, watch } from 'vue'
defineOptions({ name: 'InfiniteScroll' })
const props = defineProps({
@ -47,6 +47,14 @@ const props = defineProps({
const emit = defineEmits(['update:current_page', 'load'])
const current = ref(props.current_page)
watch(
() => props.current_page,
(val) => {
if (val === 1) {
current.value = 1
}
}
)
const noMore = computed(
() =>
props.size > 0 && props.size === props.total && props.total > props.page_size && !props.loading

View File

@ -32,6 +32,17 @@ import HtmlRander from './HtmlRander.vue'
import EchartsRander from './EchartsRander.vue'
config({
markdownItConfig(md) {
md.renderer.rules.image = (tokens, idx, options, env, self) => {
tokens[idx].attrSet('style', 'display:inline-block;min-height:33px;padding:0;margin:0')
if (tokens[idx].content) {
tokens[idx].attrSet('title', tokens[idx].content)
}
tokens[idx].attrSet(
'onerror',
'this.src="/ui/assets/load_error.png";this.onerror=null;this.height="33px"'
)
return md.renderer.renderToken(tokens, idx, options)
}
md.renderer.rules.link_open = (tokens, idx, options, env, self) => {
tokens[idx].attrSet('target', '_blank')
return md.renderer.renderToken(tokens, idx, options)
@ -52,51 +63,9 @@ const props = withDefaults(
const editorRef = ref()
const md_view_list = computed(() => {
const temp_source = props.source
const temp_md_img_list = temp_source.match(/(!\[.*?\]\(img\/.*?\){.*?})|(!\[.*?\]\(img\/.*?\))/g)
const md_img_list = temp_md_img_list ? temp_md_img_list.filter((i) => i) : []
const split_img_value = temp_source
.split(/(!\[.*?\]\(img\/.*?\){.*?})|(!\[.*?\]\(img\/.*?\))/g)
.filter((item) => item !== undefined)
.filter((item) => !md_img_list?.includes(item))
const result = Array.from(
{ length: md_img_list.length + split_img_value.length },
(v, i) => i
).map((index) => {
if (index % 2 == 0) {
return split_img_value[Math.floor(index / 2)]
} else {
return md_img_list[Math.floor(index / 2)]
}
})
return split_echarts_rander(split_html_rander(split_quick_question(split_md_img(result))))
return split_echarts_rander(split_html_rander(split_quick_question([temp_source])))
})
const split_md_img = (result: Array<string>) => {
return result
.map((item) => split_md_img_(item))
.reduce((x: any, y: any) => {
return [...x, ...y]
}, [])
}
const split_md_img_ = (source: string) => {
const temp_md_img_list = source.match(/(!\[.*?\]\(.*?\){.*?})|(!\[.*?\]\(.*?\))/g)
const md_img_list = temp_md_img_list ? temp_md_img_list.filter((i) => i) : []
const split_img_value = source
.split(/(!\[.*?\]\(.*?\){.*?})|(!\[.*?\]\(.*?\))/g)
.filter((item) => item !== undefined)
.filter((item) => !md_img_list?.includes(item))
const result = Array.from(
{ length: md_img_list.length + split_img_value.length },
(v, i) => i
).map((index) => {
if (index % 2 == 0) {
return split_img_value[Math.floor(index / 2)]
} else {
return md_img_list[Math.floor(index / 2)]
}
})
return result
}
const split_quick_question = (result: Array<string>) => {
return result
.map((item) => split_quick_question_(item))

View File

@ -12,13 +12,6 @@ const applicationRouter = {
name: 'application',
component: () => import('@/views/application/index.vue')
},
{
path: '/application/create',
name: 'CreateApplication',
meta: { activeMenu: '/application' },
component: () => import('@/views/application/CreateAndSetting.vue'),
hidden: true
},
{
path: '/application/:id/:type',
name: 'ApplicationDetail',

View File

@ -12,7 +12,7 @@ const datasetRouter = {
component: () => import('@/views/dataset/index.vue')
},
{
path: '/dataset/:type', // create 或者 upload
path: '/dataset/:type', // upload
name: 'UploadDocumentDataset',
meta: { activeMenu: '/dataset' },
component: () => import('@/views/dataset/UploadDocumentDataset.vue'),

View File

@ -36,7 +36,12 @@
:size="32"
style="background: none"
>
<img :src="detail?.icon" alt="" />
<el-image
:src="detail?.icon"
alt=""
fit="cover"
style="width: 32px; height: 32px; display: block"
/>
</AppAvatar>
<AppAvatar
v-else-if="detail?.name"
@ -178,7 +183,7 @@
</el-upload>
</div>
<el-text type="info" size="small"
>建议尺寸 64*64支持 JPGPNGGIF大小不超过 10 MB</el-text
>建议尺寸 32*32支持 JPGPNGGIF大小不超过 10 MB</el-text
>
</el-card>
<el-card shadow="never" class="mb-8">
@ -197,7 +202,7 @@
</el-upload>
</div>
<el-text type="info" size="small">
建议尺寸 64*64支持 JPGPNGGIF大小不超过 10 MB</el-text
建议尺寸 32*32支持 JPGPNGGIF大小不超过 10 MB</el-text
>
</el-card>
<el-card shadow="never" class="mb-8">
@ -215,7 +220,7 @@
</el-upload>
</div>
<el-text type="info" size="small">
建议尺寸 64*64支持 JPGPNGGIF大小不超过 10 MB
建议尺寸 32*32支持 JPGPNGGIF大小不超过 10 MB
</el-text>
<div class="border-t mt-8">
<div class="flex-between mb-8">

View File

@ -18,7 +18,12 @@
:size="32"
style="background: none"
>
<img :src="detail?.icon" alt="" />
<el-image
:src="detail?.icon"
alt=""
fit="cover"
style="width: 32px; height: 32px; display: block"
/>
</AppAvatar>
<AppAvatar
v-else-if="detail?.name"
@ -353,23 +358,22 @@ function getDetail() {
?.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.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
}
})
: []
? v.properties.input_field_list
.filter((v: any) => v.assignment_method === 'api_input')
.map((v: any) => {
return {
name: v.variable,
value: v.default_value
}
})
: []
})
})
}

View File

@ -90,7 +90,12 @@
:size="32"
style="background: none"
>
<img :src="detail?.icon" alt="" />
<el-image
:src="detail?.icon"
alt=""
fit="cover"
style="width: 32px; height: 32px; display: block"
/>
</AppAvatar>
<AppAvatar
v-else-if="detail?.name"

View File

@ -252,12 +252,12 @@
<AppAvatar v-else class="mr-8 avatar-blue" shape="square" :size="32">
<img src="@/assets/icon_document.svg" style="width: 58%" alt="" />
</AppAvatar>
<auto-tooltip
:content="relatedObject(datasetList, item, 'id')?.name"
style="width: 80%"
<span
class="ellipsis cursor"
:title="relatedObject(datasetList, item, 'id')?.name"
>
{{ relatedObject(datasetList, item, 'id')?.name }}</span
>
{{ relatedObject(datasetList, item, 'id')?.name }}
</auto-tooltip>
</div>
<el-button text @click="removeDataset(item)">
<el-icon>
@ -326,7 +326,11 @@
<AppIcon iconName="app-warning" class="app-warning-icon"></AppIcon>
</el-tooltip> -->
</div>
<el-switch size="small" v-model="applicationForm.stt_model_enable" @change="sttModelEnableChange"/>
<el-switch
size="small"
v-model="applicationForm.stt_model_enable"
@change="sttModelEnableChange"
/>
</div>
</template>
<el-select
@ -406,7 +410,11 @@
<el-icon class="mr-4"><Setting /></el-icon>
设置
</el-button>
<el-switch size="small" v-model="applicationForm.tts_model_enable" @change="ttsModelEnableChange"/>
<el-switch
size="small"
v-model="applicationForm.tts_model_enable"
@change="ttsModelEnableChange"
/>
</div>
</div>
</template>
@ -501,7 +509,11 @@
:size="32"
style="background: none"
>
<img :src="applicationForm?.icon" alt="" />
<img
:src="applicationForm?.icon"
alt=""
style="width: 32px; height: 32px; display: block"
/>
</AppAvatar>
<AppAvatar
v-else-if="applicationForm?.name"

View File

@ -1,546 +0,0 @@
<template>
<LayoutContainer
:header="
id
? $t('views.application.applicationForm.title.edit')
: $t('views.application.applicationForm.title.create')
"
:back-to="id ? '' : '-1'"
class="create-application"
>
<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.application.applicationForm.title.info') }}
</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
/>
</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')"
prop="model_id"
>
<template #label>
<div class="flex-between">
<span>{{ $t('views.application.applicationForm.form.aiModel.label') }}</span>
</div>
</template>
<el-select
v-model="applicationForm.model_id"
:placeholder="$t('views.application.applicationForm.form.aiModel.placeholder')"
class="w-full"
popper-class="select-model"
:clearable="true"
>
<el-option-group
v-for="(value, label) in modelOptions"
:key="value"
:label="relatedObject(providerOptions, label, 'provider')?.name"
>
<el-option
v-for="item in value.filter((v: any) => v.status === 'SUCCESS')"
:key="item.id"
:label="item.name"
:value="item.id"
class="flex-between"
>
<div class="flex">
<span
v-html="relatedObject(providerOptions, label, 'provider')?.icon"
class="model-icon mr-8"
></span>
<span>{{ item.name }}</span>
</div>
<el-icon class="check-icon" v-if="item.id === applicationForm.model_id"
><Check
/></el-icon>
</el-option>
<!-- 不可用 -->
<el-option
v-for="item in value.filter((v: any) => v.status !== 'SUCCESS')"
:key="item.id"
:label="item.name"
:value="item.id"
class="flex-between"
disabled
>
<div class="flex">
<span
v-html="relatedObject(providerOptions, label, 'provider')?.icon"
class="model-icon mr-8"
></span>
<span>{{ item.name }}</span>
<span class="danger">{{
$t('views.application.applicationForm.form.aiModel.unavailable')
}}</span>
</div>
<el-icon class="check-icon" v-if="item.id === applicationForm.model_id"
><Check
/></el-icon>
</el-option>
</el-option-group>
<template #footer>
<div class="w-full text-left cursor" @click="openCreateModel()">
<el-button type="primary" link>
<el-icon class="mr-4"><Plus /></el-icon>
{{ $t('views.application.applicationForm.form.addModel') }}
</el-button>
</div>
</template>
</el-select>
</el-form-item>
<el-form-item
:label="$t('views.application.applicationForm.form.prompt.label')"
prop="model_setting.prompt"
>
<template #label>
<div class="flex align-center">
<div class="flex-between mr-4">
<span
>{{ $t('views.application.applicationForm.form.prompt.label') }}
<span class="danger">*</span></span
>
</div>
<el-tooltip effect="dark" placement="right">
<template #content>{{
$t('views.application.applicationForm.form.prompt.tooltip', {
data: '{data}',
question: '{question}'
})
}}</template>
<AppIcon iconName="app-warning" class="app-warning-icon"></AppIcon>
</el-tooltip>
</div>
</template>
<el-input
v-model="applicationForm.model_setting.prompt"
:rows="6"
type="textarea"
maxlength="2048"
:placeholder="defaultPrompt"
/>
</el-form-item>
<el-form-item label="历史聊天记录" @click.prevent>
<el-input-number
v-model="applicationForm.dialogue_number"
:min="0"
:value-on-clear="0"
controls-position="right"
class="w-full"
/>
</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.relatedKnowledgeBase')
}}</span>
<div>
<el-button type="primary" link @click="openParamSettingDialog">
<AppIcon iconName="app-operation" class="mr-4"></AppIcon
>{{ $t('views.application.applicationForm.form.paramSetting') }}
</el-button>
<el-button type="primary" link @click="openDatasetDialog">
<el-icon class="mr-4"><Plus /></el-icon
>{{ $t('views.application.applicationForm.form.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.relatedKnowledgeBaseWhere')
}}</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">
<AppAvatar
v-if="relatedObject(datasetList, item, 'id')?.type === '1'"
class="mr-8 avatar-purple"
shape="square"
:size="32"
>
<img src="@/assets/icon_web.svg" style="width: 58%" alt="" />
</AppAvatar>
<AppAvatar v-else class="mr-8 avatar-blue" shape="square" :size="32">
<img src="@/assets/icon_document.svg" style="width: 58%" alt="" />
</AppAvatar>
<div class="ellipsis">
{{ relatedObject(datasetList, item, 'id')?.name }}
</div>
</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.prologue')">
<MdEditor
class="prologue-md-editor"
v-model="applicationForm.prologue"
:preview="false"
:toolbars="[]"
:footers="[]"
/>
</el-form-item>
<el-form-item @click.prevent>
<template #label>
<div class="flex align-center">
<span class="mr-4">{{
$t('views.application.applicationForm.form.problemOptimization.label')
}}</span>
<el-tooltip
effect="dark"
:content="
$t('views.application.applicationForm.form.problemOptimization.tooltip')
"
placement="right"
>
<AppIcon iconName="app-warning" class="app-warning-icon"></AppIcon>
</el-tooltip>
</div>
</template>
<el-switch size="small" v-model="applicationForm.problem_optimization"></el-switch>
</el-form-item>
</el-form>
</el-scrollbar>
</div>
<div class="text-right border-t p-16">
<el-button v-if="!id" @click="router.push({ path: `/application` })"
>{{ $t('views.application.applicationForm.buttons.cancel') }}
</el-button>
<el-button type="primary" @click="submit(applicationFormRef)" :disabled="loading">
{{
id
? $t('views.application.applicationForm.buttons.save')
: $t('views.application.applicationForm.buttons.create')
}}
</el-button>
</div>
</el-col>
<el-col :span="14" class="p-24 border-l">
<h4 class="title-decoration-1 mb-16">
{{ $t('views.application.applicationForm.form.apptest') }}
</h4>
<div class="dialog-bg">
<h4 class="p-24">
{{
applicationForm?.name || $t('views.application.applicationForm.form.appName.label')
}}
</h4>
<div class="scrollbar-height">
<AiChat :data="applicationForm"></AiChat>
</div>
</div>
</el-col>
</el-row>
<ParamSettingDialog ref="ParamSettingDialogRef" @refresh="refreshParam" />
<AddDatasetDialog
ref="AddDatasetDialogRef"
@addData="addDataset"
:data="datasetList"
@refresh="refresh"
:loading="datasetLoading"
/>
<!-- 添加模版 -->
<CreateModelDialog
ref="createModelRef"
@submit="getModel"
@change="openCreateModel($event)"
></CreateModelDialog>
<SelectProviderDialog ref="selectProviderRef" @change="openCreateModel($event)" />
</LayoutContainer>
</template>
<script setup lang="ts">
import { reactive, ref, watch, onMounted } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { groupBy } from 'lodash'
import ParamSettingDialog from './component/ParamSettingDialog.vue'
import AddDatasetDialog from './component/AddDatasetDialog.vue'
import CreateModelDialog from '@/views/template/component/CreateModelDialog.vue'
import SelectProviderDialog from '@/views/template/component/SelectProviderDialog.vue'
import applicationApi from '@/api/application'
import type { FormInstance, FormRules } from 'element-plus'
import type { ApplicationFormType } from '@/api/type/application'
import type { Provider } from '@/api/type/model'
import { relatedObject } from '@/utils/utils'
import { MsgSuccess } from '@/utils/message'
import useStore from '@/stores'
import { t } from '@/locales'
const { model, dataset, application, user } = useStore()
const router = useRouter()
const route = useRoute()
const {
params: { id }
} = route as any
// @ts-ignore
const defaultPrompt = t('views.application.prompt.defaultPrompt', {
data: '{data}',
question: '{question}'
})
const ParamSettingDialogRef = ref<InstanceType<typeof ParamSettingDialog>>()
const createModelRef = ref<InstanceType<typeof CreateModelDialog>>()
const selectProviderRef = ref<InstanceType<typeof SelectProviderDialog>>()
const applicationFormRef = ref<FormInstance>()
const AddDatasetDialogRef = ref()
const loading = ref(false)
const datasetLoading = ref(false)
const applicationForm = ref<ApplicationFormType>({
name: '',
desc: '',
model_id: '',
dialogue_number: 0,
prologue: t('views.application.prompt.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'
}
],
model_id: [
{
required: false,
message: t('views.application.applicationForm.form.aiModel.placeholder'),
trigger: 'change'
}
],
'model_setting.prompt': [
{
required: true,
message: t('views.application.applicationForm.form.prompt.placeholder'),
trigger: 'blur'
}
]
})
const modelOptions = ref<any>(null)
const providerOptions = ref<Array<Provider>>([])
const datasetList = ref([])
const submit = async (formEl: FormInstance | undefined) => {
if (!formEl) return
await formEl.validate((valid, fields) => {
if (valid) {
if (id) {
application.asyncPutApplication(id, applicationForm.value, loading).then((res) => {
MsgSuccess(t('views.application.applicationForm.buttons.saveSuccess'))
})
} else {
applicationApi.postApplication(applicationForm.value, loading).then((res) => {
MsgSuccess(t('views.application.applicationForm.buttons.createSuccess'))
router.push({ path: `/application` })
})
}
}
})
}
const openParamSettingDialog = () => {
ParamSettingDialogRef.value?.open(applicationForm.value.dataset_setting)
}
function refreshParam(data: any) {
applicationForm.value.dataset_setting = data
}
const openCreateModel = (provider?: Provider) => {
if (provider && provider.provider) {
createModelRef.value?.open(provider)
} else {
selectProviderRef.value?.open()
}
}
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
})
}
function getDataset() {
if (id) {
application.asyncGetApplicationDataset(id, datasetLoading).then((res: any) => {
datasetList.value = res.data
})
} else {
dataset.asyncGetAllDataset(datasetLoading).then((res: any) => {
datasetList.value = res.data?.filter((v: any) => v.user_id === user.userInfo?.id)
})
}
}
function getModel() {
loading.value = true
if (id) {
applicationApi
.getApplicationModel(id)
.then((res: any) => {
modelOptions.value = groupBy(res?.data, 'provider')
loading.value = false
})
.catch(() => {
loading.value = false
})
} else {
model
.asyncGetModel()
.then((res: any) => {
modelOptions.value = groupBy(res?.data, 'provider')
loading.value = false
})
.catch(() => {
loading.value = false
})
}
}
function getProvider() {
loading.value = true
model
.asyncGetProvider()
.then((res: any) => {
providerOptions.value = res?.data
loading.value = false
})
.catch(() => {
loading.value = false
})
}
function refresh() {
getDataset()
}
onMounted(() => {
getProvider()
getModel()
getDataset()
if (id) {
getDetail()
}
})
</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) - 127px);
}
.scrollbar-height {
height: calc(var(--app-main-height) - 150px);
}
}
.prologue-md-editor {
height: 150px;
}
</style>

View File

@ -28,8 +28,8 @@
</el-input>
</el-form-item>
</template>
<div v-if="configType === 'wechat'" class="flex align-center" style="margin-bottom: 8px">
<span class="el-form-item__label">认证通过</span>
<div v-if="configType === 'wechat'" class="flex align-center mb-16">
<span class="lighter mr-8">认证通过</span>
<el-switch v-if="configType === 'wechat'" v-model="form[configType].is_certification" />
</div>

View File

@ -40,9 +40,7 @@
<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">
<auto-tooltip :content="item.name" style="max-width: 170px">
{{ item.name }}
</auto-tooltip>
<span class="ellipsis cursor" :title="item.name"> {{ item.name }}</span>
</CardCheckbox>
</el-col>
</el-row>

View File

@ -51,7 +51,12 @@
style="background: none"
class="mr-8"
>
<img :src="item?.icon" alt="" />
<el-image
:src="item?.icon"
alt=""
fit="cover"
style="width: 32px; height: 32px; display: block"
/>
</AppAvatar>
<AppAvatar
v-else-if="item?.name"
@ -175,7 +180,7 @@ function openCreateDialog() {
} else {
MsgConfirm(`提示`, '社区版最多支持 5 个应用,如需拥有更多应用,请升级为专业版。', {
cancelButtonText: '确定',
confirmButtonText: '购买专业版',
confirmButtonText: '购买专业版'
})
.then(() => {
window.open('https://maxkb.cn/pricing.html', '_blank')

View File

@ -80,7 +80,7 @@ const currentPlatform = reactive<Platform>({
const formatFieldName = (key?: any): string => {
const fieldNames: { [key: string]: string } = {
corp_id: 'Corp ID',
app_key: 'APP Key',
app_key: currentPlatform?.key != 'lark' ? 'APP Key' : 'App ID',
app_secret: 'APP Secret',
agent_id: 'Agent ID',
callback_url: '回调地址'

View File

@ -28,7 +28,7 @@
<div v-if="item.isValid" class="border-t mt-16">
<el-row :gutter="12" class="mt-16">
<el-col v-for="(value, key) in item.config" :key="key" :span="12">
<el-text type="info">{{ formatFieldName(key) }}</el-text>
<el-text type="info">{{ formatFieldName(key, item) }}</el-text>
<div class="mt-4 mb-16 flex align-center">
<span
v-if="key !== 'app_secret'"
@ -146,10 +146,10 @@ function createPlatform(key: string, name: string): Platform {
}
}
function formatFieldName(key?: any): string {
function formatFieldName(key?: any, item?: Platform): string {
const fieldNames: { [key: string]: string } = {
corp_id: 'Corp ID',
app_key: 'APP Key',
app_key: item?.key != 'lark' ? 'APP Key' : 'App ID',
app_secret: 'APP Secret',
agent_id: 'Agent ID',
callback_url: '回调地址'

View File

@ -8,7 +8,12 @@
:size="32"
style="background: none"
>
<img :src="application_profile?.icon" alt="" />
<el-image
:src="application_profile?.icon"
alt=""
fit="cover"
style="width: 32px; height: 32px; display: block"
/>
</AppAvatar>
<AppAvatar
v-else-if="application_profile?.name"

View File

@ -9,7 +9,12 @@
:size="32"
style="background: none"
>
<img :src="applicationDetail?.icon" alt="" />
<el-image
:src="applicationDetail?.icon"
alt=""
fit="cover"
style="width: 32px; height: 32px; display: block"
/>
</AppAvatar>
<AppAvatar
v-else-if="applicationDetail?.name"

View File

@ -16,7 +16,12 @@
:size="32"
style="background: none"
>
<img :src="applicationDetail?.icon" alt="" />
<el-image
:src="applicationDetail?.icon"
alt=""
fit="cover"
style="width: 32px; height: 32px; display: block"
/>
</AppAvatar>
<AppAvatar
v-else-if="applicationDetail?.name"

View File

@ -17,7 +17,12 @@
:size="32"
style="background: none"
>
<img :src="applicationDetail?.icon" alt="" />
<el-image
:src="applicationDetail?.icon"
alt=""
fit="cover"
style="width: 32px; height: 32px; display: block"
/>
</AppAvatar>
<AppAvatar
v-else-if="applicationDetail?.name"

View File

@ -73,7 +73,12 @@
style="background: none"
class="mr-12"
>
<img :src="item?.icon" alt="" />
<el-image
:src="item?.icon"
alt=""
fit="cover"
style="width: 32px; height: 32px; display: block"
/>
</AppAvatar>
<AppAvatar
v-else-if="item?.name"

View File

@ -33,15 +33,15 @@
<el-checkbox
:disabled="props.manage"
v-model="allChecked[TeamEnum.MANAGE]"
:indeterminate="allIndeterminate[TeamEnum.MANAGE]"
label="管理"
@change="handleCheckAllChange($event, TeamEnum.MANAGE)"
/>
</template>
<template #default="{ row }">
<el-checkbox
:disabled="props.manage"
v-model="row.operate[TeamEnum.MANAGE]"
@change="checkedOperateChange(TeamEnum.MANAGE, row)"
@change="(e: boolean) => checkedOperateChange(TeamEnum.MANAGE, row, e)"
/>
</template>
</el-table-column>
@ -50,15 +50,15 @@
<el-checkbox
:disabled="props.manage"
v-model="allChecked[TeamEnum.USE]"
:indeterminate="allIndeterminate[TeamEnum.USE]"
label="查看"
@change="handleCheckAllChange($event, TeamEnum.USE)"
/>
</template>
<template #default="{ row }">
<el-checkbox
:disabled="props.manage"
v-model="row.operate[TeamEnum.USE]"
@change="checkedOperateChange(TeamEnum.USE, row)"
@change="(e: boolean) => checkedOperateChange(TeamEnum.USE, row, e)"
/>
</template>
</el-table-column>
@ -85,58 +85,67 @@ const isApplication = computed(() => props.type === TeamEnum.APPLICATION)
const emit = defineEmits(['update:data'])
const allChecked: any = ref({
[TeamEnum.MANAGE]: false,
[TeamEnum.USE]: false
[TeamEnum.MANAGE]: computed({
get: () => {
return filterData.value.some((item: any) => item.operate[TeamEnum.MANAGE])
},
set: (val: boolean) => {
if (val) {
filterData.value.map((item: any) => {
item.operate[TeamEnum.MANAGE] = true
})
} else {
filterData.value.map((item: any) => {
item.operate[TeamEnum.MANAGE] = false
})
}
}
}),
[TeamEnum.USE]: computed({
get: () => {
return filterData.value.some((item: any) => item.operate[TeamEnum.USE])
},
set: (val: boolean) => {
if (val) {
filterData.value.map((item: any) => {
item.operate[TeamEnum.USE] = true
})
} else {
filterData.value.map((item: any) => {
item.operate[TeamEnum.USE] = false
})
}
}
})
})
const filterText = ref('')
const filterData = computed(() => props.data.filter((v: any) => v.name.includes(filterText.value)))
watch(
() => props.data,
(val) => {
Object.keys(allChecked.value).map((item) => {
allChecked.value[item] = compare(item)
})
emit('update:data', val)
},
{
deep: true
}
)
function handleCheckAllChange(val: string | number | boolean, Name: string | number) {
if (val) {
props.data.map((item: any) => {
item.operate[Name] = true
})
} else {
props.data.map((item: any) => {
item.operate[Name] = false
})
}
}
function checkedOperateChange(Name: string | number, row: any) {
if (Name === TeamEnum.MANAGE && row.operate[TeamEnum.MANAGE]) {
props.data.map((item: any) => {
if (item.id === row.id) {
item.operate[TeamEnum.USE] = true
}
})
}
allChecked.value[Name] = compare(Name)
}
function compare(attrs: string | number) {
const filterData = props.data.filter((item: any) => item?.operate[attrs])
return props.data.length > 0 && filterData.length === props.data.length
}
onMounted(() => {
Object.keys(allChecked.value).map((item) => {
allChecked.value[item] = compare(item)
const allIndeterminate: any = ref({
[TeamEnum.MANAGE]: computed(() => {
const all_not_checked = filterData.value.every((item: any) => !item.operate[TeamEnum.MANAGE])
if (all_not_checked) {
return false
}
return !filterData.value.every((item: any) => item.operate[TeamEnum.MANAGE])
}),
[TeamEnum.USE]: computed(() => {
const all_not_checked = filterData.value.every((item: any) => !item.operate[TeamEnum.USE])
if (all_not_checked) {
return false
}
return !filterData.value.every((item: any) => item.operate[TeamEnum.USE])
})
})
function checkedOperateChange(Name: string | number, row: any, e: boolean) {
props.data.map((item: any) => {
if (item.id === row.id) {
item.operate[Name] = e
}
})
}
</script>
<style lang="scss" scope></style>

View File

@ -26,19 +26,13 @@
<h4 v-else>{{ nodeModel.properties.stepName }}</h4>
</div>
<div
@mousemove.stop
@mousedown.stop
@keydown.stop
@click.stop
v-if="showOperate(nodeModel.type)"
>
<div @mousemove.stop @mousedown.stop @keydown.stop @click.stop>
<el-button text @click="showNode = !showNode" class="mr-4">
<el-icon class="arrow-icon" :class="showNode ? 'rotate-180' : ''"
><ArrowDownBold />
</el-icon>
</el-button>
<el-dropdown :teleported="false" trigger="click">
<el-dropdown v-if="showOperate(nodeModel.type)" :teleported="false" trigger="click">
<el-button text>
<el-icon class="color-secondary"><MoreFilled /></el-icon>
</el-button>
@ -52,7 +46,7 @@
</div>
</div>
<el-collapse-transition>
<div @mousedown.stop @keydown.stop @click.stop v-if="showNode" class="mt-16">
<div @mousedown.stop @keydown.stop @click.stop v-show="showNode" class="mt-16">
<el-alert
v-if="node_status != 200"
class="mb-16"
@ -130,7 +124,19 @@ const height = ref<{
})
const showAnchor = ref<boolean>(false)
const anchorData = ref<any>()
const showNode = ref<boolean>(true)
// const showNode = ref<boolean>(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 node_status = computed(() => {
if (props.nodeModel.properties.status) {
return props.nodeModel.properties.status

View File

@ -234,13 +234,14 @@ class AppNodeModel extends HtmlResize.model {
}
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: y,
y: showNode ? y : y - 15,
id: `${id}_left`,
edgeAddable: false,
type: 'left'
@ -248,7 +249,7 @@ class AppNodeModel extends HtmlResize.model {
}
anchors.push({
x: x + width / 2 - 10,
y: y,
y: showNode ? y : y - 15,
id: `${id}_right`,
type: 'right'
})

View File

@ -35,10 +35,11 @@ class ConditionModel extends AppNodeModel {
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: y,
y: showNode ? y : y - 15,
id: `${id}_left`,
edgeAddable: false,
type: 'left'
@ -50,7 +51,7 @@ class ConditionModel extends AppNodeModel {
const h = get_up_index_height(branch_condition_list, index)
anchors.push({
x: x + width / 2 - 10,
y: y - height / 2 + 75 + h + element.height / 2,
y: showNode ? y - height / 2 + 75 + h + element.height / 2 : y - 15,
id: `${id}_${element.id}_right`,
type: 'right'
})