This commit is contained in:
吴红兵
2025-12-02 10:37:49 +08:00
commit 1f645dad3e
1183 changed files with 147673 additions and 0 deletions

View File

@@ -0,0 +1,549 @@
<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>

View File

@@ -0,0 +1,243 @@
<template>
<div class="layout-padding">
<div class="layout-padding-auto layout-padding-view">
<el-row>
<div class="mb8" style="width: 100%">
<el-button icon="folder-add" type="primary" class="ml10" @click="formDialogRef.openDialog()" v-auth="'knowledge_aiDataset_add'">
</el-button>
<el-button plain :disabled="multiple" icon="Delete" type="primary" v-auth="'knowledge_aiDataset_del'" @click="handleDelete(selectObjs)">
删除
</el-button>
<right-toolbar
v-model:showSearch="showSearch"
:export="'knowledge_aiDataset_export'"
@exportExcel="exportExcel"
class="ml10 mr20"
style="float: right"
@queryTable="getDataList"
></right-toolbar>
</div>
</el-row>
<el-scrollbar class="h-[calc(100vh-280px)] mb-4">
<div class="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
<div
v-for="dataset in state.dataList"
:key="dataset.id"
class="group overflow-hidden bg-white rounded-lg shadow-sm border border-gray-100 transition-all duration-300 cursor-pointer dark:bg-gray-800 dark:border-gray-700 hover:shadow-lg hover:border-primary-100 hover:translate-y-[-2px]"
@dblclick="go2document(dataset)"
>
<div class="p-5">
<div class="flex items-start">
<div class="flex overflow-hidden justify-center items-center rounded-lg border border-gray-200 size-12 dark:border-gray-700">
<img :src="baseURL + dataset.avatarUrl" alt="Avatar" class="object-cover w-full h-full" />
</div>
<div class="overflow-hidden flex-1 ml-3">
<div class="text-base font-medium text-gray-900 truncate dark:text-white">
{{ dataset.name }}
</div>
<div class="flex items-center mt-1 text-xs text-gray-500 dark:text-gray-400">
<el-icon class="mr-1"><Document /></el-icon>
文档数{{ dataset.units || 0 }}
</div>
</div>
<div v-if="dataset.publicFlag === '1'" class="ml-2">
<el-tag size="small" type="success">已发布</el-tag>
</div>
</div>
<div class="overflow-y-auto mt-4 h-16 text-sm text-gray-600 dark:text-gray-300 line-clamp-3">
{{ dataset.description || '暂无描述' }}
</div>
<div class="flex justify-start items-center pt-3 mt-4 border-t border-gray-100 dark:border-gray-700">
<el-button
v-if="dataset.publicFlag === '1'"
class="!p-2 text-gray-600 rounded-full transition-colors dark:text-gray-300 hover:text-primary hover:bg-gray-100 dark:hover:bg-gray-700"
text
type="primary"
v-auth="'knowledge_aiDataset_edit'"
@click="mountDialogRef.openDialog(dataset.id)"
>
<el-icon><Link /></el-icon>
</el-button>
<el-button
class="!p-2 text-gray-600 rounded-full transition-colors dark:text-gray-300 hover:text-primary hover:bg-gray-100 dark:hover:bg-gray-700"
text
type="primary"
v-auth="'knowledge_aiDataset_edit'"
@click="formDialogRef.openDialog(dataset.id)"
>
<el-icon><EditPen /></el-icon>
</el-button>
<el-button
class="!p-2 text-gray-600 rounded-full transition-colors dark:text-gray-300 hover:text-primary hover:bg-gray-100 dark:hover:bg-gray-700"
text
type="primary"
v-auth="'knowledge_aiDataset_del'"
@click="handleDelete([dataset.id])"
>
<el-icon><Delete /></el-icon>
</el-button>
<div class="mx-2 w-px h-4 bg-gray-200 dark:bg-gray-700"></div>
<el-button
class="!p-2 text-gray-600 rounded-full transition-colors dark:text-gray-300 hover:text-primary hover:bg-gray-100 dark:hover:bg-gray-700"
text
type="primary"
@click="recallDialogRef.openDialog(dataset)"
>
<el-icon><Connection /></el-icon>
</el-button>
<el-button
class="!p-2 text-gray-600 rounded-full transition-colors dark:text-gray-300 hover:text-primary hover:bg-gray-100 dark:hover:bg-gray-700"
text
type="primary"
@click="handleNavigateToChat(dataset)"
>
<el-icon><ChatDotRound /></el-icon>
</el-button>
<div class="flex-grow"></div>
<el-checkbox
class="ml-4"
:value="selectObjs.includes(dataset.id)"
@change="(val: boolean) => handleCardSelect(val, dataset.id)"
></el-checkbox>
</div>
</div>
</div>
</div>
</el-scrollbar>
<!-- 无数据显示 -->
<el-empty v-if="!state.dataList || state.dataList.length === 0" description="暂无数据"></el-empty>
<pagination @size-change="sizeChangeHandle" @current-change="currentChangeHandle" v-bind="state.pagination" />
</div>
<!-- 编辑新增 -->
<form-dialog ref="formDialogRef" @refresh="getDataList(false)" />
<mount-dialog ref="mountDialogRef" />
<recall-dialog ref="recallDialogRef" />
</div>
</template>
<script setup lang="ts" name="systemAiDataset">
import { BasicTableProps, useTable } from '/@/hooks/table';
import { fetchList, delObjs } from '/@/api/knowledge/aiDataset';
import { useMessage, useMessageBox } from '/@/hooks/message';
import { EditPen, Delete, Document, Link, ChatDotRound, Connection } from '@element-plus/icons-vue';
import { computed } from 'vue';
const router = useRouter();
const baseURL = computed(() => import.meta.env.VITE_API_URL);
// 引入组件
const FormDialog = defineAsyncComponent(() => import('./form.vue'));
const MountDialog = defineAsyncComponent(() => import('./mount.vue'));
const RecallDialog = defineAsyncComponent(() => import('./recall.vue'));
// 定义变量内容
const formDialogRef = ref();
const mountDialogRef = ref();
const recallDialogRef = ref();
// 搜索变量
const queryRef = ref();
const showSearch = ref(true);
// 多选变量
const selectObjs = ref([]) as any;
const multiple = ref(true);
const state: BasicTableProps = reactive<BasicTableProps>({
queryForm: {},
pageList: fetchList,
dataList: [],
});
// table hook
const { getDataList, currentChangeHandle, sizeChangeHandle, downBlobFile } = useTable(state);
// 清空搜索条件
const resetQuery = () => {
// 清空搜索条件
queryRef.value?.resetFields();
// 清空多选
selectObjs.value = [];
getDataList();
};
// 导出excel
const exportExcel = () => {
downBlobFile('/knowledge/aiDataset/export', Object.assign(state.queryForm, { ids: selectObjs }), 'aiDataset.xlsx');
};
// 多选事件 - 为卡片视图添加的选择函数
const handleCardSelect = (selected: boolean, dataId: string) => {
if (selected) {
selectObjs.value.push(dataId);
} else {
selectObjs.value = selectObjs.value.filter((id: string) => id !== dataId);
}
multiple.value = selectObjs.value.length === 0;
};
// 删除操作
const handleDelete = async (ids: string[]) => {
try {
await useMessageBox().confirm('此操作将永久删除');
} catch {
return;
}
try {
await delObjs(ids);
getDataList();
useMessage().success('删除成功');
} catch (err: any) {
useMessage().error(err.msg);
}
};
const go2document = (dataset: any) => {
router.push({
path: '/knowledge/aiDocument/index',
query: {
datasetId: dataset.id,
},
});
};
const handleNavigateToChat = (dataset: any) => {
router.push({
path: '/knowledge/aiChat/index',
query: {
datasetId: dataset.id,
},
});
};
</script>
<style lang="scss" scoped>
:deep(.el-scrollbar__wrap) {
overflow-x: hidden !important;
}
:deep(.el-checkbox) {
margin-right: 0;
}
.bg-primary-100 {
background-color: var(--el-color-primary-light-9);
}
.text-primary {
color: var(--el-color-primary);
}
</style>

