Files
school-developer/src/views/knowledge/aiDataset/form.vue
吴红兵 b997b3ba48 fix
2026-03-07 12:35:45 +08:00

545 lines
18 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>
<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>