545 lines
18 KiB
Vue
545 lines
18 KiB
Vue
<template>
|
||
<el-drawer v-model="visible" :title="form.id ? '编辑' : '新增'" size="50%">
|
||
<el-form ref="dataFormRef" :model="form" :rules="dataRules" label-width="auto" v-loading="loading">
|
||
<!-- Tab buttons -->
|
||
<div role="tablist" class="mb-5 tabs tabs-boxed abs-bordered">
|
||
<a role="tab" class="tab" :class="{ 'tab-active': activeTab === 'basic' }" @click="activeTab = 'basic'"
|
||
><span class="inline-flex justify-center items-center mr-2 w-5 h-5 text-white rounded-full bg-primary">1</span>基础配置</a
|
||
>
|
||
<a role="tab" class="tab" :class="{ 'tab-active': activeTab === 'advanced' }" @click="activeTab = 'advanced'"
|
||
><span class="inline-flex justify-center items-center mr-2 w-5 h-5 text-white rounded-full bg-primary">2</span>高级配置</a
|
||
>
|
||
<a role="tab" class="tab" :class="{ 'tab-active': activeTab === 'security' }" @click="activeTab = 'security'"
|
||
><span class="inline-flex justify-center items-center mr-2 w-5 h-5 text-white rounded-full bg-primary">3</span>安全配置</a
|
||
>
|
||
</div>
|
||
|
||
<!-- Basic Configuration Tab -->
|
||
<div v-show="activeTab === 'basic'" class="px-2">
|
||
<el-row :gutter="30">
|
||
<el-col :span="12" class="mb-6">
|
||
<el-form-item label="头像" prop="avatarUrl">
|
||
<upload-img v-model:image-url="form.avatarUrl" />
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="12" class="mb-6">
|
||
<!-- Placeholder for alignment -->
|
||
</el-col>
|
||
</el-row>
|
||
|
||
<el-row :gutter="30">
|
||
<el-col :span="12" class="mb-6">
|
||
<el-form-item label="名称" prop="name">
|
||
<el-input v-model="form.name" maxlength="20" placeholder="请输入名称" />
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="12" class="mb-6">
|
||
<el-form-item prop="sortOrder">
|
||
<template #label>
|
||
排序值
|
||
<tip content="越大展示越靠前" />
|
||
</template>
|
||
<el-input-number v-model="form.sortOrder" :min="1" :max="9999" :step="1" />
|
||
</el-form-item>
|
||
</el-col>
|
||
</el-row>
|
||
|
||
<el-row :gutter="30">
|
||
<el-col :span="24" class="mb-6">
|
||
<el-form-item label="欢迎语" prop="welcomeMsg">
|
||
<el-input
|
||
v-model="form.welcomeMsg"
|
||
type="textarea"
|
||
placeholder="描述知识库的内容,详尽的描述将帮助AI能深入理解该知识库的内容,能更准确的检索到内容,提高该知识库的命中率。"
|
||
maxlength="1024"
|
||
show-word-limit
|
||
rows="10"
|
||
/>
|
||
</el-form-item>
|
||
</el-col>
|
||
</el-row>
|
||
</div>
|
||
|
||
<!-- Advanced Configuration Tab -->
|
||
<div v-show="activeTab === 'advanced'" class="px-2">
|
||
<el-divider content-position="left">模型配置</el-divider>
|
||
|
||
<el-row :gutter="30" class="mb-6">
|
||
<el-col :span="12">
|
||
<el-form-item label="向量库" prop="storeId">
|
||
<el-select v-model="form.storeId" placeholder="请选择向量库" clearable filterable :disabled="form.id !== ''">
|
||
<el-option v-for="item in storeList" :key="item.storeId" :label="item.name" :value="item.storeId" />
|
||
</el-select>
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="12">
|
||
<el-form-item label="向量模型" prop="embeddingModel">
|
||
<el-select v-model="form.embeddingModel" placeholder="请选择向量模型" clearable filterable :disabled="form.id !== ''">
|
||
<el-option v-for="item in embeddingModelList" :key="item.id" :label="item.name" :value="item.name" />
|
||
</el-select>
|
||
</el-form-item>
|
||
</el-col>
|
||
</el-row>
|
||
|
||
<el-row :gutter="30" class="mb-6">
|
||
<el-col :span="12">
|
||
<el-form-item label="总结模型" prop="summaryModel">
|
||
<el-select v-model="form.summaryModel" placeholder="请选择总结模型" clearable filterable>
|
||
<el-option v-for="item in chatModelList" :key="item.id" :label="item.name" :value="item.name" />
|
||
</el-select>
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="12">
|
||
<el-form-item label="重排模型" prop="rerankerModel">
|
||
<template #label>
|
||
重排模型
|
||
<tip content="重排模型,如没有重排模型,则后台使用默认重排算法" />
|
||
</template>
|
||
<el-select v-model="form.rerankerModel" placeholder="请选择重排模型" clearable filterable>
|
||
<el-option v-for="item in rerankerModelList" :key="item.id" :label="item.name" :value="item.name" />
|
||
</el-select>
|
||
</el-form-item>
|
||
</el-col>
|
||
</el-row>
|
||
|
||
<el-divider content-position="left">检索配置</el-divider>
|
||
|
||
<el-row :gutter="30" class="mb-6">
|
||
<el-col :span="12">
|
||
<el-form-item label="会话轮数" prop="multiRound">
|
||
<template #label>
|
||
会话轮数
|
||
<tip content="会话轮数,0代表不记忆上文会话" />
|
||
</template>
|
||
<el-input-number v-model="form.multiRound" :min="0" :max="5" :step="1" class="w-full" />
|
||
</el-form-item>
|
||
</el-col>
|
||
|
||
<el-col :span="12">
|
||
<el-form-item prop="topK">
|
||
<template #label>
|
||
匹配条数
|
||
<tip content="向量数据库匹配最多几条结果" />
|
||
</template>
|
||
<el-input-number v-model="form.topK" :min="1" :max="5" :step="1" class="w-full" />
|
||
</el-form-item>
|
||
</el-col>
|
||
</el-row>
|
||
|
||
<el-row :gutter="30" class="mb-6">
|
||
<el-col :span="12">
|
||
<el-form-item prop="fragmentSize">
|
||
<template #label>
|
||
分片值
|
||
<tip content="分片值取决于模型自身能力,理论上分片值越大越准确" />
|
||
</template>
|
||
<el-input-number v-model="form.fragmentSize" :min="500" :max="9999" :step="1" class="w-full" />
|
||
</el-form-item>
|
||
</el-col>
|
||
|
||
<el-col :span="12">
|
||
<el-form-item prop="score">
|
||
<template #label>
|
||
匹配率
|
||
<tip content="向量数据库匹配率,建议不低于 50%" />
|
||
</template>
|
||
<el-slider v-model="form.score" :step="10" :min="10" :max="90" :format-tooltip="(value: number) => value + '%'" show-stops />
|
||
</el-form-item>
|
||
</el-col>
|
||
</el-row>
|
||
|
||
<el-row :gutter="30" class="mb-6">
|
||
<el-col :span="12">
|
||
<el-form-item prop="emptyLlmFlag">
|
||
<template #label>
|
||
空查询
|
||
<tip content="查询不到结果时,是否调用大模型查询" />
|
||
</template>
|
||
<el-switch v-model="form.emptyLlmFlag" :active-value="'1'" :inactive-value="'0'" />
|
||
</el-form-item>
|
||
</el-col>
|
||
|
||
<el-col :span="12" v-if="form.emptyLlmFlag !== '1'">
|
||
<el-form-item prop="emptyDesc">
|
||
<template #label>
|
||
空提示
|
||
<tip content="未匹配的时候,返回的文案" />
|
||
</template>
|
||
<el-input v-model="form.emptyDesc" placeholder="请输入描述" />
|
||
</el-form-item>
|
||
</el-col>
|
||
</el-row>
|
||
|
||
<el-divider content-position="left">处理选项</el-divider>
|
||
|
||
<el-row :gutter="30" class="mb-6">
|
||
<el-col :span="12">
|
||
<el-form-item label="文档总结" prop="preSummary">
|
||
<el-switch v-model="form.preSummary" :active-value="'1'" :inactive-value="'0'" />
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="12">
|
||
<el-form-item prop="aiOcrFlag">
|
||
<template #label>
|
||
AI OCR
|
||
<tip content="PDF、图片等文件,自动进行 AI OCR 识别" />
|
||
</template>
|
||
<el-switch v-model="form.aiOcrFlag" :active-value="'1'" :inactive-value="'0'" />
|
||
</el-form-item>
|
||
</el-col>
|
||
</el-row>
|
||
|
||
<el-row :gutter="30" class="mb-6">
|
||
<el-col :span="12">
|
||
<el-form-item label="会话压缩" prop="preCompress">
|
||
<el-switch v-model="form.preCompress" :active-value="'1'" :inactive-value="'0'" />
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="12">
|
||
<el-form-item prop="standardFlag">
|
||
<template #label>
|
||
标注数据
|
||
<tip content="使用已经标注修正后的答案,直接返回" />
|
||
</template>
|
||
<el-switch v-model="form.standardFlag" :active-value="'1'" :inactive-value="'0'" />
|
||
</el-form-item>
|
||
</el-col>
|
||
</el-row>
|
||
</div>
|
||
|
||
<!-- Security Configuration Tab -->
|
||
<div v-show="activeTab === 'security'" class="px-2">
|
||
<el-divider content-position="left">访问控制</el-divider>
|
||
|
||
<el-row :gutter="30" class="mb-6">
|
||
<el-col :span="12">
|
||
<el-form-item label="是否对外" prop="publicFlag">
|
||
<el-switch v-model="form.publicFlag" :active-value="'1'" :inactive-value="'0'" />
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="12">
|
||
<el-form-item prop="publicPassword">
|
||
<template #label>
|
||
安全密钥
|
||
<tip content="对外服务,需要用户输入的密码" />
|
||
</template>
|
||
<el-input v-model="form.publicPassword" placeholder="请输入密码" />
|
||
</el-form-item>
|
||
</el-col>
|
||
</el-row>
|
||
|
||
<el-row :gutter="30" class="mb-6">
|
||
<el-col :span="24">
|
||
<el-form-item prop="visibleUsers">
|
||
<template #label>
|
||
可见范围
|
||
<tip content="选择可以访问此知识库的用户,如果不选择则全部的用户可访问" />
|
||
</template>
|
||
<org-selector v-model="form.visibleUsers" :type="'user'" :multiple="true" :selectSelf="true" />
|
||
</el-form-item>
|
||
</el-col>
|
||
</el-row>
|
||
|
||
<el-divider content-position="left">内容过滤</el-divider>
|
||
|
||
<el-row :gutter="30" class="mb-6">
|
||
<el-col :span="12">
|
||
<el-form-item label="敏感词过滤" prop="sensitiveFlag">
|
||
<el-switch v-model="form.sensitiveFlag" :active-value="'1'" :inactive-value="'0'" />
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="12">
|
||
<el-form-item prop="sensitiveMsg">
|
||
<template #label>
|
||
提示
|
||
<tip content="命中敏感词,返回的文案" />
|
||
</template>
|
||
<el-input v-model="form.sensitiveMsg" placeholder="请输入描述" />
|
||
</el-form-item>
|
||
</el-col>
|
||
</el-row>
|
||
|
||
<el-divider content-position="left">附加信息</el-divider>
|
||
|
||
<el-row :gutter="30" class="mb-6">
|
||
<el-col :span="24">
|
||
<el-form-item label="底部信息" prop="footer">
|
||
<el-input type="textarea" maxlength="255" :rows="5" v-model="form.footer" placeholder="聊天框底部的信息,支持 HTML 语法" />
|
||
</el-form-item>
|
||
</el-col>
|
||
</el-row>
|
||
</div>
|
||
</el-form>
|
||
<template #footer>
|
||
<span class="dialog-footer">
|
||
<el-button @click="visible = false">取消</el-button>
|
||
<el-button type="primary" @click="onSubmit" :disabled="loading">确认</el-button>
|
||
</span>
|
||
</template>
|
||
</el-drawer>
|
||
</template>
|
||
|
||
<script setup lang="ts" name="AiDatasetDialog">
|
||
// import { useDict } from '/@/hooks/dict'; // Removed as it's unused
|
||
import { useMessage } from '/@/hooks/message';
|
||
import { getDetails, addObj, putObj, validateName } from '/@/api/knowledge/aiDataset';
|
||
import { list as aiModelList } from '/@/api/knowledge/aiModel';
|
||
import { list } from '/@/api/knowledge/aiEmbedStore';
|
||
import { rule } from '/@/utils/validate';
|
||
import UploadImg from '/@/components/Upload/Image.vue';
|
||
import OrgSelector from '/@/components/OrgSelector/index.vue';
|
||
|
||
const emit = defineEmits(['refresh']);
|
||
|
||
// 定义变量内容
|
||
const dataFormRef = ref();
|
||
const visible = ref(false);
|
||
const loading = ref(false);
|
||
const activeTab = ref('basic'); // Added for tab management
|
||
|
||
// 提交表单数据
|
||
const form = reactive({
|
||
id: '',
|
||
name: '',
|
||
avatarUrl: '',
|
||
description: '',
|
||
units: '0',
|
||
fileSize: '0',
|
||
multiRound: 3,
|
||
topK: 2,
|
||
score: 40,
|
||
fragmentSize: 1000,
|
||
sortOrder: 1,
|
||
emptyLlmFlag: '0',
|
||
emptyDesc: '知识库未匹配相关问题,请重新提问',
|
||
sensitiveFlag: '1',
|
||
preSummary: '0',
|
||
preCompress: '0',
|
||
sensitiveMsg: '您输入内容包含敏感词,请重新输入',
|
||
publicFlag: '1',
|
||
publicPassword: '' as string | undefined, // Allow undefined for publicPassword
|
||
standardFlag: '0',
|
||
aiOcrFlag: '1',
|
||
welcomeMsg: '',
|
||
footer: '',
|
||
embeddingModel: '',
|
||
summaryModel: '',
|
||
storeId: '',
|
||
rerankerModel: '',
|
||
visibleUsers: [] as Array<any>, // 可见范围用户列表
|
||
});
|
||
|
||
// 定义校验规则
|
||
const dataRules = ref({
|
||
name: [
|
||
{ required: true, message: '知识库名称不能为空', trigger: 'blur' },
|
||
{
|
||
validator: (rule: any, value: any, callback: any) => {
|
||
validateName(rule, value, callback, form.id !== '');
|
||
},
|
||
trigger: 'blur',
|
||
},
|
||
],
|
||
welcomeMsg: [{ required: true, message: '欢迎语不能为空', trigger: 'blur' }],
|
||
multiRound: [{ required: true, message: '多轮会话不能为空', trigger: 'blur' }],
|
||
topK: [{ required: true, message: '多轮会话不能为空', trigger: 'blur' }],
|
||
avatarUrl: [{ required: true, message: '封面不能为空', trigger: 'blur' }],
|
||
emptyDesc: [
|
||
{ validator: rule.overLength, trigger: 'blur' },
|
||
{
|
||
required: true,
|
||
message: '提示不能为空',
|
||
trigger: 'blur',
|
||
},
|
||
],
|
||
sensitiveMsg: [
|
||
{ validator: rule.overLength, trigger: 'blur' },
|
||
{
|
||
required: true,
|
||
message: '提示不能为空',
|
||
trigger: 'blur',
|
||
},
|
||
],
|
||
storeId: [{ required: true, message: '请选择向量库', trigger: 'change' }],
|
||
embeddingModel: [{ required: true, message: '请选择向量模型', trigger: 'change' }],
|
||
summaryModel: [{ required: true, message: '请选择总结模型', trigger: 'change' }],
|
||
rerankerModel: [
|
||
{
|
||
validator: rule.overLength,
|
||
trigger: 'blur',
|
||
},
|
||
],
|
||
});
|
||
|
||
const embeddingModelList = ref<Array<{ id: string; name: string }>>([]);
|
||
const chatModelList = ref<Array<{ id: string; name: string }>>([]);
|
||
const storeList = ref<Array<{ storeId: string; name: string }>>([]);
|
||
const rerankerModelList = ref<Array<{ id: string; name: string }>>([]);
|
||
|
||
// Modify the function to fetch AI models based on modelType
|
||
async function loadAiModelList() {
|
||
try {
|
||
const [embeddingResponse, chatResponse, rerankerResponse] = await Promise.all([
|
||
aiModelList({ modelType: 'Embedding' }),
|
||
aiModelList({ modelType: ['Chat', 'Reason'] }),
|
||
aiModelList({ modelType: 'Reranker' }),
|
||
]);
|
||
|
||
embeddingModelList.value = embeddingResponse.data.map((item: any) => ({
|
||
id: item.id,
|
||
name: item.name,
|
||
}));
|
||
|
||
chatModelList.value = chatResponse.data.map((item: any) => ({
|
||
id: item.id,
|
||
name: item.name,
|
||
}));
|
||
|
||
rerankerModelList.value = rerankerResponse.data.map((item: any) => ({
|
||
id: item.id,
|
||
name: item.name,
|
||
}));
|
||
|
||
// Set default values if lists are not empty
|
||
if (embeddingModelList.value.length > 0 && !form.embeddingModel) {
|
||
form.embeddingModel = embeddingModelList.value[0].name;
|
||
}
|
||
if (chatModelList.value.length > 0 && !form.summaryModel) {
|
||
form.summaryModel = chatModelList.value[0].name;
|
||
}
|
||
if (rerankerModelList.value.length > 0 && !form.rerankerModel) {
|
||
form.rerankerModel = rerankerModelList.value[0].name;
|
||
}
|
||
} catch (error) {
|
||
useMessage().error('加载AI模型列表失败');
|
||
}
|
||
}
|
||
|
||
// 初始化表单数据
|
||
const getAiDatasetData = (id: string) => {
|
||
// 获取数据
|
||
loading.value = true;
|
||
getDetails({ id })
|
||
.then((res: any) => {
|
||
Object.assign(form, res.data);
|
||
})
|
||
.finally(() => {
|
||
loading.value = false;
|
||
});
|
||
};
|
||
|
||
// Modify the openDialog function
|
||
const openDialog = async (id: string) => {
|
||
visible.value = true;
|
||
form.id = '';
|
||
|
||
// Reset form data
|
||
nextTick(() => {
|
||
dataFormRef.value?.resetFields();
|
||
});
|
||
|
||
// Load the collection list and AI model list
|
||
await loadAiModelList();
|
||
|
||
// 初始化向量库列表
|
||
const { data } = await list();
|
||
storeList.value = data;
|
||
|
||
// Set default value for storeId if list is not empty
|
||
if (storeList.value.length > 0 && !form.storeId) {
|
||
form.storeId = storeList.value[0].storeId;
|
||
}
|
||
|
||
// Get aiDataset information
|
||
if (id) {
|
||
form.id = id;
|
||
getAiDatasetData(id);
|
||
} else {
|
||
// Set default values for new records
|
||
if (embeddingModelList.value.length > 0) {
|
||
form.embeddingModel = embeddingModelList.value[0].name;
|
||
}
|
||
if (chatModelList.value.length > 0) {
|
||
form.summaryModel = chatModelList.value[0].name;
|
||
}
|
||
if (rerankerModelList.value.length > 0) {
|
||
form.rerankerModel = rerankerModelList.value[0].name;
|
||
}
|
||
}
|
||
};
|
||
|
||
// 提交
|
||
const onSubmit = async () => {
|
||
try {
|
||
// 步骤 1: 异步验证整个表单
|
||
// dataFormRef.value.validate() 会返回一个 Promise
|
||
// 如果验证失败,它会抛出错误,该错误将被下面的 catch 块捕获
|
||
await dataFormRef.value.validate();
|
||
|
||
// 步骤 2: 如果验证通过,则继续执行提交逻辑
|
||
// 特殊处理:如果存在 ID (即为编辑模式) 且密码字段包含 '**' (表示未修改),
|
||
// 则将密码设置为空,避免将占位符提交到后端
|
||
if (form.id && form.publicPassword?.includes('**')) {
|
||
form.publicPassword = undefined;
|
||
}
|
||
|
||
loading.value = true; // 开始加载状态,禁用提交按钮等
|
||
// 根据是否存在 form.id 来判断是更新 (putObj) 还是新增 (addObj)
|
||
form.id ? await putObj(form) : await addObj(form);
|
||
|
||
// 操作成功提示
|
||
useMessage().success(form.id ? '修改成功' : '添加成功');
|
||
visible.value = false; // 关闭抽屉(或弹窗)
|
||
emit('refresh'); // 触发 'refresh' 事件,通知父组件刷新列表数据
|
||
} catch (invalidFields: any) {
|
||
// 步骤 3: 处理表单验证失败的情况
|
||
// invalidFields 对象包含了所有未通过验证的字段及其错误信息
|
||
// Element Plus 的 validate 方法在验证失败时会 reject 一个包含错误字段的对象
|
||
if (invalidFields && Object.keys(invalidFields).length > 0) {
|
||
const errorFields: string[] = Object.keys(invalidFields); // 获取所有验证失败的字段名
|
||
|
||
// 定义各选项卡包含的字段,用于定位错误字段所在的选项卡
|
||
const tabFields: Record<string, string[]> = {
|
||
basic: ['name', 'avatarUrl', 'welcomeMsg', 'sortOrder'], // 基础配置选项卡下的字段
|
||
advanced: [
|
||
'storeId',
|
||
'embeddingModel',
|
||
'summaryModel',
|
||
'multiRound',
|
||
'topK',
|
||
'fragmentSize',
|
||
'score',
|
||
'emptyLlmFlag',
|
||
'emptyDesc',
|
||
'preSummary',
|
||
'preCompress',
|
||
'aiOcrFlag',
|
||
'standardFlag',
|
||
'rerankerModel',
|
||
], // 高级配置选项卡下的字段
|
||
security: ['publicFlag', 'publicPassword', 'visibleUsers', 'sensitiveFlag', 'sensitiveMsg', 'footer'], // 安全配置选项卡下的字段
|
||
};
|
||
|
||
// 遍历 tabFields 来确定哪个选项卡包含第一个出错的字段
|
||
for (const [tab, fieldsInTab] of Object.entries(tabFields)) {
|
||
// .some() 方法检查 errorFields 数组中是否有任何一个字段存在于当前 fieldsInTab 数组中
|
||
if (errorFields.some((errorField: string) => fieldsInTab.includes(errorField))) {
|
||
activeTab.value = tab; // 如果找到,则切换到该选项卡
|
||
break; // 找到第一个错误字段所在的选项卡后,停止遍历
|
||
}
|
||
}
|
||
}
|
||
// 如果验证失败,错误被捕获,提交过程在此处停止。
|
||
// 函数将继续执行 finally 块。
|
||
} finally {
|
||
// 步骤 4: 无论提交成功还是失败,最后都会执行 finally 块
|
||
loading.value = false; // 结束加载状态
|
||
}
|
||
};
|
||
|
||
// 暴露变量
|
||
defineExpose({
|
||
openDialog,
|
||
});
|
||
</script>
|