View File

@@ -0,0 +1,215 @@
<template>
<el-dialog
v-model="dialogVisible"
title="选择挂载方式"
width="50%"
:modal="false"
>
<div>
<el-card size="mini">
<el-row :gutter="10" justify="center">
<el-col :span="8">
<div :class="selectedMethod === 1 ? 'img-selected' : 'img-unselect'" @click="selectImg(1)">
<div class="css-1awpln7">
<div class="css-0">
<img alt="" src="/@/assets/ai/link.svg" class="chakra-image css-0">
</div>
</div>
</div>
</el-col>
<el-col :span="8">
<div :class="selectedMethod === 2 ? 'img-selected' : 'img-unselect'" @click="selectImg(2)">
<div class="css-1awpln7">
<div class="css-0">
<img alt="" src="/@/assets/ai/iframe.svg" class="chakra-image css-0">
</div>
</div>
</div>
</el-col>
<el-col :span="8">
<div :class="selectedMethod === 3 ? 'img-selected' : 'img-unselect'" @click="selectImg(3)">
<div class="css-1awpln7">
<div class="css-0">
<img alt="" src="/@/assets/ai/script.svg" class="chakra-image css-0">
</div>
</div>
</div>
</el-col>
</el-row>
</el-card>
</div>
<div>
<el-card size="mini" body-class="card-class">
<template #header>
<div class="card-header">
<div v-if="selectedMethod === 1">
<span>将下面链接复制到浏览器打开</span>
<span>
<el-button style="float: right" class="ml-2" type="primary" @click="openText(url())">打开</el-button>
<el-button style="float: right" type="primary" @click="copyText(url())">复制</el-button>
</span>
</div>
<div v-if="selectedMethod === 2">
<span>复制下面 Iframe 加入到你的网站中</span>
<span>
<el-button style="float: right" type="primary" @click="copyText(iframe())">复制</el-button>
</span>
</div>
<div v-if="selectedMethod === 3">
<span>将下面代码加入到你的网站中</span>
<span>
<el-button style="float: right" type="primary" @click="copyText(script())">复制</el-button>
</span>
</div>
</div>
</template>
<span v-if="selectedMethod === 1">
<div class="mockup-code">
<pre data-prefix="$"><code> {{ url() }}</code></pre>
</div>
</span>
<span v-if="selectedMethod === 2">
<div class="mockup-code">
<pre data-prefix="$"><code> {{ iframe() }}</code></pre>
</div>
</span>
<span v-if="selectedMethod === 3">
<div class="mockup-code">
<pre data-prefix="$"><code> {{ script() }}</code></pre>
</div>
</span>
</el-card>
</div>
</el-dialog>
</template>
<script setup lang="ts" name="AiDatasetDialog">
import commonFunction from "/@/utils/commonFunction";
const emit = defineEmits(['refresh']);
const {copyText} = commonFunction();
const left_right = [
{
label: '左侧',
value: 'left',
},
{
label: '右侧',
value: 'right',
}
]
const top_bottom = [
{
label: '顶部',
value: 'top',
},
{
label: '底部',
value: 'bottom',
}
]
const dialogVisible = ref(false)
const selectedDatasetId = ref()
const selectedMethod = ref(1)
const data_btn_x = ref(16)
const data_btn_y = ref(16)
const data_stream = ref(true)
const data_direction_x = ref('right')
const data_direction_y = ref('bottom')
const openDialog = (datasetid: any) => {
dialogVisible.value = true
selectedDatasetId.value = datasetid
}
const currentHostname = window.location.origin;
const openImageBase64 = ref("")
const closeImageBase64 = ref("")
function handleOpenIconBeforeUpload(file: Blob) {
const reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = e => {
// 在这里处理 Base64 数据
openImageBase64.value = e.target.result
};
return false;
}
function handleCloseIconBeforeUpload(file: Blob) {
const reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = e => {
// 在这里处理 Base64 数据
closeImageBase64.value = e.target.result
};
return false;
}
const isMicro = import.meta.env.VITE_IS_MICRO === 'true' ? 1 : 0
const script = () => {
return `<script id="chatbot-iframe" src="${currentHostname}/bot/embed.min.js?t=${Date.now()}" data-bot-src="${currentHostname}/bot/index.html#/${isMicro}/${selectedDatasetId.value}/chat" async defer><\/script>`
}
const iframe = () => {
return `<iframe src="${currentHostname}/bot/bot/index.html#/${isMicro}/${selectedDatasetId.value}/chat" style="width: 100%; height: 100%;" frameborder="0" allow="microphone"/>`
}
const url = () => {
return `${currentHostname}/bot/index.html#/${isMicro}/${selectedDatasetId.value}/chat`
}
const selectImg = (index: number) => {
selectedMethod.value = index
}
const openText = (url: string) => {
window.open(url)
}
// 暴露变量
defineExpose({
openDialog
});
</script>
<style scoped>
.img-unselect {
display: flex;
-webkit-box-align: center;
align-items: center;
cursor: pointer;
user-select: none;
border: 1.5px solid rgb(232, 235, 240);
border-radius: 5px;
position: relative;
background: #fbfbfc;
padding: 0px !important;
}
.img-unselect:hover {
border-color: #1dbcd8;
}
.img-selected {
display: flex;
-webkit-box-align: center;
align-items: center;
cursor: pointer;
user-select: none;
border-style: solid;
border-image: initial;
border-width: 1.5px;
border-radius: 5px;
position: relative;
border-color: #1dbcd8;
background: #f0f4ff;
padding: 0px !important;
}
.card-class {
background-color: #F4F4F7;
}
</style>

View File

@@ -0,0 +1,161 @@
<template>
<el-drawer v-model="visible" title="召回测试" size="50%">
<el-form ref="dataFormRef" :model="form" :rules="dataRules" label-width="auto" v-loading="loading">
<el-row>
<el-col :span="12" class="mb20">
<el-form-item label="知识库名称">
<el-input :placeholder="dataset?.name" disabled />
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-form-item label="召回数量" prop="topK">
<el-input-number v-model="form.topK" :min="1" :max="10" :step="1" />
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="12" class="mb20">
<el-form-item label="是否全文检索" prop="fullTextSearch">
<el-radio-group v-model="form.fullTextSearch">
<el-radio v-for="(item, index) in yes_no_type" :key="index" :label="item.value" border>{{ item.label }}</el-radio>
</el-radio-group>
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-form-item label="是否重排" prop="reRanking">
<el-radio-group v-model="form.reRanking">
<el-radio v-for="(item, index) in yes_no_type" :key="index" :label="item.value" border>{{ item.label }}</el-radio>
</el-radio-group>
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="24" class="mb20">
<el-form-item label="问题" prop="content">
<el-input v-model="form.content" type="textarea" placeholder="请输入需要召回的问题内容" maxlength="1024" show-word-limit rows="6" />
</el-form-item>
</el-col>
</el-row>
<el-divider content-position="center">召回结果</el-divider>
<el-table v-if="tableData.length > 0" :data="tableData" border style="width: 100%">
<el-table-column type="index" label="序号" width="60" />
<el-table-column prop="score" label="相似度" width="120" />
<el-table-column prop="content" label="切片" :show-overflow-tooltip="true" />
<el-table-column prop="metadata" label="元数据" width="200" :show-overflow-tooltip="true" />
</el-table>
<el-empty v-else description="暂无数据" />
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="visible = false">取消</el-button>
<el-button type="primary" @click="handleTest" :disabled="loading || !form.content">测试</el-button>
</span>
</template>
</el-drawer>
</template>
<script setup lang="ts" name="AiDatasetRecall">
import { useMessage } from '/@/hooks/message';
import { fetchRecall } from '/@/api/knowledge/aiSlice';
import { useDict } from '/@/hooks/dict';
// 定义接口
interface RecallParams {
datasetId: string;
content: string;
topK: number;
fullTextSearch: boolean;
reRanking: boolean;
}
// 定义变量内容
const visible = ref(false);
const loading = ref(false);
const tableData = ref([]);
const dataset = ref<any>(null);
// 定义字典
const { yes_no_type } = useDict('yes_no_type');
// 定义表单变量
interface FormState {
datasetId: string;
content: string;
topK: number;
fullTextSearch: string;
reRanking: string;
}
const form = reactive<FormState>({
datasetId: '',
content: '',
topK: 5,
fullTextSearch: '1', // 是否全文检索
reRanking: '1', // 是否重排
});
// 表单校验规则
const dataRules = ref({
content: [{ required: true, message: '请输入问题内容', trigger: 'blur' }],
});
// 打开抽屉
const openDialog = (datasetVal: any) => {
visible.value = true;
form.datasetId = datasetVal.id;
dataset.value = datasetVal;
resetForm();
};
// 重置表单
const resetForm = () => {
form.content = '';
tableData.value = [];
};
// 测试召回
const handleTest = async () => {
if (!form.content) {
useMessage().warning('请输入问题内容');
return;
}
loading.value = true;
try {
const { data } = await fetchRecall({
datasetId: form.datasetId,
content: form.content,
topK: form.topK,
fullTextSearch: form.fullTextSearch === '1',
reRanking: form.reRanking === '1',
} as RecallParams);
if (data) {
tableData.value = data;
} else {
tableData.value = [];
useMessage().warning('未查询到数据');
}
} catch (error: any) {
useMessage().error(error.msg || '操作失败');
tableData.value = [];
} finally {
loading.value = false;
}
};
// 暴露方法
defineExpose({
openDialog,
});
</script>
<style lang="scss" scoped>
:deep(.el-drawer__body) {
padding: 20px;
overflow: auto;
}
</style>