Commit ed3948ed authored by nick zheng's avatar nick zheng

feat: 知识库支持问答知识库

parent f9bb6cc4
import { request } from '@/utils/request'
import qs from 'qs'
/* 知识库部分开始 */
......@@ -164,3 +165,86 @@ export function fetchOpenKnowledgeChunk<T>(payload: { kdId: number; chunkRelatio
return request.post<T>('/knowledgeRest/openKnowledgeChunk.json', payload)
}
/* 知识库文档部分结束 */
/* 问答知识库文档片段开始 */
/**
* @query kdId 知识库文档Id
* @returns 获取问答知识库分片结构
*/
export function fetchQAKnowledgeChunkStruct<T>(kdId: number) {
return request.post<T>(`/qAKnowledgeRest/getKnowledgeStruct.json?kdId=${kdId}`)
}
/**
* @query query 模糊搜索
* @query kdId 知识库文档Id
* @params payload { page: number, pageSize: number }
* @returns 获取问答知识库分片列表
*/
export function fetchQAKnowledgeChunkList<T>(query: string, kdId: number, payload: object) {
return request.post<T>(`/qAKnowledgeRest/getQAKnowledgeChunks.json?query=${query}&kdId=${kdId + ''}`, payload)
}
/**
* @query kdId 知识库文档Id structId 结构Id structName 结构名称 isIndex 是否开启索引
* @returns 修改问答知识库表结构
*/
export function fetchUpdateQAKnowledgeStruct<T>(payload: object) {
return request.post<T>(
'/qAKnowledgeRest/updateKnowledgeChunk.json',
{},
{
params: payload,
paramsSerializer: function (params) {
return qs.stringify(params, { arrayFormat: 'repeat' })
},
},
)
}
/**
* @params { kdId 知识库文档Id chunkInfos 分片内容 chunkSort 文档顺序 }
* @returns 添加问答知识库分片
*/
export function fetchAddQAKnowledgeChunk<T>(payload: object) {
return request.post<T>('/qAKnowledgeRest/addKnowledgeChunk.json', payload)
}
/**
* @params { kdId 知识库文档Id chunkRelationId 分片Id chunkInfos 分片内容 chunkSort 文档顺序 }
* @returns 编辑问答知识库分片
*/
export function fetchEditQAKnowledgeChunk<T>(payload: object) {
return request.post<T>('/qAKnowledgeRest/batchUpdateKnowledgeChunkDoc.json', payload, {
timeout: 15000,
})
}
/**
* @payload kdId 知识库文档Id chunkRelationId 分片Id
* @returns 删除问答知识库分片
*/
export function fetchDeleteQAKnowledgeChunk<T>(payload: { kdId: number; chunkRelationId: string }) {
return request.post<T>('/qAKnowledgeRest/deleteKnowledgeChunk.json', payload)
}
/**
* @payload kdId 知识库文档Id chunkRelationId 分片Id isOpen 开启状态
* @returns 问答知识库分片开启
*/
export function fetchOpenQAKnowledgeChunk<T>(payload: { kdId: number; chunkRelationId: string; isOpen: 'Y' | 'N' }) {
return request.post<T>('/qAKnowledgeRest/openKnowledgeChunk.json', payload)
}
/**
* @query kdId 知识库文档Id chunkRelationIds 分片Id数组
* @returns 批量删除问答知识库分片
*/
export function fetchBatchDeleteQAKnowledgeChunks<T>(kdId: number, chunkRelationIds: string[]) {
return request.post<T>(
`/qAKnowledgeRest/batchDeleteKnowledgeChunks.json?kdId=${kdId}&chunkRelationIds=${chunkRelationIds}`,
)
}
/* 知识库文档部分结束 */
export enum KnowledgeTypeIcon {
Base = 'https://gsst-poe-sit.gz.bcebos.com/icon/knowledge-Base.svg',
QA = 'https://gsst-poe-sit.gz.bcebos.com/icon/knowledge-QA.svg',
}
......@@ -25,7 +25,7 @@ common_module:
available: 'Usable'
studying: 'Studying'
config: 'Configure'
agent: 'Apply'
agent: 'Agent'
knowledge: 'Knowledge base'
custom: 'Custom'
char: 'Character'
......@@ -34,7 +34,7 @@ common_module:
upload_success_message: 'Upload successfully'
empty_data: 'No data available'
search_empty_data: 'No related content found'
create_agent_btn_text: 'Create an application'
create_agent_btn_text: 'Create agent'
copy_success_message: 'Successful replication'
delete_success_message: 'Successfully deleted'
save_success_message: 'Save successfully'
......@@ -238,7 +238,7 @@ home_module:
personal_space_module:
title: 'Personal space'
create_btn_text: 'Newly build'
create_btn_text: 'Create'
agent_module:
agent_list_module:
......@@ -465,9 +465,11 @@ personal_space_module:
knowledge_type_rule: 'Please select the data set type'
knowledge_name_rule: 'Please enter a data set name'
knowledge_import_type_rule: 'Select a data set import method'
knowledge_document_text_type: 'Text format'
base_knowledge_type: 'Text format'
import_local_document_knowledge: 'Local document'
import_local_document_knowledge_desc: 'Supports uploading local files in TXT, MD, PDF, DOC, DOCX formats'
QA_knowledge_type: 'Q&A knowledge'
import_QA_local_document_knowledge_desc: 'Upload local documents in xls and xlsx formats, suitable for Q&A scenarios'
segment: 'Segment'
auto_segment: 'Automatic segmentation'
......@@ -489,6 +491,17 @@ personal_space_module:
English_exclamation_mark: 'English exclamation mark'
ellipsis: 'Ellipsis'
index: 'Index'
no: 'No'
add_QA: 'Add Q&A'
add_QA_data: 'Add Q&A data'
edit_QA_data: 'Edit Q&A data'
setting_index_desc: 'The index is an important key field of the knowledge base search slice. Select at least one field as the index.'
select_at_least_one_field_as_the_index: 'Select at least one field as the index'
setting_index: 'Setting Index'
field_name: 'Field name'
please_enter_the_content: 'Please enter the content'
upload_document_module:
segment_setting: 'Segmented processing'
data_process: 'Data processing'
......@@ -514,6 +527,10 @@ personal_space_module:
segment_overlap_word_proportion_tip: 'The ratio of the "number of overlapping characters" between the current slice and the previous slice to the set "Maximum length of slice". If there are incomplete sentences in the overlapping part, this slice deletes the sentence, the larger the proportion, the more overlapping characters in the adjacent slice, the smaller the proportion, the fewer overlapping characters, and the maximum bit 25'
please_input_segment_overlap_word_proportion: 'Please input segment overlap word proportion'
data_process_tip_message: 'Click confirm does not affect the data processing, after processing can be referenced'
upload_QA_limit_tip_message: 'Support XLSX\XLS, up to 5 files can be uploaded, each file does not exceed 10MB'
upload_QA_suggest_tip_message: 'It is recommended to use the "Q&A Template" to upload, otherwise the parsing will fail. Click below to download the template'
upload_QA_format_error_message: 'Only xls and xlsx files can be uploaded. Please upload them again'
download_QA_template: 'Download Q&A Template'
share_agent_module:
please: 'Please first'
......
......@@ -463,9 +463,11 @@ personal_space_module:
knowledge_type_rule: '请选择数据集类型'
knowledge_name_rule: '请输入数据集名称'
knowledge_import_type_rule: '请选择数据集导入方式'
knowledge_document_text_type: '文本格式'
base_knowledge_type: '文本格式'
import_local_document_knowledge: '本地文档'
import_local_document_knowledge_desc: '支持上传TXT、MD、PDF、DOC、DOCX格式的本地文件'
QA_knowledge_type: '问答知识库'
import_QA_local_document_knowledge_desc: '支持上传xls,xlsx格式的本地文档,适合问答场景使用'
segment: '分段'
auto_segment: '自动分段'
......@@ -487,6 +489,17 @@ personal_space_module:
English_exclamation_mark: '英文感叹号'
ellipsis: '省略号'
index: '索引'
no: '序列号'
add_QA: '新建问答'
add_QA_data: '添加问答数据'
edit_QA_data: '编辑问答数据'
setting_index_desc: '索引是知识库检索切片的重要关键字段,至少选择1个字段作为索引'
select_at_least_one_field_as_the_index: '至少选择1个字段作为索引'
setting_index: '设置索引'
field_name: '字段名称'
please_enter_the_content: '请输入内容'
upload_document_module:
segment_setting: '分段处理'
data_process: '数据处理'
......@@ -512,6 +525,10 @@ personal_space_module:
segment_overlap_word_proportion_tip: '当前切片与前后切片的“重叠部分字符数”相较于设置的“切片最大长度”的比例,如果重叠部分存在不完整的句子,则此切片舍去该句,占比越大,相邻切片重叠字符越多,占比越少,重叠字符越少,最大值位25'
please_input_segment_overlap_word_proportion: '请输入分段重叠字数占比'
data_process_tip_message: '点击确认不影响数据处理,处理完毕后可进行引用'
upload_QA_limit_tip_message: '支持XLSX\XLS,最多可上传5个文件,每个文件不超过10MB'
upload_QA_suggest_tip_message: '建议使用【问答模版】上传,否则会解析失败,点击下方下载模版'
upload_QA_format_error_message: '只能上传xls,xlsx格式文件,请重新上传'
download_QA_template: '下载问答模版'
share_agent_module:
please: '请先'
......@@ -697,4 +714,3 @@ editor_module:
center_align: '居中对齐'
justify_right: '右对齐'
align_both_ends: '两端对齐'
......@@ -463,9 +463,11 @@ personal_space_module:
knowledge_type_rule: '請選擇數據集類型'
knowledge_name_rule: '請輸入數據集名稱'
knowledge_import_type_rule: '請選擇數據集導入方式'
knowledge_document_text_type: '文本格式'
base_knowledge_type: '文本格式'
import_local_document_knowledge: '本地文檔'
import_local_document_knowledge_desc: '支持上傳TXT、MD、PDF、DOC、DOCX格式的本地文件'
QA_knowledge_type: '問答知識庫'
import_QA_local_document_knowledge_desc: '支持上傳xls,xlsx格式的本地文檔,適合問答場景使用'
segment: '分段'
auto_segment: '自動分段'
......@@ -487,6 +489,17 @@ personal_space_module:
English_exclamation_mark: '英文感嘆號'
ellipsis: '省略號'
index: '索引'
no: '序列號'
add_QA: '新建問答'
add_QA_data: '添加問答數據'
edit_QA_data: '編輯問答數據'
setting_index_desc: '索引是知識庫檢索切片的重要關鍵字段,至少選擇1個字段作為索引'
select_at_least_one_field_as_the_index: '至少選擇1個字段作為索引'
setting_index: '設置索引'
field_name: '字段名稱'
please_enter_the_content: '請輸入內容'
upload_document_module:
segment_setting: '分段處理'
data_process: '數據處理'
......@@ -512,6 +525,10 @@ personal_space_module:
segment_overlap_word_proportion_tip: '當前切片與前後切片的“重疊部分字符數”相較於設置的“切片最大長度”的比例,如果重疊部分存在不完整的句子,則此切片捨去該句,佔比越大,相鄰切片重疊字符越多,佔比越少,重疊字符越少,最大值位25'
please_input_segment_overlap_word_proportion: '請輸入分段重疊字數佔比'
data_process_tip_message: '點擊確認不影響數據處理,處理完畢後可進行引用'
upload_QA_limit_tip_message: '支持XLSX\XLS,最多可上傳5個文件,每個文件不超過10MB'
upload_QA_suggest_tip_message: '建議使用【問答模版】上傳,否則會解析失敗,點擊下方下載模版'
upload_QA_format_error_message: '只能上傳xls,xlsx格式文件,請重新上傳'
download_QA_template: '下載問答模版'
share_agent_module:
please: '請先'
......
......@@ -67,7 +67,7 @@ export default [
title: 'router_title_module.knowledge_document_detail',
belong: 'PersonalSpace',
},
component: () => import('@/views/personal-space/personal-knowledge/document-detail.vue'),
component: () => import('@/views/personal-space/personal-knowledge/knowledge-detail/knowledge-detail.vue'),
},
{
......
export function downloadFile(fileUrl: string, name: string) {
const a = document.createElement('a')
a.download = name || 'file'
a.href = fileUrl
a.click()
a.remove()
}
......@@ -12,6 +12,7 @@ import { formatDateTime } from '@/utils/date-formatter'
import CreateKnowledgeModal, {
KnowledgeFormDataInterface,
} from '@/views/personal-space/personal-knowledge/components/create-knowledge-modal.vue'
import { KnowledgeTypeIcon } from '@/enums/knowledge'
interface Props {
isShowModal: boolean
......@@ -202,7 +203,7 @@ async function handleCreateKnowledgeNextStep(createKnowledgeData: KnowledgeFormD
key-field="id"
items-style="padding-right: 12px;"
>
<template #default="{ item }">
<template #default="{ item }: { item: KnowledgeItem }">
<div
:key="item.id"
class="border-inactive-border-color hover:border-theme-color mb-4.5 flex h-[118px] w-full cursor-pointer items-center gap-4 rounded-[10px] border px-4 py-3.5"
......@@ -211,7 +212,8 @@ async function handleCreateKnowledgeNextStep(createKnowledgeData: KnowledgeFormD
<div class="flex flex-1 flex-col overflow-hidden">
<div class="flex flex-1">
<div
class="mt-[3px] h-9 w-9 flex-shrink-0 bg-[url(https://gsst-poe-sit.gz.bcebos.com/data/20241106/1730873304749.png)] bg-contain"
class="mt-[3px] h-9 w-9 flex-shrink-0 bg-contain"
:style="{ backgroundImage: `url(${KnowledgeTypeIcon[item.knowledgeType]})` }"
/>
<div class="ml-3.5 flex flex-col items-start overflow-hidden">
......
......@@ -4,6 +4,7 @@ import { FormInst } from 'naive-ui'
import { useI18n } from 'vue-i18n'
import { FileAddition } from '@icon-park/vue-next'
import CustomModal from '@/components/custom-modal/custom-modal.vue'
import { KnowledgeTypeIcon } from '@/enums/knowledge'
const { t } = useI18n()
......@@ -30,7 +31,7 @@ const emit = defineEmits<Emits>()
const createKnowledgeFormRef = ref<FormInst | null>(null)
const defaultKnowledgeFormData = {
knowledgeType: 'text',
knowledgeType: 'Base',
knowledgeName: '',
knowledgeDesc: '',
knowledgeImportType: 'local-document',
......@@ -61,9 +62,15 @@ const createKnowledgeFormDataRules = {
const knowledgeTypeList = readonly([
{
id: 'KJ7rCkq3HNOk',
icon: 'https://gsst-poe-sit.gz.bcebos.com/data/20241012/1728700558225.png',
type: 'text',
name: 'personal_space_module.knowledge_module.knowledge_document_text_type',
icon: KnowledgeTypeIcon.Base,
type: 'Base',
name: t('personal_space_module.knowledge_module.base_knowledge_type'),
},
{
id: 'O7Y9BsSm5J5j',
icon: KnowledgeTypeIcon.QA,
type: 'QA',
name: t('personal_space_module.knowledge_module.QA_knowledge_type'),
},
])
......@@ -71,12 +78,25 @@ const importKnowledgeFileList = readonly([
{
id: 'ewwBbe9di5Ym',
icon: () => h(FileAddition, { theme: 'outline', size: '16', fill: '#333' }),
knowledgeType: 'Base',
type: 'local-document',
title: t('personal_space_module.knowledge_module.import_local_document_knowledge'),
desc: t('personal_space_module.knowledge_module.import_local_document_knowledge_desc'),
},
{
id: 'M8BMXe2tOIlB',
icon: () => h(FileAddition, { theme: 'outline', size: '16', fill: '#333' }),
knowledgeType: 'QA',
type: 'local-document',
title: 'personal_space_module.knowledge_module.import_local_document_knowledge',
desc: 'personal_space_module.knowledge_module.import_local_document_knowledge_desc',
title: t('personal_space_module.knowledge_module.import_local_document_knowledge'),
desc: t('personal_space_module.knowledge_module.import_QA_local_document_knowledge_desc'),
},
])
const currentKnowledgeTypeList = computed(() => {
return importKnowledgeFileList.filter((item) => item.knowledgeType === createKnowledgeFormData.value.knowledgeType)
})
const showModal = computed({
get() {
return props.isShowModal
......@@ -133,13 +153,14 @@ function handleNext() {
:key="knowledgeTypeItem.id"
:class="
createKnowledgeFormData.knowledgeType === knowledgeTypeItem.type
? 'border-theme-color text-theme-color bg-[#ecedfa]'
? 'border-theme-color text-theme-color'
: 'text-font-color bg-white'
"
class="hover:border-theme-color hover:text-theme-color flex h-[66px] cursor-pointer flex-col items-center justify-center rounded-lg border"
@click="createKnowledgeFormData.knowledgeType = knowledgeTypeItem.type"
>
<img :src="knowledgeTypeItem.icon" class="h-6 w-6" />
<span class="mt-2 text-sm leading-[14px]">{{ t(knowledgeTypeItem.name) }}</span>
<span class="mt-2 text-sm leading-[14px]">{{ knowledgeTypeItem.name }}</span>
</div>
</div>
</div>
......@@ -169,22 +190,22 @@ function handleNext() {
>
<div class="grid w-full grid-cols-2 gap-2">
<div
v-for="importKnowledgeFileItem in importKnowledgeFileList"
v-for="importKnowledgeFileItem in currentKnowledgeTypeList"
:key="importKnowledgeFileItem.id"
class="border-inactive-border-color flex h-[64px] flex-col items-start justify-center rounded-lg border bg-[#FAFAFA] px-[14px]"
>
<div class="flex items-center">
<component :is="importKnowledgeFileItem.icon()" />
<span class="text-font-color ml-1 line-clamp-1">{{ t(importKnowledgeFileItem.title) }}</span>
<span class="text-font-color ml-1 line-clamp-1">{{ importKnowledgeFileItem.title }}</span>
</div>
<NPopover trigger="hover">
<template #trigger>
<span class="text-gray-font-color mt-1.5 line-clamp-1 text-xs">
{{ t(importKnowledgeFileItem.desc) }}
{{ importKnowledgeFileItem.desc }}
</span>
</template>
<span>{{ t(importKnowledgeFileItem.desc) }}</span>
<span>{{ importKnowledgeFileItem.desc }}</span>
</NPopover>
</div>
</div>
......
......@@ -9,6 +9,7 @@ const { t } = useI18n()
export interface KnowledgeFormDataInterface {
knowledgeName: string
desc: string
knowledgeType: 'Base' | 'QA'
}
interface Props {
......@@ -28,6 +29,7 @@ const emit = defineEmits<Emits>()
const defaultKnowledgeFormData = {
knowledgeName: '',
desc: '',
knowledgeType: 'Base' as const,
}
const knowledgeFormRef = ref<FormInst | null>(null)
......
<script setup lang="ts">
import { computed, onMounted, ref } from 'vue'
import { useRouter } from 'vue-router'
import { useI18n } from 'vue-i18n'
import { ScrollbarInst } from 'naive-ui'
import { Left, Search } from '@icon-park/vue-next'
import { KnowledgeChunkItem, KnowledgeDocumentItem } from './types.d'
import { KnowledgeChunkItem, KnowledgeDocumentItem } from '../../types'
import KnowledgeChuckItem from './components/knowledge-chuck-item.vue'
import {
fetchAddKnowledgeChunk,
fetchDeleteKnowledgeChunk,
fetchGetKnowledgeChunkList,
fetchGetKnowledgeDocumentListByKdIds,
fetchOpenKnowledgeChunk,
fetchUpdateKnowledgeChunk,
} from '@/apis/knowledge'
......@@ -19,16 +17,20 @@ import { usePagination } from '@/composables/usePagination.ts'
import EditKnowledgeChunkModal from './components/edit-knowledge-chunk-modal.vue'
import AddKnowledgeChunkModal from './components/add-knowledge-chunk-modal.vue'
const { t } = useI18n()
interface Props {
kdId: number
knowledgeDocument: Pick<KnowledgeDocumentItem, 'documentUrl' | 'documentName' | 'segmentationConfig'>
}
const router = useRouter()
const props = defineProps<Props>()
const { paginationData } = usePagination()
const emit = defineEmits<{
backList: []
}>()
const { t } = useI18n()
const currentKdId = ref(0)
const currentKnowledgeDocumentName = ref('')
const currentKnowledgeDocumentUrl = ref('')
const currentKnowledgeSegmentationType = ref('')
const { paginationData } = usePagination()
const scrollBarRef = ref<ScrollbarInst | null>(null)
......@@ -63,38 +65,15 @@ const emptyKnowledgeChunkListText = computed(() => {
})
onMounted(async () => {
if (!router.currentRoute.value.params.kdId) {
window.$message.warning(t('personal_space_module.knowledge_module.not_find_knowledge_document_message'))
router.replace({ name: 'PersonalSpaceKnowledge' })
return
}
currentKdId.value = Number(router.currentRoute.value.params.kdId)
await handleGetKnowledgeDocumentDetail()
await handleGetKnowledgeChunkList()
props.kdId && (await handleGetKnowledgeChunkList())
})
function handleGetKnowledgeDocumentDetail() {
fetchGetKnowledgeDocumentListByKdIds<KnowledgeDocumentItem[]>([currentKdId.value])
.then((res) => {
if (res.code === 0) {
currentKnowledgeDocumentName.value = res.data[0].documentName
currentKnowledgeDocumentUrl.value = res.data[0].documentUrl
currentKnowledgeSegmentationType.value = res.data[0]?.segmentationConfig?.segmentationType
}
})
.catch(() => {
window.$message.warning(t('personal_space_module.knowledge_module.not_find_knowledge_document_message'))
router.replace({ name: 'PersonalSpaceKnowledge' })
})
}
async function handleGetKnowledgeChunkList() {
knowledgeChunkListLoading.value = true
const res = await fetchGetKnowledgeChunkList<{ totalChunk: number; chunkInfos: KnowledgeChunkItem[] }>(
searchKnowledgeChunkValue.value,
currentKdId.value,
props.kdId,
{
pagingInfo: paginationData,
},
......@@ -111,7 +90,7 @@ async function handleGetKnowledgeChunkList() {
}
function handleBackKnowledgeDocumentList() {
router.replace({ name: 'KnowledgeDocument', params: { id: router.currentRoute.value.params.id } })
emit('backList')
}
async function handleSearchKnowledgeChunkList() {
......@@ -129,7 +108,7 @@ async function handleUpdateKnowledgeChunk(chunkRelationId: string, chunkContent:
updateKnowledgeChunkBtnLoading.value = true
const res = await fetchUpdateKnowledgeChunk({
kdId: currentKdId.value,
kdId: props.kdId,
chunkRelationId,
chunkContent,
}).finally(() => (updateKnowledgeChunkBtnLoading.value = false))
......@@ -155,7 +134,7 @@ async function handleAddKnowledgeChunk(chunkContent: string, chunkSort: number)
addKnowledgeChunkBtnLoading.value = true
const res = await fetchAddKnowledgeChunk({
kdId: currentKdId.value,
kdId: props.kdId,
chunkContent,
chunkSort,
}).finally(() => {
......@@ -174,7 +153,7 @@ async function handleDeleteKnowledgeChunk(chunkRelationId: string) {
.ctWarning('', t('personal_space_module.knowledge_module.delete_knowledge_chunk_content_message'))
.then(async () => {
const res = await fetchDeleteKnowledgeChunk({
kdId: currentKdId.value,
kdId: props.kdId,
chunkRelationId,
})
......@@ -191,7 +170,7 @@ async function handleUpdateOpenKnowledgeChunk(chunkItem: KnowledgeChunkItem) {
const res = await fetchOpenKnowledgeChunk({
chunkRelationId,
isOpen: isOpen === 'Y' ? 'N' : 'Y',
kdId: currentKdId.value,
kdId: props.kdId,
})
if (res.code === 0) {
......@@ -214,31 +193,29 @@ async function handleGetKnowledgeChunkListUpdatePageSize(pageSize: number) {
</script>
<template>
<div class="flex h-full flex-col overflow-hidden">
<div class="my-6">
<div class="flex h-full w-full flex-col overflow-hidden">
<div class="mb-4.5 gap-4.5 mt-6 flex items-center">
<div class="flex h-9 items-center">
<Left theme="outline" size="20" fill="#333" class="cursor-pointer" @click="handleBackKnowledgeDocumentList" />
<img :src="documentIcon(currentKnowledgeDocumentUrl)" class="ml-[2px] h-9 w-9" />
<img :src="documentIcon(knowledgeDocument.documentUrl)" class="ml-[2px] h-9 w-9" />
<span class="text-font-color ml-[5px] line-clamp-1 select-none break-all text-lg">
{{ currentKnowledgeDocumentName }}
{{ knowledgeDocument.documentName }}
</span>
</div>
<div class="ml-[66px] mt-1">
<ul class="flex gap-2">
<li class="border-inactive-border-color select-none rounded-full border px-4 py-1 leading-[18px]">
{{ totalChunk }}
{{ t('personal_space_module.knowledge_module.segment') }}
</li>
<li class="border-inactive-border-color select-none rounded-full border px-4 py-1 leading-[18px]">
{{
currentKnowledgeSegmentationType === 'DEFAULT'
? t('personal_space_module.knowledge_module.auto_segment')
: t('personal_space_module.knowledge_module.custom_segment')
}}
</li>
</ul>
</div>
<ul class="flex flex-shrink-0 gap-2">
<li class="border-inactive-border-color select-none rounded-full border px-4 py-1 leading-[18px]">
{{ totalChunk }}
{{ t('personal_space_module.knowledge_module.segment') }}
</li>
<li class="border-inactive-border-color select-none rounded-full border px-4 py-1 leading-[18px]">
{{
knowledgeDocument.segmentationConfig.segmentationType === 'DEFAULT'
? t('personal_space_module.knowledge_module.auto_segment')
: t('personal_space_module.knowledge_module.custom_segment')
}}
</li>
</ul>
</div>
<div class="mb-[18px] flex justify-end">
......
......@@ -3,7 +3,7 @@ import { computed, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import { FormInst } from 'naive-ui'
import CustomModal from '@/components/custom-modal/custom-modal.vue'
import { KnowledgeChunkItem } from '../knowledge-type'
import { KnowledgeChunkItem } from '../../../types'
interface Props {
isShowModal: boolean
......
......@@ -3,7 +3,7 @@ import { computed, ref, watch } from 'vue'
import { FormInst } from 'naive-ui'
import { useI18n } from 'vue-i18n'
import CustomModal from '@/components/custom-modal/custom-modal.vue'
import { KnowledgeChunkItem } from '../knowledge-type'
import { KnowledgeChunkItem } from '../../../types'
interface Props {
isShowModal: boolean
......
<script setup lang="ts">
import { ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { KnowledgeChunkItem } from '../types.d'
import { KnowledgeChunkItem } from '../../../types.d'
interface Props {
chunkItem: KnowledgeChunkItem
......@@ -74,11 +74,11 @@ const isShowKnowledgeChunkAction = ref(false)
<span> {{ t('common_module.data_table_module.delete') }}</span>
</n-popover>
<n-switch :value="chunkItem.isOpen === 'Y'" @update:value="emit('updateOpen', chunkItem)" />
<n-switch :value="chunkItem.isOpen === 'Y'" size="small" @update:value="emit('updateOpen', chunkItem)" />
</div>
</div>
<div class="text-font-color mt-[14px] whitespace-pre-wrap">
<div class="text-font-color mt-[14px] whitespace-pre-wrap break-all">
{{ chunkItem.chunkContent }}
</div>
</div>
......
import { NEllipsis, NPopover, NSwitch } from 'naive-ui'
import i18n from '@/locales'
import { QAKnowledgeChunkContentItem, QAKnowledgeChunkKeyItem } from '../types'
const t = i18n.global.t
export function createQAKnowledgeChunkColumn(
chunkColumnKey: QAKnowledgeChunkKeyItem[],
handleKnowledgeChunkTableAction: (
actionType: string,
chunkRelationId: string,
KnowledgeItem?: QAKnowledgeChunkContentItem,
) => void,
) {
return [
{
type: 'selection',
fixed: 'left',
className: 'qa-knowledge-chunk-selection',
},
{
title: () => <span class='font-family-medium'>{t('personal_space_module.knowledge_module.no')}</span>,
key: 'key',
minWidth: 92,
className: 'qa-knowledge-chunk-index',
render: (_: any, index: number) => {
return `${index + 1}`
},
},
...chunkColumnKey.map((item) => {
return {
title: () => {
return (
<div class='flex items-center gap-[10px]'>
<NEllipsis class='font-family-medium max-w-full' tooltip={{ width: 200 }}>
{item.structName}
</NEllipsis>
{item.isIndex === 'Y' && (
<span class='border-theme-color text-theme-color rounded-theme flex-shrink-0 border px-[11px] py-[2px] text-xs'>
{t('personal_space_module.knowledge_module.index')}
</span>
)}
</div>
)
},
key: item.structId,
align: 'left',
width: 246,
className: 'qa-knowledge-chunk-item',
render(row: QAKnowledgeChunkContentItem & { expanded: boolean }) {
return (
<div
class={[row.expanded ? '' : 'line-clamp-1', 'cursor-pointer']}
onClick={() => {
row.expanded = !row.expanded
}}
>
{row.chunkInfo.find((chunkInfo) => chunkInfo.structId === item.structId)?.content || '--'}
</div>
)
},
}
}),
{
title: '',
key: 'action',
align: '',
fixed: 'right',
width: 0,
render(row: QAKnowledgeChunkContentItem) {
return (
<div class='top-4.5 flex-center absolute right-[24px] z-10 gap-[10px]'>
<NPopover
v-slots={{
trigger: () => (
<i
class='iconfont icon-edit-document hover:text-font-color hover:bg-background-color flex h-6 w-6 cursor-pointer items-center justify-center rounded-full text-sm'
onClick={() => {
handleKnowledgeChunkTableAction('edit', row.chunkRelationId, row)
}}
/>
),
}}
>
<span>{t('common_module.data_table_module.edit')}</span>
</NPopover>
<NPopover
v-slots={{
trigger: () => (
<i
class='iconfont icon-add-chunk-up hover:text-font-color hover:bg-background-color flex h-6 w-6 cursor-pointer items-center justify-center rounded-full text-sm'
onClick={() => {
handleKnowledgeChunkTableAction('addUp', row.chunkRelationId, row)
}}
/>
),
}}
>
<span>{t('personal_space_module.knowledge_module.add_chunk_up_message')}</span>
</NPopover>
<NPopover
v-slots={{
trigger: () => (
<i
class='iconfont icon-add-chunk-up hover:text-font-color hover:bg-background-color flex h-6 w-6 rotate-180 cursor-pointer items-center justify-center rounded-full text-sm'
onClick={() => {
handleKnowledgeChunkTableAction('addDown', row.chunkRelationId, row)
}}
/>
),
}}
>
<span>{t('personal_space_module.knowledge_module.add_chunk_down_message')}</span>
</NPopover>
<NPopover
v-slots={{
trigger: () => (
<i
class='iconfont icon-delete hover:text-font-color hover:bg-background-color flex h-6 w-6 cursor-pointer items-center justify-center rounded-full text-sm'
onClick={() => {
handleKnowledgeChunkTableAction('delete', row.chunkRelationId)
}}
/>
),
}}
>
<span>{t('common_module.delete')}</span>
</NPopover>
<NSwitch
value={row.isOpen === 'Y'}
size='small'
onUpdateValue={() => handleKnowledgeChunkTableAction('updateOpen', row.chunkRelationId, row)}
/>
</div>
)
},
},
]
}
<script setup lang="ts">
import { onMounted, ref } from 'vue'
import { useRouter } from 'vue-router'
import { useI18n } from 'vue-i18n'
import { fetchGetKnowledgeDocumentListByKdIds } from '@/apis/knowledge'
import { KnowledgeDocumentItem } from '../types'
import BaseKnowledgeChunkDetail from './base-knowledge/base-knowledge-chunk-detail.vue'
import QAKnowledgeChunkDetail from './qa-knowledge/qa-knowledge-chunk-detail.vue'
const { t } = useI18n()
const router = useRouter()
const currentKdId = ref(0)
const currentKnowledgeDocument = ref<
Pick<KnowledgeDocumentItem, 'documentUrl' | 'documentName' | 'segmentationConfig'>
>({
documentName: '',
documentUrl: '',
segmentationConfig: {
segmentationType: 'DEFAULT',
chunkSize: 0,
scrapSize: 0,
repetitionRate: 0,
relationInfo: [],
regex: '',
punctuations: [],
},
})
const currentKnowledgeType = ref()
onMounted(async () => {
if (!router.currentRoute.value.params.kdId) {
window.$message.warning(t('personal_space_module.knowledge_module.not_find_knowledge_document_message'))
router.replace({ name: 'PersonalSpaceKnowledge' })
return
}
currentKdId.value = Number(router.currentRoute.value.params.kdId)
await handleGetKnowledgeDocumentDetail()
})
function handleGetKnowledgeDocumentDetail() {
fetchGetKnowledgeDocumentListByKdIds<KnowledgeDocumentItem[]>([currentKdId.value])
.then((res) => {
if (res.code === 0) {
currentKnowledgeDocument.value = res.data?.[0]
currentKnowledgeType.value = res.data?.[0]?.knowledgeType || 'Base'
}
})
.catch(() => {
window.$message.warning(t('personal_space_module.knowledge_module.not_find_knowledge_document_message'))
router.replace({ name: 'PersonalSpaceKnowledge' })
})
}
function handleBackKnowledgeDocumentList() {
router.replace({ name: 'KnowledgeDocument', params: { id: router.currentRoute.value.params.id } })
}
</script>
<template>
<div class="flex h-full flex-col overflow-hidden">
<div v-if="currentKnowledgeType === 'BASE'" class="flex h-full w-full">
<BaseKnowledgeChunkDetail
:kd-id="currentKdId"
:knowledge-document="currentKnowledgeDocument"
@back-list="handleBackKnowledgeDocumentList"
/>
</div>
<div v-if="currentKnowledgeType === 'QA'" class="flex h-full w-full">
<QAKnowledgeChunkDetail
:kd-id="currentKdId"
:knowledge-document="currentKnowledgeDocument"
@back-list="handleBackKnowledgeDocumentList"
/>
</div>
</div>
</template>
<script setup lang="ts">
import { computed, ref, watch } from 'vue'
import { FormInst } from 'naive-ui'
import { useI18n } from 'vue-i18n'
import CustomModal from '@/components/custom-modal/custom-modal.vue'
import { QAKnowledgeChunkKeyItem, QAKnowledgeChunkInfoItem } from '../../../types'
type QAKnowledgeChunkStructItem = QAKnowledgeChunkKeyItem & { content: string }
interface Props {
btnLoading: boolean
knowledgeChunkKeyList: QAKnowledgeChunkKeyItem[]
}
const emit = defineEmits<{
confirm: [chunkStructInfo: QAKnowledgeChunkInfoItem[]]
}>()
const props = defineProps<Props>()
const { t } = useI18n()
const isShow = defineModel<boolean>('isShow', { required: true })
const knowledgeChunkFormRef = ref<FormInst | null>(null)
const chunkStructList = ref<QAKnowledgeChunkStructItem[]>([])
const chunkStructRules = {
content: {
required: true,
trigger: 'blur',
message: t('personal_space_module.knowledge_module.please_enter_the_content'),
},
}
const dataTableScrollX = computed(() => {
return props.knowledgeChunkKeyList.length * 215
})
watch(
() => isShow.value,
(newVal) => {
if (newVal) {
chunkStructList.value = props.knowledgeChunkKeyList.map((chunkKeyItem) => {
return { ...chunkKeyItem, content: '' }
})
}
},
)
function handleAddKnowledgeChunk() {
knowledgeChunkFormRef.value?.validate((valid) => {
if (!valid) {
const newChunkStructInfo = chunkStructList.value.map((chunkItem) => {
const { structId, content } = chunkItem
return {
structId,
content,
}
})
emit('confirm', newChunkStructInfo)
}
})
}
</script>
<template>
<CustomModal
v-model:is-show="isShow"
:title="t('personal_space_module.knowledge_module.add_QA_data')"
:btn-loading="btnLoading"
:width="1000"
@confirm="handleAddKnowledgeChunk"
>
<template #content>
<n-scrollbar x-scrollable>
<n-form
ref="knowledgeChunkFormRef"
:inline="true"
:model="{ chunkStructList }"
:rules="chunkStructRules"
:style="{ width: dataTableScrollX + 'px' }"
class="rounded-theme"
>
<n-form-item
v-for="(chunkItem, index) in chunkStructList"
:key="chunkItem.structId"
class="qa-knowledge-chunk-key w-[215px]! box-border border-collapse overflow-hidden border border-r-0 border-[#e5e7eb] first:rounded-l-[10px] last:rounded-r-[10px] last:border-r"
:path="`chunkStructList[${index}].content`"
:show-require-mark="false"
feedback-class="px-3!"
:rule="chunkItem.isIndex === 'Y' ? chunkStructRules.content : []"
>
<template #label>
<div class="flex h-12 w-full flex-shrink-0 items-center border-b border-[#e5e7eb] bg-[#f7f7fa] px-3">
<span v-show="chunkItem.isIndex === 'Y'" class="mr-1 text-base text-[#d03050]">*</span>
<n-ellipsis :tooltip="{ width: '200px' }">
{{ chunkItem.structName }}
</n-ellipsis>
<span
v-show="chunkItem.isIndex === 'Y'"
class="border-theme-color text-theme-color rounded-theme ml-[10px] flex-shrink-0 border px-[11px] py-[2px] text-xs"
>
{{ t('personal_space_module.knowledge_module.index') }}
</span>
</div>
</template>
<div class="flex w-full flex-shrink-0 p-0">
<n-input
v-model:value="chunkItem.content"
type="textarea"
:placeholder="t('personal_space_module.knowledge_module.please_enter_the_content')"
class="chunk-content-input"
:autosize="{
minRows: 3,
maxRows: 3,
}"
/>
</div>
</n-form-item>
</n-form>
</n-scrollbar>
</template>
</CustomModal>
</template>
<style lang="scss" scoped>
:deep(.qa-knowledge-chunk-key.n-form-item) {
flex-shrink: 0;
width: 215px;
margin-right: 0 !important;
.n-form-item-label__text {
width: 100%;
}
.n-form-item-label {
padding: 0;
}
}
:deep(.chunk-content-input.n-input--textarea) {
--n-border: none !important;
--n-border-focus: none !important;
--n-border-hover: none !important;
--n-box-shadow-focus: none !important;
}
</style>
<script setup lang="ts">
import { computed, ref, watch } from 'vue'
import { FormInst } from 'naive-ui'
import { useI18n } from 'vue-i18n'
import CustomModal from '@/components/custom-modal/custom-modal.vue'
import { QAKnowledgeChunkKeyItem, QAKnowledgeChunkInfoItem, QAKnowledgeChunkContentItem } from '../../../types'
type QAKnowledgeChunkStructItem = QAKnowledgeChunkKeyItem & { content: string }
interface Props {
btnLoading: boolean
knowledgeChunkKeyList: QAKnowledgeChunkKeyItem[]
currentChunkItem: QAKnowledgeChunkContentItem
}
const emit = defineEmits<{
confirm: [chunkRelationId: string, chunkStructInfo: QAKnowledgeChunkInfoItem[]]
}>()
const props = defineProps<Props>()
const { t } = useI18n()
const isShow = defineModel<boolean>('isShow', { required: true })
const knowledgeChunkFormRef = ref<FormInst | null>(null)
const chunkStructList = ref<QAKnowledgeChunkStructItem[]>([])
const chunkStructRules = {
content: {
required: true,
trigger: 'blur',
message: t('personal_space_module.knowledge_module.please_enter_the_content'),
},
}
const dataTableScrollX = computed(() => {
return props.knowledgeChunkKeyList.length * 215
})
watch(
() => isShow.value,
(newVal) => {
if (newVal) {
const map = new Map(
props.currentChunkItem.chunkInfo.map((chunkInfoItem) => [chunkInfoItem.structId, chunkInfoItem]),
)
chunkStructList.value = props.knowledgeChunkKeyList.map((chunkKeyItem) => ({
...chunkKeyItem,
content: map.get(chunkKeyItem.structId)?.content || '',
}))
}
},
)
function handleAddKnowledgeChunk() {
knowledgeChunkFormRef.value?.validate((valid) => {
if (!valid) {
const newChunkStructInfo = chunkStructList.value.map((chunkItem) => {
const { structId, content } = chunkItem
return {
structId,
content,
}
})
emit('confirm', props.currentChunkItem.chunkRelationId, newChunkStructInfo)
}
})
}
</script>
<template>
<CustomModal
v-model:is-show="isShow"
:title="t('personal_space_module.knowledge_module.edit_QA_data')"
:btn-loading="btnLoading"
:width="1000"
@confirm="handleAddKnowledgeChunk"
>
<template #content>
<n-scrollbar x-scrollable>
<n-form
ref="knowledgeChunkFormRef"
:inline="true"
:model="{ chunkStructList }"
:rules="chunkStructRules"
:style="{ width: dataTableScrollX + 'px' }"
class="rounded-theme"
>
<n-form-item
v-for="(chunkItem, index) in chunkStructList"
:key="chunkItem.structId"
class="qa-knowledge-chunk-key w-[215px]! box-border border-collapse overflow-hidden border border-r-0 border-[#e5e7eb] first:rounded-l-[10px] last:rounded-r-[10px] last:border-r"
:path="`chunkStructList[${index}].content`"
:show-require-mark="false"
feedback-class="px-3!"
:rule="chunkItem.isIndex === 'Y' ? chunkStructRules.content : []"
>
<template #label>
<div class="flex h-12 w-full flex-shrink-0 items-center border-b border-[#e5e7eb] bg-[#f7f7fa] px-3">
<span v-show="chunkItem.isIndex === 'Y'" class="mr-1 text-base text-[#d03050]">*</span>
<n-ellipsis :tooltip="{ width: '200px' }">
{{ chunkItem.structName }}
</n-ellipsis>
<span
v-show="chunkItem.isIndex === 'Y'"
class="border-theme-color text-theme-color rounded-theme ml-[10px] flex-shrink-0 border px-[11px] py-[2px] text-xs"
>
{{ t('personal_space_module.knowledge_module.index') }}
</span>
</div>
</template>
<div class="flex w-full flex-shrink-0 p-0">
<n-input
v-model:value="chunkItem.content"
type="textarea"
:placeholder="t('personal_space_module.knowledge_module.please_enter_the_content')"
class="chunk-content-input"
:autosize="{
minRows: 3,
maxRows: 3,
}"
/>
</div>
</n-form-item>
</n-form>
</n-scrollbar>
</template>
</CustomModal>
</template>
<style lang="scss" scoped>
:deep(.qa-knowledge-chunk-key.n-form-item) {
flex-shrink: 0;
width: 215px;
margin-right: 0 !important;
.n-form-item-label__text {
width: 100%;
}
.n-form-item-label {
padding: 0;
}
}
:deep(.chunk-content-input.n-input--textarea) {
--n-border: none !important;
--n-border-focus: none !important;
--n-border-hover: none !important;
--n-box-shadow-focus: none !important;
}
</style>
<script setup lang="ts">
import { computed, h } from 'vue'
import { NSwitch } from 'naive-ui'
import { useI18n } from 'vue-i18n'
import CustomModal from '@/components/custom-modal/custom-modal.vue'
import { QAKnowledgeChunkKeyItem } from '../../../types'
interface Props {
kdId: number
knowledgeChunkKeyList: QAKnowledgeChunkKeyItem[]
}
const props = defineProps<Props>()
const emit = defineEmits<{
updateKnowledgeChunkIndex: [chunkKeyItem: QAKnowledgeChunkKeyItem]
confirm: []
}>()
const { t } = useI18n()
const isShow = defineModel<boolean>('isShow', { required: true })
const isHasChunkIndex = computed(() => {
return props.knowledgeChunkKeyList.some((item) => item.isIndex === 'Y')
})
const chunkIndexColumns = createChunkIndexColumns()
function createChunkIndexColumns() {
return [
{
title: () => t('personal_space_module.knowledge_module.field_name'),
key: 'structName',
render: (row: QAKnowledgeChunkKeyItem) => {
return row.structName || '-'
},
},
{
title: () => t('personal_space_module.knowledge_module.setting_index'),
key: 'isIndex',
render: (row: QAKnowledgeChunkKeyItem) => {
return h(
NSwitch,
{ value: row.isIndex === 'Y', size: 'small', onUpdateValue: () => handleKnowledgeTableAction(row) },
{},
)
},
},
]
}
function handleKnowledgeTableAction(chunkKeyItem: QAKnowledgeChunkKeyItem) {
chunkKeyItem.isIndex = chunkKeyItem.isIndex === 'Y' ? 'N' : 'Y'
if (!isHasChunkIndex.value) {
chunkKeyItem.isIndex = chunkKeyItem.isIndex === 'Y' ? 'N' : 'Y'
window.$message.warning(t('personal_space_module.knowledge_module.select_at_least_one_field_as_the_index'))
return
}
emit('updateKnowledgeChunkIndex', chunkKeyItem)
}
</script>
<template>
<CustomModal
v-model:is-show="isShow"
:title="t('personal_space_module.knowledge_module.setting_index')"
:width="644"
@confirm="emit('confirm')"
>
<template #content>
<p class="text-gray-font-color mb-2.5">
{{ t('personal_space_module.knowledge_module.setting_index_desc') }}
</p>
<n-data-table :single-line="false" :columns="chunkIndexColumns" :data="knowledgeChunkKeyList" :max-height="300" />
</template>
</CustomModal>
</template>
<script lang="ts" setup>
import { computed, onMounted, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { Left, Search } from '@icon-park/vue-next'
import { useElementSize } from '@vueuse/core'
import {
fetchAddQAKnowledgeChunk,
fetchBatchDeleteQAKnowledgeChunks,
fetchDeleteQAKnowledgeChunk,
fetchEditQAKnowledgeChunk,
fetchOpenQAKnowledgeChunk,
fetchQAKnowledgeChunkList,
fetchQAKnowledgeChunkStruct,
fetchUpdateQAKnowledgeStruct,
} from '@/apis/knowledge'
import CustomPagination from '@/components/custom-pagination/custom-pagination.vue'
import { usePagination } from '@/composables/usePagination'
import { useUserStore } from '@/store/modules/user'
import SettingKnowledgeChunkIndexModal from './components/setting-knowledge-chunk-index-modal.vue'
import AddQAKnowledgeChunkModal from './components/add-qa-knowledge-chunk-modal.vue'
import EditQAKnowledgeChunkModal from './components/edit-qa-knowledge-chunk-modal.vue'
import { createQAKnowledgeChunkColumn } from '../columns'
import {
KnowledgeDocumentItem,
QAKnowledgeChunkContentItem,
QAKnowledgeChunkKeyItem,
QAKnowledgeChunkInfoItem,
} from '../../types'
interface Props {
kdId: number
knowledgeDocument: Pick<KnowledgeDocumentItem, 'documentUrl' | 'documentName' | 'segmentationConfig'>
}
const props = defineProps<Props>()
const emit = defineEmits<{
backList: []
}>()
const { t } = useI18n()
const userStore = useUserStore()
const { paginationData } = usePagination()
const knowledgeChunkListContainer = ref<HTMLDivElement | null>(null)
const { height } = useElementSize(knowledgeChunkListContainer)
const totalChunk = ref(0) // 总分片数
const currentChunkSort = ref(0) // 当前分片排序
const showAddKnowledgeChunkModal = ref(false)
const showEditKnowledgeChunkModal = ref(false)
const knowledgeChunkBtnLoading = ref(false)
const showSettingChunkIndexModal = ref(false)
const isSearchEmptyList = ref(false)
const searchKnowledgeChunkValue = ref('')
const chunkKeyList = ref<QAKnowledgeChunkKeyItem[]>([]) // QA知识库分片表头列表
const knowledgeChunkList = ref<QAKnowledgeChunkContentItem[]>([]) // QA知识库分片内容列表
const knowledgeChunkColumnTableLoading = ref(false)
const checkedChunkIdList = ref<string[]>([])
const currentChunkItem = ref<QAKnowledgeChunkContentItem>({
chunkRelationId: '',
chunkSort: 0,
chunkInfo: [],
isOpen: 'Y',
})
const knowledgeChunkColumn = computed(() => {
return createQAKnowledgeChunkColumn(chunkKeyList.value, handleKnowledgeChunkTableAction)
})
const dataTableScrollX = computed(() => {
return chunkKeyList.value.length * 246 + 40 + 92
})
const tableContentY = computed(() => {
return height.value - 48
})
const emptyTableDataText = computed(() => {
return isSearchEmptyList.value
? t('common_module.search_empty_data')
: t('personal_space_module.knowledge_module.empty_knowledge_document_list')
})
const documentIcon = computed(() => (documentUrl: string) => {
const type = documentUrl.substring(documentUrl.lastIndexOf('.') + 1)
return `https://gsst-poe-sit.gz.bcebos.com/icon/${type}.svg`
})
const isDisabledOperateBtn = computed(() => {
return chunkKeyList.value.length <= 0
})
const isDisabledBatchDelBtn = computed(() => {
return checkedChunkIdList.value.length <= 0
})
onMounted(async () => {
if (props.kdId) {
await handleGetQAKnowledgeChunkStruct()
await handleQAKnowledgeChunkList()
}
})
function handleBackKnowledgeDocumentList() {
emit('backList')
}
// 获取QA知识库分片结构
async function handleGetQAKnowledgeChunkStruct() {
const res = await fetchQAKnowledgeChunkStruct<{ key: QAKnowledgeChunkKeyItem[] }>(props.kdId)
if (res.code === 0) {
chunkKeyList.value = res.data.key || []
}
}
// 获取QA知识库分片列表
async function handleQAKnowledgeChunkList() {
knowledgeChunkColumnTableLoading.value = true
const res = await fetchQAKnowledgeChunkList<{
chunk: QAKnowledgeChunkContentItem[]
totalChunk: number
}>(searchKnowledgeChunkValue.value, props.kdId, {
pagingInfo: paginationData,
})
if (res.code === 0) {
knowledgeChunkColumnTableLoading.value = false
knowledgeChunkList.value = res.data.chunk || []
totalChunk.value = res.data.totalChunk || 0
paginationData.totalRows = res.pagingInfo?.totalRows || 0
paginationData.totalPages = res.pagingInfo?.totalPages || 0
isSearchEmptyList.value = !!searchKnowledgeChunkValue.value && !res.data.chunk?.length
}
}
// 搜索QA知识库分片列表
async function handleSearchKnowledgeChunkList() {
paginationData.pageNo = 1
await handleQAKnowledgeChunkList()
}
// 更新分页数获取分片列表
async function handleGetKnowledgeChunkListUpdatePageNo(pageNo: number) {
paginationData.pageNo = pageNo
await handleQAKnowledgeChunkList()
}
// 更新分页大小获取分片列表
async function handleGetKnowledgeChunkListUpdatePageSize(pageSize: number) {
paginationData.pageNo = 1
paginationData.pageSize = pageSize
await handleQAKnowledgeChunkList()
}
// 更新知识库索引
async function handleUpdateKnowledgeChunkIndex(chunkKeyItem: QAKnowledgeChunkKeyItem) {
const { structId, isIndex } = chunkKeyItem
const payload = {
kdId: props.kdId,
structId,
isIndex,
}
fetchUpdateQAKnowledgeStruct(payload).catch(() => {
chunkKeyItem.isIndex = chunkKeyItem.isIndex === 'Y' ? 'N' : 'Y'
})
}
// 关闭设置索引对话框
function handleCloseSettingChunkIndexModal() {
showSettingChunkIndexModal.value = false
handleGetQAKnowledgeChunkStruct()
}
// QA知识库分片列表操作
function handleKnowledgeChunkTableAction(
actionType: string,
chunkRelationId: string,
chunkContentItem?: QAKnowledgeChunkContentItem,
) {
switch (actionType) {
case 'addUp': {
const chunkSort = chunkContentItem?.chunkSort || 0
handleShowAddQAKnowledgeChunk(chunkSort)
break
}
case 'addDown': {
const chunkSort = chunkContentItem?.chunkSort || 0
handleShowAddQAKnowledgeChunk(chunkSort + 1)
break
}
case 'edit':
handleUpdateQAKnowledgeChunk(chunkContentItem!)
break
case 'delete':
handleDeleteQAKnowledgeChunk(chunkRelationId)
break
case 'updateOpen':
handleUpdateQAKnowledgeChunkOpen(chunkRelationId, chunkContentItem!)
break
}
}
// 更新选中的分片Id
function handleUpdateCheckedChunkId(chunkIdList: string[]) {
checkedChunkIdList.value = chunkIdList
}
// 显示新增知识库分片对话框
function handleShowAddQAKnowledgeChunk(chunkSort: number) {
showAddKnowledgeChunkModal.value = true
currentChunkSort.value = chunkSort
}
// 执行添加QA知识库分片
async function handleAddQAKnowledgeChunk(chunkInfos: QAKnowledgeChunkInfoItem[]) {
knowledgeChunkBtnLoading.value = true
const payload = {
kdId: props.kdId,
chunkInfos,
chunkSort: currentChunkSort.value,
}
const res = await fetchAddQAKnowledgeChunk(payload).finally(() => {
knowledgeChunkBtnLoading.value = false
})
if (res.code === 0) {
showAddKnowledgeChunkModal.value = false
window.$message.success(t('common_module.add_success_message'))
currentChunkSort.value === 0 && (paginationData.pageNo = 1)
handleQAKnowledgeChunkList()
}
}
// 显示编辑知识库分片对话框
function handleUpdateQAKnowledgeChunk(chunkContentItem: QAKnowledgeChunkContentItem) {
showEditKnowledgeChunkModal.value = true
currentChunkItem.value = JSON.parse(JSON.stringify(chunkContentItem || []))
}
// 执行编辑知识库分片
async function handleEditQAKnowledgeChunk(chunkRelationId: string, chunkInfos: QAKnowledgeChunkInfoItem[]) {
knowledgeChunkBtnLoading.value = true
const payload = {
kdId: props.kdId,
chunkRelationId,
chunkInfos,
}
const res = await fetchEditQAKnowledgeChunk(payload).finally(() => {
knowledgeChunkBtnLoading.value = false
})
if (res.code === 0) {
showEditKnowledgeChunkModal.value = false
window.$message.success(t('common_module.edit_success_message'))
handleQAKnowledgeChunkList()
}
}
// 删除知识库分片
async function handleDeleteQAKnowledgeChunk(chunkRelationId: string) {
window.$message
.ctWarning('', t('personal_space_module.knowledge_module.delete_knowledge_chunk_content_message'))
.then(async () => {
knowledgeChunkColumnTableLoading.value = true
const res = await fetchDeleteQAKnowledgeChunk({
kdId: props.kdId,
chunkRelationId,
})
if (res.code === 0) {
window.$message.success(t('common_module.delete_success_message'))
await handleQAKnowledgeChunkList()
}
})
}
// 更新知识库是否开启
function handleUpdateQAKnowledgeChunkOpen(chunkRelationId: string, chunkContentItem: QAKnowledgeChunkContentItem) {
chunkContentItem.isOpen = chunkContentItem.isOpen === 'Y' ? 'N' : 'Y'
const payload = {
kdId: props.kdId,
chunkRelationId,
isOpen: chunkContentItem.isOpen,
}
fetchOpenQAKnowledgeChunk(payload).catch(() => {
chunkContentItem.isOpen = chunkContentItem.isOpen === 'Y' ? 'N' : 'Y'
})
}
// 批量删除知识库分片
function handleBatchDeleteKnowledgeChunk() {
window.$message
.ctWarning('', t('personal_space_module.knowledge_module.delete_knowledge_dialog_content'))
.then(async () => {
knowledgeChunkColumnTableLoading.value = true
const res = await fetchBatchDeleteQAKnowledgeChunks(props.kdId, checkedChunkIdList.value)
if (res.code === 0) {
window.$message.success(t('common_module.delete_success_message'))
await userStore.fetchUpdateEquityInfo()
await handleQAKnowledgeChunkList()
}
})
}
</script>
<template>
<div class="flex h-full w-full flex-col overflow-hidden">
<div class="mb-4.5 gap-4.5 mt-6 flex items-center">
<div class="flex h-9 items-center">
<Left theme="outline" size="20" fill="#333" class="cursor-pointer" @click="handleBackKnowledgeDocumentList" />
<img :src="documentIcon(knowledgeDocument.documentUrl)" class="ml-[2px] h-9 w-9" />
<span class="text-font-color ml-[5px] line-clamp-1 select-none break-all text-lg">
{{ knowledgeDocument.documentName }}
</span>
</div>
<ul class="flex flex-shrink-0 gap-2">
<li class="border-inactive-border-color select-none rounded-full border px-4 py-1 leading-[18px]">
{{ totalChunk }}
{{ t('personal_space_module.knowledge_module.segment') }}
</li>
</ul>
</div>
<div class="flex-center mb-4.5 justify-between">
<div class="flex-center gap-2.5">
<n-button type="primary" :disabled="isDisabledOperateBtn" @click="handleShowAddQAKnowledgeChunk(0)">
{{ t('personal_space_module.knowledge_module.add_QA') }}
</n-button>
<n-button type="primary" :disabled="isDisabledOperateBtn" @click="showSettingChunkIndexModal = true">
{{ t('personal_space_module.knowledge_module.setting_index') }}
</n-button>
</div>
<div class="flex-center gap-2.5">
<n-input
v-model:value="searchKnowledgeChunkValue"
:placeholder="t('common_module.search')"
class="w-[214px]!"
@keyup.enter="handleSearchKnowledgeChunkList"
>
<template #suffix>
<Search
theme="outline"
size="16"
fill="#999"
class="cursor-pointer"
@click="handleSearchKnowledgeChunkList"
/>
</template>
</n-input>
<n-button type="info" :disabled="isDisabledBatchDelBtn" @click="handleBatchDeleteKnowledgeChunk">
{{ t('personal_space_module.knowledge_module.batch_delete_knowledge_document_btn_text') }}
</n-button>
</div>
</div>
<div ref="knowledgeChunkListContainer" class="w-full flex-1 overflow-hidden rounded-[10px] bg-white p-6">
<n-data-table
:loading="knowledgeChunkColumnTableLoading"
:columns="knowledgeChunkColumn"
:data="knowledgeChunkList"
:scroll-x="dataTableScrollX"
:max-height="tableContentY"
:row-key="(row: QAKnowledgeChunkContentItem) => row.chunkRelationId"
:checked-row-keys="checkedChunkIdList"
@update:checked-row-keys="handleUpdateCheckedChunkId"
>
<template #empty>
<div :style="{ height: tableContentY + 'px' }" class="flex items-center justify-center">
<div class="flex flex-col items-center justify-center">
<div
class="mb-5 h-[68px] w-[68px]"
:class="isSearchEmptyList ? 'bg-px-search_empty_list-png' : 'bg-px-empty_document_list-png'"
/>
<p class="select-none text-[#84868c]">{{ emptyTableDataText }}</p>
</div>
</div>
</template>
</n-data-table>
</div>
<div class="mt-4 flex justify-end">
<CustomPagination
:paging-info="paginationData"
@update-page-no="handleGetKnowledgeChunkListUpdatePageNo"
@update-page-size="handleGetKnowledgeChunkListUpdatePageSize"
/>
</div>
<SettingKnowledgeChunkIndexModal
v-model:is-show="showSettingChunkIndexModal"
:kd-id="kdId"
:knowledge-chunk-key-list="chunkKeyList"
@update-knowledge-chunk-index="handleUpdateKnowledgeChunkIndex"
@confirm="handleCloseSettingChunkIndexModal"
/>
<AddQAKnowledgeChunkModal
v-model:is-show="showAddKnowledgeChunkModal"
:btn-loading="knowledgeChunkBtnLoading"
:knowledge-chunk-key-list="chunkKeyList"
@confirm="handleAddQAKnowledgeChunk"
/>
<EditQAKnowledgeChunkModal
v-model:is-show="showEditKnowledgeChunkModal"
:btn-loading="knowledgeChunkBtnLoading"
:knowledge-chunk-key-list="chunkKeyList"
:current-chunk-item="currentChunkItem"
@confirm="handleEditQAKnowledgeChunk"
/>
</div>
</template>
<style lang="scss" scoped>
:deep(.qa-knowledge-chunk-selection.n-data-table-td .n-checkbox) {
margin-top: 46px;
}
:deep(.qa-knowledge-chunk-index.n-data-table-td) {
padding-top: 56px;
}
:deep(.qa-knowledge-chunk-item.n-data-table-td) {
padding-top: 56px;
vertical-align: top;
}
:deep(.n-data-table-tr) {
position: relative;
}
</style>
......@@ -40,6 +40,7 @@ const updateKnowledgeInfoBtnLoading = ref(false)
const currentKnowledgeData = ref<KnowledgeFormDataInterface>({
knowledgeName: '',
desc: '',
knowledgeType: 'Base',
})
const isDisabledBatchDelBtn = computed(() => {
......@@ -203,9 +204,11 @@ async function handleBatchDelDocument() {
}
function handleToUploadDocument() {
const knowledgeType = currentKnowledgeData.value.knowledgeType || 'Base'
router.push({
name: 'UploadKnowledge',
params: { id: router.currentRoute.value.params.id, type: 'text-local-document' },
params: { id: router.currentRoute.value.params.id, type: `${knowledgeType}-local-document` },
})
}
......
......@@ -11,6 +11,7 @@ export interface KnowledgeItem {
knowledgeName: string
desc: string
trainStatus: TrainStatus
knowledgeType: 'Base' | 'QA'
isOpen: 'Y' | 'N'
createdTime: Date
documentInfos: KnowledgeDocumentItem[]
......@@ -46,3 +47,22 @@ export interface KnowledgeChunkItem {
chunkSort: number
isOpen: 'Y' | 'N'
}
export interface QAKnowledgeChunkKeyItem {
structId: number
isIndex: 'Y' | 'N'
sort: number
structName: string
}
export interface QAKnowledgeChunkContentItem {
chunkRelationId: string
chunkSort: number
chunkInfo: QAKnowledgeChunkInfoItem[]
isOpen: 'Y' | 'N'
}
export interface QAKnowledgeChunkInfoItem {
structId: number
content: string
}
<script setup lang="ts">
import { computed, onMounted, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import { useIntervalFn } from '@vueuse/core'
import { fetchGetKnowledgeDocumentListByKdIds } from '@/apis/knowledge'
interface Props {
kdIds: number[]
}
interface Emit {
(e: 'confirm'): void
}
enum TrainStatus {
UNOPENED = 'Unopened',
LINE = 'Line',
TRAINING = 'Training',
COMPLETE = 'Complete',
FAIL = 'Fail',
}
interface TrainFileItem {
kdId: number
documentName: string
documentUrl: string
trainStatus: TrainStatus
knowledgeId: string
documentSize: string
}
const { t } = useI18n()
const props = defineProps<Props>()
const emit = defineEmits<Emit>()
const trainFileList = ref<TrainFileItem[]>([])
onMounted(() => {
!!props.kdIds.length && handleGetKnowledgeDocumentList()
})
const trainFileText = computed(() => (status: TrainStatus) => {
switch (status) {
case TrainStatus.COMPLETE:
return 'personal_space_module.knowledge_module.upload_document_module.process_success'
case TrainStatus.FAIL:
return 'personal_space_module.knowledge_module.upload_document_module.process_fail'
default:
return 'personal_space_module.knowledge_module.upload_document_module.processed'
}
})
const trainFileFontColor = computed(() => (status: TrainStatus) => {
switch (status) {
case TrainStatus.COMPLETE:
return '#0B7DFF'
case TrainStatus.FAIL:
return '#F25744'
default:
return '#333333'
}
})
const trainFileIcon = computed(() => (documentUrl: string) => {
const type = documentUrl.substring(documentUrl.lastIndexOf('.') + 1)
return `https://gsst-poe-sit.gz.bcebos.com/icon/${type}.svg`
})
const isTrainFileListFinish = computed(() => {
return trainFileList.value.every((trainFileItem) => {
return [TrainStatus.FAIL, TrainStatus.COMPLETE].includes(trainFileItem.trainStatus)
})
})
watch(
() => isTrainFileListFinish.value,
(newVal) => {
newVal ? pauseIntervalFn() : resumeIntervalFn()
},
{ deep: true },
)
const { pause: pauseIntervalFn, resume: resumeIntervalFn } = useIntervalFn(
async () => {
handleGetKnowledgeDocumentList()
},
2000,
{ immediateCallback: false, immediate: false },
)
async function handleGetKnowledgeDocumentList() {
const res = await fetchGetKnowledgeDocumentListByKdIds<TrainFileItem[]>(props.kdIds)
if (res.code === 0) {
trainFileList.value = res.data
}
}
function handleConfirm() {
emit('confirm')
}
</script>
<template>
<ul class="grid gap-[18px]">
<li
v-for="trainFileItem in trainFileList"
:key="trainFileItem.kdId"
class="border-inactive-border-color relative h-[90px] select-none overflow-hidden rounded-[10px] border bg-white p-6"
>
<div class="flex h-full items-center justify-between">
<div class="flex items-center">
<img :src="trainFileIcon(trainFileItem.documentUrl)" class="mr-[15px] h-9 w-9" />
<div class="flex h-9 flex-col justify-between">
<span class="text-font-color line-clamp-1 break-all text-sm leading-[14px]">
{{ trainFileItem.documentName }}
</span>
<span class="text-gray-font-color line-clamp-1 break-all text-[13px] leading-[13px]">
{{ trainFileItem.documentSize }}
</span>
</div>
</div>
<span :style="{ color: trainFileFontColor(trainFileItem.trainStatus) }">
{{ t(trainFileText(trainFileItem.trainStatus)) }}
</span>
</div>
<div
v-show="![TrainStatus.COMPLETE, TrainStatus.FAIL].includes(trainFileItem.trainStatus)"
class="animate-training absolute left-0 top-[-1px] h-[90px] w-full rounded-[10px] bg-gradient-to-r from-transparent via-[#9EA0FF] to-transparent"
/>
</li>
</ul>
<div class="mt-14 flex items-center justify-end">
<span class="text-gray-font-color mr-2.5 select-none text-xs">
{{ t('personal_space_module.knowledge_module.upload_document_module.data_process_tip_message') }}
</span>
<NButton type="primary" :bordered="false" @click="handleConfirm">
{{ t('common_module.confirm_btn_text') }}
</NButton>
</div>
</template>
<script lang="ts" setup>
import { onMounted, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { useRouter } from 'vue-router'
import UploadFile from './upload-file.vue'
import DataSetting from './data-setting.vue'
import { fetchTrainKnowledge } from '@/apis/knowledge'
interface StepItem {
stepId: number
title: string
}
const { t } = useI18n()
const router = useRouter()
const stepList = ref<StepItem[]>([
{
stepId: 1,
title: t('common_module.upload'),
},
{
stepId: 2,
title: t('personal_space_module.knowledge_module.upload_document_module.data_process'),
},
])
const currentStepId = ref(1)
const currentKdIdList = ref<number[]>([])
const currentKnowledgeId = ref(0)
onMounted(() => {
if (!router.currentRoute.value.params.id) {
window.$message.warning(t('personal_space_module.knowledge_module.not_find_knowledge_message'))
router.replace({ name: 'PersonalSpaceKnowledge' })
return
}
currentKnowledgeId.value = Number(router.currentRoute.value.params.id)
})
async function handleToDataProcess(kdId: number[]) {
currentKdIdList.value = kdId
const res = await fetchTrainKnowledge({
knowledgeInfoId: currentKnowledgeId.value,
kdIds: currentKdIdList.value,
segmentationConfig: {},
})
if (res.code === 0) {
currentStepId.value = 2
}
}
function handleToKnowledgeList() {
router.replace({ name: 'KnowledgeDocument', params: { id: currentKnowledgeId.value } })
}
</script>
<template>
<div class="flex flex-1 flex-col overflow-hidden">
<ul class="mx-auto my-10 flex w-[870px] shrink-0 duration-500 ease-in">
<li
v-for="(stepItem, index) in stepList"
:key="stepItem.stepId"
class="relative flex flex-1 shrink-0 flex-col items-center justify-center"
>
<div
v-show="stepItem.stepId !== 1"
class="absolute left-0 top-[24px] w-[calc((100%-48px)/2)] border-t"
:class="[currentStepId >= stepItem.stepId ? 'border-[#9EA3FF]' : 'border-inactive-border-color']"
/>
<div class="flex flex-col items-center justify-center">
<div
class="relative mb-3 flex h-12 w-12 items-center justify-center rounded-full border text-lg"
:class="{
'border-[#6F77FF] bg-[#6F77FF]': stepItem.stepId === currentStepId,
'border-inactive-border-color text-gray-font-color': stepItem.stepId > currentStepId,
'bg-[#9ea3ff]': stepItem.stepId < currentStepId,
}"
>
<span
v-show="currentStepId <= stepItem.stepId"
class="select-none text-xl"
:class="stepItem.stepId === currentStepId ? 'text-white' : 'text-gray-font-color'"
>
{{ index + 1 }}
</span>
<Check v-show="currentStepId > stepItem.stepId" theme="outline" size="24" fill="#fff" :stroke-width="3" />
</div>
<span
class="text-base"
:class="[currentStepId === stepItem.stepId ? 'text-theme-color' : 'text-gray-font-color']"
>
{{ stepItem.title }}
</span>
</div>
<div
v-show="index < stepList.length - 1"
class="absolute right-0 top-[24px] w-[calc((100%-48px)/2)] border-t"
:class="[currentStepId > stepItem.stepId ? 'border-[#9EA3FF]' : 'border-inactive-border-color']"
/>
</li>
</ul>
<div class="mx-10 flex-1 overflow-hidden">
<NScrollbar class="max-h-full! px-10">
<div v-show="currentStepId === 1">
<UploadFile @next="handleToDataProcess" />
</div>
<div v-if="currentStepId === 2">
<DataSetting :kd-ids="currentKdIdList" @confirm="handleToKnowledgeList" />
</div>
</NScrollbar>
</div>
</div>
</template>
<script setup lang="ts">
import { computed, onMounted, ref } from 'vue'
import { UploadFileInfo } from 'naive-ui'
import { useI18n } from 'vue-i18n'
import { useRouter } from 'vue-router'
import { UploadOne } from '@icon-park/vue-next'
import { fetchUploadKnowledgeDocument } from '@/apis/knowledge'
import { useUserStore } from '@/store/modules/user'
import { downloadFile } from '@/utils/utils'
interface Emit {
(e: 'next', value: number[]): void
}
enum FileItemStatus {
PENDING = 'pending',
UPLOADING = 'uploading',
FINISHED = 'finished',
REMOVED = 'removed',
ERROR = 'error',
}
interface FileItem {
id: string
name: string
size: number
status: FileItemStatus
kdId: number
type?: string
}
const { t } = useI18n()
const router = useRouter()
const userStore = useUserStore()
const emit = defineEmits<Emit>()
const uploadFileList = ref<FileItem[]>([])
const isExceedKnowledgeCount = ref(false)
const uploadFileIcon = (type: string) => {
return `https://gsst-poe-sit.gz.bcebos.com/icon/${type}.svg`
}
const isDisabledNextBtn = computed(() => {
return (
uploadFileList.value.length > 0 && uploadFileList.value.every((item) => item.status === FileItemStatus.FINISHED)
)
})
const uploadFileSize = computed(() => (fileSize: number) => {
if (fileSize == 0) return '0B'
const binarySize = 1024
const sizes = ['B', 'KB', 'MB', 'GB', 'TB']
const unit = Math.floor(Math.log(fileSize) / Math.log(binarySize))
return (fileSize / Math.pow(binarySize, unit)).toPrecision(3) + ' ' + sizes[unit]
})
onMounted(async () => {
await handleGetEquityInfo()
})
async function handleGetEquityInfo() {
await userStore.fetchUpdateEquityInfo()
}
// 上传文件前限制
function handleLimitUpload(data: { file: UploadFileInfo; fileList: UploadFileInfo[] }) {
// 上传数量达到上限
if (isExceedKnowledgeCount.value) {
return false
}
// 获取文件上传时多选的个数
let uploadKnowledgeCount = 0
const knowledgeFileList = document.getElementsByClassName('upload-knowledge-file')
if (knowledgeFileList && knowledgeFileList.length > 0) {
const knowledgeFile = knowledgeFileList[0].getElementsByTagName('input')
if (knowledgeFile && knowledgeFile.length > 0 && knowledgeFile[0].files && knowledgeFile[0].files.length > 0) {
uploadKnowledgeCount = knowledgeFile[0].files.length
}
}
const enableKnowledgeCount = Math.max(
0,
userStore.equityInfo.maxKnowledgeCount - userStore.equityInfo.usedKnowledgeCount - uploadFileList.value.length,
)
if (userStore.equityInfo.maxKnowledgeCount !== 0 && enableKnowledgeCount < uploadKnowledgeCount) {
isExceedKnowledgeCount.value = true
window.$message
.ctWarning(t('equity_module.documents_uploaded_exceeds_tip', { count: enableKnowledgeCount }), '')
.then(() => {
router.push({ name: 'Equity' })
})
.catch(() => {})
return false
}
const allowTypeList = ['xls', 'xlsx']
const fileType = (data.file.file && data.file.file?.name.split('.')?.pop()?.toLowerCase()) || ''
if (data.file.file && !allowTypeList.includes(fileType)) {
window.$message.error(
t('personal_space_module.knowledge_module.upload_document_module.upload_QA_format_error_message'),
)
return false
}
if (data.file.file && data.file.file?.size === 0) {
window.$message.error(
t('personal_space_module.knowledge_module.upload_document_module.empty_document_content_message'),
)
const fileData = {
id: data.file.id,
name: data.file.name,
status: FileItemStatus.ERROR,
size: data.file.file?.size || 0,
type: fileType,
kdId: 0,
}
uploadFileList.value.push(fileData)
return false
}
if (data.file.file && data.file.file?.size > 10 * 1024 * 1024) {
window.$message.error(t('personal_space_module.knowledge_module.upload_document_module.upload_size_error_message'))
const fileData = {
id: data.file.id,
name: data.file.name,
status: FileItemStatus.ERROR,
size: data.file.file?.size || 0,
type: fileType,
kdId: 0,
}
uploadFileList.value.push(fileData)
return false
}
return true
}
// 上传文件
async function handleUpload(file: any) {
const formData = new FormData()
formData.append('documentFiles', file.file.file)
formData.append('knowledgeType', 'QA')
const fileData = {
id: file.file.id,
name: file.file.name,
status: FileItemStatus.UPLOADING,
size: file.file?.file?.size || 0,
type: file.file?.name.split('.')?.pop()?.toLowerCase(),
kdId: 0,
}
if (uploadFileList.value.length < 5) {
await uploadFileList.value.push(fileData)
fetchUploadKnowledgeDocument<{ kdId: number }[]>(formData)
.then((res) => {
if (res.code === 0) {
uploadFileList.value.forEach((fileItem) => {
if (fileItem.id === file.file.id) {
fileItem.status = FileItemStatus.FINISHED
fileItem.kdId = res.data?.[0]?.kdId
}
})
}
})
.catch(() => {
uploadFileList.value.forEach((fileItem) => {
if (fileItem.id === file.file.id) {
fileItem.status = FileItemStatus.ERROR
}
})
})
}
}
// 拖拽文件
function handleDropFile(e: DragEvent) {
const files = e.dataTransfer?.files as FileList
const file = files[0]
isExceedKnowledgeCount.value = false
const dropKnowledgeCount = files.length
const enableKnowledgeCount = Math.max(
0,
userStore.equityInfo.maxKnowledgeCount - userStore.equityInfo.usedKnowledgeCount - uploadFileList.value.length,
)
if (userStore.equityInfo.maxKnowledgeCount !== 0 && enableKnowledgeCount < dropKnowledgeCount) {
isExceedKnowledgeCount.value = true
window.$message
.ctWarning(t('equity_module.documents_uploaded_exceeds_tip', { count: enableKnowledgeCount }), '')
.then(() => {
router.push({ name: 'Equity' })
})
.catch(() => {})
return
}
const allowTypeList = ['xls', 'xlsx']
if (file && file.name) {
const fileType = file.name.split('.')?.pop()?.toLowerCase() || ''
if (!allowTypeList.includes(fileType)) {
window.$message.error(
t('personal_space_module.knowledge_module.upload_document_module.upload_QA_format_error_message'),
)
return false
}
}
}
function handleRemoveFile(id: string) {
uploadFileList.value = uploadFileList.value.filter((item) => item.id !== id)
}
function handleDownloadQATemplate() {
downloadFile(
'https://gsst-poe-sit.gz.bcebos.com/v1/%E9%97%AE%E7%AD%94%E6%95%B0%E6%8D%AE%E5%BA%93%E6%A8%A1%E6%9D%BF.xlsx',
'问答模版.xlsx',
)
}
function handleNextStep() {
const kdIds = uploadFileList.value.map((item) => item.kdId)
emit('next', kdIds)
}
</script>
<template>
<div class="w-full">
<NUpload
multiple
directory-dnd
:show-file-list="false"
:disabled="uploadFileList.length >= 5"
class="upload-knowledge-file"
accept=".xlsx, .xls"
@before-upload="handleLimitUpload"
@change="handleUpload"
@drop="handleDropFile"
>
<NUploadDragger @click="isExceedKnowledgeCount = false">
<div class="mb-3 flex justify-center">
<UploadOne theme="outline" size="36" fill="#333" />
</div>
<NText class="text-base">
{{ t('personal_space_module.knowledge_module.upload_document_module.upload_action_tip_message') }}
</NText>
<p class="text-gray-font-color mt-3 text-sm">
{{ t('personal_space_module.knowledge_module.upload_document_module.upload_QA_limit_tip_message') }}
</p>
<p class="text-gray-font-color mt-3 text-sm">
{{ t('personal_space_module.knowledge_module.upload_document_module.upload_QA_suggest_tip_message') }}
</p>
<p class="text-theme-color mt-3 text-sm hover:opacity-80" @click.stop="handleDownloadQATemplate">
{{ t('personal_space_module.knowledge_module.upload_document_module.download_QA_template') }}
</p>
</NUploadDragger>
</NUpload>
<div class="mt-[18px]">
<ul class="grid grid-cols-2 gap-4">
<li
v-for="uploadFileItem in uploadFileList"
:key="uploadFileItem.id"
class="group relative h-[92px] w-full overflow-hidden rounded-[10px] border bg-white p-6"
:class="uploadFileItem.status === 'error' ? 'border-error-font-color' : 'border-transparent'"
>
<div class="flex items-center justify-between">
<div class="flex items-center">
<img :src="uploadFileIcon(uploadFileItem.type!)" class="h-9 w-9" />
<div class="mx-4 flex flex-col">
<NPopover trigger="hover">
<template #trigger>
<span class="text-font-color mb-2 line-clamp-1 select-none break-all text-base leading-4">
{{ uploadFileItem.name }}
</span>
</template>
<span>{{ uploadFileItem.name }}</span>
</NPopover>
<div
v-if="uploadFileItem.status === FileItemStatus.FINISHED"
class="text-gray-font-color line-clamp-1 select-none break-all"
>
<span class="inline-block group-hover:hidden">{{ uploadFileSize(uploadFileItem.size) }}</span>
<span class="hidden group-hover:inline-block">{{ t('common_module.upload_success_message') }}</span>
</div>
<div
v-else-if="uploadFileItem.status === FileItemStatus.UPLOADING"
class="text-gray-font-color line-clamp-1 select-none break-all"
>
<span>{{ t('common_module.uploading') }}</span>
</div>
<span v-else class="text-error-font-color line-clamp-1 select-none break-all">
{{ t('personal_space_module.knowledge_module.upload_document_module.upload_error_message') }}
</span>
</div>
</div>
<div
v-show="[FileItemStatus.FINISHED, FileItemStatus.ERROR].includes(uploadFileItem.status)"
class="hidden group-hover:block"
>
<NPopover trigger="hover">
<template #trigger>
<i
class="iconfont icon-close cursor-pointer text-base outline-none hover:opacity-80"
:class="
uploadFileItem.status === FileItemStatus.ERROR ? 'text-error-font-color' : 'text-font-color'
"
@click="handleRemoveFile(uploadFileItem.id)"
/>
</template>
<span>{{ t('common_module.delete') }}</span>
</NPopover>
</div>
</div>
<div
v-show="[FileItemStatus.UPLOADING].includes(uploadFileItem.status)"
class="animate-training absolute left-0 top-[-1px] h-[92px] w-full rounded-[10px] bg-gradient-to-r from-transparent via-[#9EA0FF] to-transparent"
/>
</li>
</ul>
</div>
<div class="mt-14 flex justify-end">
<NButton type="primary" :disabled="!isDisabledNextBtn" :bordered="false" @click="handleNextStep">
{{ t('common_module.next_btn_text') }}
</NButton>
</div>
</div>
</template>
......@@ -3,7 +3,8 @@ import { onMounted, ref } from 'vue'
import { useRouter } from 'vue-router'
import { useI18n } from 'vue-i18n'
import { Left } from '@icon-park/vue-next'
import UploadLocalDocument from './upload-local-document/index.vue'
import BaseLocalDocument from './upload-local-document/index.vue'
import QALocalDocument from './qa-local-document/index.vue'
import { fetchGetKnowledgeDetail, fetchUpdateKnowledgeInfo } from '@/apis/knowledge'
import EditKnowledgeModal, { KnowledgeFormDataInterface } from '../components/edit-knowledge-modal.vue'
......@@ -19,6 +20,7 @@ const updateKnowledgeInfoBtnLoading = ref(false)
const currentKnowledgeData = ref<KnowledgeFormDataInterface>({
knowledgeName: '',
desc: '',
knowledgeType: 'Base',
})
onMounted(async () => {
......@@ -81,7 +83,9 @@ async function handleUpdateKnowledgeInfo(knowledgeData: KnowledgeFormDataInterfa
</div>
</div>
<UploadLocalDocument v-if="uploadKnowledgeType === 'text-local-document'" />
<BaseLocalDocument v-if="uploadKnowledgeType === 'Base-local-document'" />
<QALocalDocument v-if="uploadKnowledgeType === 'QA-local-document'" />
<EditKnowledgeModal
v-model:is-show-modal="isShowEditKnowledgeModal"
......
......@@ -39,7 +39,7 @@ const stepList = ref<StepItem[]>([
onMounted(() => {
if (!router.currentRoute.value.params.id) {
window.$message.warning('知识库不存在')
window.$message.warning(t('personal_space_module.knowledge_module.not_find_knowledge_message'))
router.replace({ name: 'PersonalSpaceKnowledge' })
return
}
......
......@@ -152,6 +152,7 @@ function handleLimitUpload(data: { file: UploadFileInfo; fileList: UploadFileInf
async function handleUpload(file: any) {
const formData = new FormData()
formData.append('documentFiles', file.file.file)
formData.append('knowledgeType', 'Base')
const fileData = {
id: file.file.id,
......
......@@ -71,6 +71,7 @@ async function handleCreateKnowledgeNextStep(createKnowledgeData: KnowledgeFormD
const res = await fetchCreateKnowledge<{ id: number }>({
knowledgeName: createKnowledgeData.knowledgeName,
desc: createKnowledgeData.knowledgeDesc,
knowledgeType: createKnowledgeData.knowledgeType,
}).finally(() => {
createKnowledgeBtnLoading.value = false
})
......
......@@ -28,7 +28,7 @@ export default {
],
'font-family-name-quotes': null,
'font-family-no-missing-generic-family-keyword': null,
'scss/at-import-partial-extension': 'always',
'scss/load-partial-extension': 'always',
'alpha-value-notation': 'number',
'selector-class-pattern': null,
},
......
......@@ -248,279 +248,294 @@ declare namespace I18n {
title: string
create_btn_text: string
tab_module: {
agent_module: {
agent_list_module: {
agent_title: string
large_model: string
agent_id: string
agent_publish_status: string
publish_status: string
modified_time: string
channel: string
channel_popover_text: string
agent_copy: string
empty_agent_list: string
delete_agent_dialog_title: string
delete_agent_dialog_content: string
}
agent_module: {
agent_list_module: {
agent_title: string
large_model: string
agent_id: string
agent_publish_status: string
publish_status: string
modified_time: string
channel: string
channel_popover_text: string
agent_copy: string
empty_agent_list: string
delete_agent_dialog_title: string
delete_agent_dialog_content: string
}
}
agent_setting_module: {
my_agent: string
modified: string
auto_save_in: string
update_publish_btn_text: string
publish_btn_text: string
please_finish_publish: string
missing_agent_title_message: string
missing_agent_desc_message: string
agent_config_module: {
stop_generate: string
generating_config_message: string
title: string
ai_auto_config: string
question_answer_model: string
question_answer_model_desc: string
generate_diversity: string
topP: string
topP_popover_message: string
temperature: string
temperature_popover_message: string
communication_turn: string
communication_turn_popover_message: string
agent_setting: string
base_info: string
ai_generate_picture: string
agent_title_input_placeholder: string
agent_title_input_rule_message: string
agent_desc_input_placeholder: string
update_avatar: string
agent_system_prompt: string
agent_system_popover_message: string
optimize_agent_system_prompt: string
optimize_agent_system_popover_message: string
agent_system_template: string
agent_system_template_message: string
agent_system_input_placeholder: string
ability_expand: string
plugin: string
plugin_desc: string
add_plugin: string
add_plugin_successfully: string
remove_plugin_successfully: string
knowledge: string
knowledge_base: string
knowledge_base_desc: string
upload_file: string
upload_file_desc: string
dialogue: string
preamble: string
preamble_input_placeholder: string
featured_questions: string
featured_questions_input_placeholder: string
continuous_question: string
continuous_question_popover_message: string
continuous_question_default: string
continuous_question_default_desc: string
continuous_question_close: string
continuous_question_close_desc: string
memory: string
add_memory_variable: string
memory_variable: string
memory_message: string
memory_variable_message: string
memory_variable_action_edit: string
memory_variable_action_copy: string
variable_name: string
variable_value: string
memory_fragment: string
memory_fragment_message: string
memory_fragment_content: string
memory_variable_delete_tip_content: string
memory_fragment_delete_all_tip_content: string
memory_fragment_delete_row_tip_content: string
add_knowledge_successfully: string
remove_knowledge_successfully: string
setting_voice: string
setting_voice_message: string
setting_voice_desc: string
currently_only_one_voice_can_be_set: string
memory_variable_modal: {
edit_memory_variable: string
memory_variable_message_tip: string
memory_variable_table_name: string
memory_variable_table_name_tip: string
memory_variable_table_name_content: string
memory_variable_table_name_length: string
memory_variable_table_default_value: string
default_value_tip: string
default_value_tip_example: string
memory_variable_table_action: string
default_value_placeholder: string
name_placeholder: string
add_variable: string
memory_variable_add_now: string
none_memory_variable: string
add_variable_message: string
memory_variable_rules: {
name_not_null: string
name_length: string
name_supports: string
name_not_duplicated: string
}
agent_setting_module: {
my_agent: string
modified: string
auto_save_in: string
update_publish_btn_text: string
publish_btn_text: string
please_finish_publish: string
missing_agent_title_message: string
missing_agent_desc_message: string
agent_config_module: {
stop_generate: string
generating_config_message: string
title: string
ai_auto_config: string
question_answer_model: string
question_answer_model_desc: string
generate_diversity: string
topP: string
topP_popover_message: string
temperature: string
temperature_popover_message: string
communication_turn: string
communication_turn_popover_message: string
agent_setting: string
base_info: string
ai_generate_picture: string
agent_title_input_placeholder: string
agent_title_input_rule_message: string
agent_desc_input_placeholder: string
update_avatar: string
agent_system_prompt: string
agent_system_popover_message: string
optimize_agent_system_prompt: string
optimize_agent_system_popover_message: string
agent_system_template: string
agent_system_template_message: string
agent_system_input_placeholder: string
ability_expand: string
plugin: string
plugin_desc: string
add_plugin: string
add_plugin_successfully: string
remove_plugin_successfully: string
knowledge: string
knowledge_base: string
knowledge_base_desc: string
upload_file: string
upload_file_desc: string
dialogue: string
preamble: string
preamble_input_placeholder: string
featured_questions: string
featured_questions_input_placeholder: string
continuous_question: string
continuous_question_popover_message: string
continuous_question_default: string
continuous_question_default_desc: string
continuous_question_close: string
continuous_question_close_desc: string
memory: string
add_memory_variable: string
memory_variable: string
memory_message: string
memory_variable_message: string
memory_variable_action_edit: string
memory_variable_action_copy: string
variable_name: string
variable_value: string
memory_fragment: string
memory_fragment_message: string
memory_fragment_content: string
memory_variable_delete_tip_content: string
memory_fragment_delete_all_tip_content: string
memory_fragment_delete_row_tip_content: string
add_knowledge_successfully: string
remove_knowledge_successfully: string
setting_voice: string
setting_voice_message: string
setting_voice_desc: string
currently_only_one_voice_can_be_set: string
memory_variable_modal: {
edit_memory_variable: string
memory_variable_message_tip: string
memory_variable_table_name: string
memory_variable_table_name_tip: string
memory_variable_table_name_content: string
memory_variable_table_name_length: string
memory_variable_table_default_value: string
default_value_tip: string
default_value_tip_example: string
memory_variable_table_action: string
default_value_placeholder: string
name_placeholder: string
add_variable: string
memory_variable_add_now: string
none_memory_variable: string
add_variable_message: string
memory_variable_rules: {
name_not_null: string
name_length: string
name_supports: string
name_not_duplicated: string
}
}
preview: string
avatar_oversize_message: string
generate_format_error_message: string
preview: string
auto_config_modal_module: {
modal_title: string
generate_tip_message: string
auto_config_input_placeholder: string
cancel_btn_text: string
random_generate_btn_text: string
}
avatar_oversize_message: string
generate_format_error_message: string
optimize_system_modal_module: {
modal_title: string
confirm_btn_text: string
}
auto_config_modal_module: {
modal_title: string
generate_tip_message: string
auto_config_input_placeholder: string
cancel_btn_text: string
random_generate_btn_text: string
}
agent_publish_module: {
channel: string
web_channel_name: string
web_channel_desc: string
access_page: string
share_link: string
copy_share_url_success_message: string
application_square_desc: string
modify_setting_btn: string
not_configured_btn: string
removal_prompt_title: string
removal_prompt_content: string
successfully_configured_published: string
api_call: string
api_call_desc: string
interface_document: string
click_to_generate: string
agentId: string
copy_id: string
api_call_details: string
api_call_datetime: string
optimize_system_modal_module: {
modal_title: string
confirm_btn_text: string
}
}
agent_sale_module: {
application_square_release_setting: string
application_classification: string
application_classification_null: string
application_classify: {
media_entertainment: string
education_training: string
business_services: string
medical_health: string
efficiency_tools: string
office_personnel: string
marketing_commerce: string
finance: string
law: string
culture_tourism: string
}
is_copy: string
yes: string
no: string
confirm_release: string
copy_tip: string
agent_publish_module: {
channel: string
web_channel_name: string
web_channel_desc: string
access_page: string
share_link: string
copy_share_url_success_message: string
application_square_desc: string
modify_setting_btn: string
not_configured_btn: string
removal_prompt_title: string
removal_prompt_content: string
successfully_configured_published: string
api_call: string
api_call_desc: string
interface_document: string
click_to_generate: string
agentId: string
copy_id: string
api_call_details: string
api_call_datetime: string
}
}
knowledge_module: {
search_knowledge_placeholder: string
knowledge_name: string
knowledge_desc: string
delete_knowledge_dialog_content: string
not_find_knowledge_message: string
search_knowledge_document_placeholder: string
knowledge_document_name: string
knowledge_document_char_count: string
knowledge_document_format: string
view_knowledge_document_fragment: string
training_knowledge_document: string
empty_knowledge_document_list: string
upload_knowledge_document_btn_text: string
batch_delete_knowledge_document_btn_text: string
not_find_knowledge_document_message: string
not_all_files_train_complete_tip: string
cannot_add_tip_when_file_is_training: string
create_knowledge_modal_title: string
edit_knowledge_modal_title: string
knowledge_name_input_placeholder: string
knowledge_desc_input_placeholder: string
knowledge_import_type_label: string
knowledge_type_rule: string
knowledge_name_rule: string
knowledge_import_type_rule: string
knowledge_document_text_type: string
import_local_document_knowledge: string
import_local_document_knowledge_desc: string
segment: string
auto_segment: string
custom_segment: string
add_chunk_up_message: string
add_chunk_down_message: string
search_knowledge_chunk_placeholder: string
add_knowledge_chunk_modal_title: string
knowledge_chunk_content_input_placeholder: string
knowledge_chunk_content_input_rule: string
delete_knowledge_chunk_content_message: string
divide_by_word_count: string
Chinese_comma: string
Chinese_period: string
Chinese_question_mark: string
Chinese_exclamation_mark: string
English_period: string
English_exclamation_mark: string
ellipsis: string
upload_document_module: {
segment_setting: string
data_process: string
process_success: string
process_fail: string
processed: string
upload_action_tip_message: string
upload_limit_tip_message: string
empty_document_content_message: string
upload_format_error_message: string
upload_size_error_message: string
upload_error_message: string
default_segment_setting_title: string
default_segment_setting_desc: string
custom_segment_setting_desc: string
segment_identifier: string
segment_identifier_tip: string
please_select_segment_identifier: string
segment_maximum_number_of_words: string
segment_maximum_number_of_words_tip: string
please_input_segment_word_number: string
segment_overlap_word_proportion: string
segment_overlap_word_proportion_tip: string
please_input_segment_overlap_word_proportion: string
data_process_tip_message: string
}
agent_sale_module: {
application_square_release_setting: string
application_classification: string
application_classification_null: string
application_classify: {
media_entertainment: string
education_training: string
business_services: string
medical_health: string
efficiency_tools: string
office_personnel: string
marketing_commerce: string
finance: string
law: string
culture_tourism: string
}
is_copy: string
yes: string
no: string
confirm_release: string
copy_tip: string
}
knowledge_module: {
search_knowledge_placeholder: string
knowledge_name: string
knowledge_desc: string
delete_knowledge_dialog_content: string
not_find_knowledge_message: string
search_knowledge_document_placeholder: string
knowledge_document_name: string
knowledge_document_char_count: string
knowledge_document_format: string
view_knowledge_document_fragment: string
training_knowledge_document: string
empty_knowledge_document_list: string
upload_knowledge_document_btn_text: string
batch_delete_knowledge_document_btn_text: string
not_find_knowledge_document_message: string
not_all_files_train_complete_tip: string
cannot_add_tip_when_file_is_training: string
create_knowledge_modal_title: string
edit_knowledge_modal_title: string
knowledge_name_input_placeholder: string
knowledge_desc_input_placeholder: string
knowledge_import_type_label: string
knowledge_type_rule: string
knowledge_name_rule: string
knowledge_import_type_rule: string
knowledge_document_text_type: string
import_local_document_knowledge: string
import_local_document_knowledge_desc: string
QA_knowledge_type: string
import_QA_local_document_knowledge_desc: string
segment: string
auto_segment: string
custom_segment: string
add_chunk_up_message: string
add_chunk_down_message: string
search_knowledge_chunk_placeholder: string
add_knowledge_chunk_modal_title: string
knowledge_chunk_content_input_placeholder: string
knowledge_chunk_content_input_rule: string
delete_knowledge_chunk_content_message: string
divide_by_word_count: string
Chinese_comma: string
Chinese_period: string
Chinese_question_mark: string
Chinese_exclamation_mark: string
English_period: string
English_exclamation_mark: string
ellipsis: string
index: string
no: string
add_QA: string
add_QA_data: string
edit_QA_data: string
setting_index_desc: string
select_at_least_one_field_as_the_index: string
setting_index: string
field_name: string
please_enter_the_content: string
upload_document_module: {
segment_setting: string
data_process: string
process_success: string
process_fail: string
processed: string
upload_action_tip_message: string
upload_limit_tip_message: string
empty_document_content_message: string
upload_format_error_message: string
upload_size_error_message: string
upload_error_message: string
default_segment_setting_title: string
default_segment_setting_desc: string
custom_segment_setting_desc: string
segment_identifier: string
segment_identifier_tip: string
please_select_segment_identifier: string
segment_maximum_number_of_words: string
segment_maximum_number_of_words_tip: string
please_input_segment_word_number: string
segment_overlap_word_proportion: string
segment_overlap_word_proportion_tip: string
please_input_segment_overlap_word_proportion: string
data_process_tip_message: string
upload_QA_limit_tip_message: string
upload_QA_suggest_tip_message: string
upload_QA_format_error_message: string
download_QA_template: string
}
}
}
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment