feat: knowledge

This commit is contained in:
wangdan-fit2cloud 2025-05-27 19:06:47 +08:00
parent 896fb5fa52
commit b45095f7ec
52 changed files with 4602 additions and 68 deletions

View File

@ -0,0 +1,420 @@
import { Result } from '@/request/Result'
import { get, post, del, put, exportExcel, exportFile } from '@/request/index'
import type { Ref } from 'vue'
import type { KeyValue } from '@/api/type/common'
import type { pageRequest } from '@/api/type/common'
const prefix = '/workspace'
/**
*
* @param knowledge_id,
* page {
"current_page": "string",
"page_size": "string",
}
* param {
"name": "string",
}
*/
const getDocument: (
wordspace_id: string,
knowledge_id: string,
page: pageRequest,
param: any,
loading?: Ref<boolean>,
) => Promise<Result<any>> = (wordspace_id, knowledge_id, page, param, loading) => {
return get(
`${prefix}/${wordspace_id}/knowledge/${knowledge_id}/document/${page.current_page}/${page.page_size}`,
param,
loading,
)
}
/**
*
* @param file:file,limit:number,patterns:array,with_filter:boolean
*/
const postSplitDocument: (data: any) => Promise<Result<any>> = (data) => {
return post(`${prefix}/document/split`, data, undefined, undefined, 1000 * 60 * 60)
}
/**
*
* @param loading
* @returns
*/
const listSplitPattern: (
loading?: Ref<boolean>,
) => Promise<Result<Array<KeyValue<string, string>>>> = (loading) => {
return get(`${prefix}/document/split_pattern`, {}, loading)
}
const getAllDocument: (dataset_id: string, loading?: Ref<boolean>) => Promise<Result<any>> = (
dataset_id,
loading,
) => {
return get(`${prefix}/${dataset_id}/document`, undefined, loading)
}
/**
*
* @param
* {
"name": "string",
"paragraphs": [
{
"content": "string",
"title": "string",
"problem_list": [
{
"id": "string",
"content": "string"
}
]
}
]
}
*/
const postDocument: (
dataset_id: string,
data: any,
loading?: Ref<boolean>,
) => Promise<Result<any>> = (dataset_id, data, loading) => {
return post(`${prefix}/${dataset_id}/document/_bach`, data, {}, loading, 1000 * 60 * 5)
}
/**
*
* @param
* dataset_id, document_id,
* {
"name": "string",
"is_active": true,
"meta": {}
}
*/
const putDocument: (
dataset_id: string,
document_id: string,
data: any,
loading?: Ref<boolean>,
) => Promise<Result<any>> = (dataset_id, document_id, data: any, loading) => {
return put(`${prefix}/${dataset_id}/document/${document_id}`, data, undefined, loading)
}
/**
*
* @param dataset_id, document_id,
*/
const delDocument: (
dataset_id: string,
document_id: string,
loading?: Ref<boolean>,
) => Promise<Result<boolean>> = (dataset_id, document_id, loading) => {
return del(`${prefix}/${dataset_id}/document/${document_id}`, loading)
}
/**
*
* @param dataset_id,
*/
const delMulDocument: (
dataset_id: string,
data: any,
loading?: Ref<boolean>,
) => Promise<Result<boolean>> = (dataset_id, data, loading) => {
return del(`${prefix}/${dataset_id}/document/_bach`, undefined, { id_list: data }, loading)
}
const batchRefresh: (
dataset_id: string,
data: any,
stateList: Array<string>,
loading?: Ref<boolean>,
) => Promise<Result<boolean>> = (dataset_id, data, stateList, loading) => {
return put(
`${prefix}/${dataset_id}/document/batch_refresh`,
{ id_list: data, state_list: stateList },
undefined,
loading,
)
}
/**
*
* @param dataset_id
*/
const getDocumentDetail: (dataset_id: string, document_id: string) => Promise<Result<any>> = (
dataset_id,
document_id,
) => {
return get(`${prefix}/${dataset_id}/document/${document_id}`)
}
/**
*
* @param
* dataset_id, document_id,
*/
const putDocumentRefresh: (
dataset_id: string,
document_id: string,
state_list: Array<string>,
loading?: Ref<boolean>,
) => Promise<Result<any>> = (dataset_id, document_id, state_list, loading) => {
return put(
`${prefix}/${dataset_id}/document/${document_id}/refresh`,
{ state_list },
undefined,
loading,
)
}
/**
* web站点类型
* @param
* dataset_id, document_id,
*/
const putDocumentSync: (
dataset_id: string,
document_id: string,
loading?: Ref<boolean>,
) => Promise<Result<any>> = (dataset_id, document_id, loading) => {
return put(`${prefix}/${dataset_id}/document/${document_id}/sync`, undefined, undefined, loading)
}
const putLarkDocumentSync: (
dataset_id: string,
document_id: string,
loading?: Ref<boolean>,
) => Promise<Result<any>> = (dataset_id, document_id, loading) => {
return put(
`${prefix}/lark/${dataset_id}/document/${document_id}/sync`,
undefined,
undefined,
loading,
)
}
/**
*
* @param dataset_id,
*/
const delMulSyncDocument: (
dataset_id: string,
data: any,
loading?: Ref<boolean>,
) => Promise<Result<boolean>> = (dataset_id, data, loading) => {
return put(`${prefix}/${dataset_id}/document/_bach`, { id_list: data }, undefined, loading)
}
const delMulLarkSyncDocument: (
dataset_id: string,
data: any,
loading?: Ref<boolean>,
) => Promise<Result<boolean>> = (dataset_id, data, loading) => {
return put(`${prefix}/lark/${dataset_id}/_batch`, { id_list: data }, undefined, loading)
}
/**
* Web站点文档
* @param
* {
"source_url_list": [
"string"
],
"selector": "string"
}
}
*/
const postWebDocument: (
dataset_id: string,
data: any,
loading?: Ref<boolean>,
) => Promise<Result<any>> = (dataset_id, data, loading) => {
return post(`${prefix}/${dataset_id}/document/web`, data, undefined, loading)
}
/**
* QA文档
* @param
* file
}
*/
const postQADocument: (
dataset_id: string,
data: any,
loading?: Ref<boolean>,
) => Promise<Result<any>> = (dataset_id, data, loading) => {
return post(`${prefix}/${dataset_id}/document/qa`, data, undefined, loading)
}
/**
*
* @param
* file
*/
const postTableDocument: (
dataset_id: string,
data: any,
loading?: Ref<boolean>,
) => Promise<Result<any>> = (dataset_id, data, loading) => {
return post(`${prefix}/${dataset_id}/document/table`, data, undefined, loading)
}
/**
*
* @param dataset_id,target_dataset_id,
*/
const putMigrateMulDocument: (
dataset_id: string,
target_dataset_id: string,
data: any,
loading?: Ref<boolean>,
) => Promise<Result<boolean>> = (dataset_id, target_dataset_id, data, loading) => {
return put(
`${prefix}/${dataset_id}/document/migrate/${target_dataset_id}`,
data,
undefined,
loading,
)
}
/**
*
* @param dataset_id id
* @param data {id_list:[],hit_handling_method:'directly_return|optimization'}
* @param loading
* @returns
*/
const batchEditHitHandling: (
dataset_id: string,
data: any,
loading?: Ref<boolean>,
) => Promise<Result<boolean>> = (dataset_id, data, loading) => {
return put(`${prefix}/${dataset_id}/document/batch_hit_handling`, data, undefined, loading)
}
/**
* QA模版
* @param fileName,type,
*/
const exportQATemplate: (fileName: string, type: string, loading?: Ref<boolean>) => void = (
fileName,
type,
loading,
) => {
return exportExcel(fileName, `${prefix}/document/template/export`, { type }, loading)
}
/**
* table模版
* @param fileName,type,
*/
const exportTableTemplate: (fileName: string, type: string, loading?: Ref<boolean>) => void = (
fileName,
type,
loading,
) => {
return exportExcel(fileName, `${prefix}/document/table_template/export`, { type }, loading)
}
/**
*
* @param document_name
* @param dataset_id id
* @param document_id id
* @param loading
* @returns
*/
const exportDocument: (
document_name: string,
dataset_id: string,
document_id: string,
loading?: Ref<boolean>,
) => Promise<any> = (document_name, dataset_id, document_id, loading) => {
return exportExcel(
document_name + '.xlsx',
`${prefix}/${dataset_id}/document/${document_id}/export`,
{},
loading,
)
}
/**
*
* @param document_name
* @param dataset_id id
* @param document_id id
* @param loading
* @returns
*/
const exportDocumentZip: (
document_name: string,
dataset_id: string,
document_id: string,
loading?: Ref<boolean>,
) => Promise<any> = (document_name, dataset_id, document_id, loading) => {
return exportFile(
document_name + '.zip',
`${prefix}/${dataset_id}/document/${document_id}/export_zip`,
{},
loading,
)
}
const batchGenerateRelated: (
dataset_id: string,
data: any,
loading?: Ref<boolean>,
) => Promise<Result<boolean>> = (dataset_id, data, loading) => {
return put(`${prefix}/${dataset_id}/document/batch_generate_related`, data, undefined, loading)
}
const cancelTask: (
dataset_id: string,
document_id: string,
data: any,
loading?: Ref<boolean>,
) => Promise<Result<boolean>> = (dataset_id, document_id, data, loading) => {
return put(
`${prefix}/${dataset_id}/document/${document_id}/cancel_task`,
data,
undefined,
loading,
)
}
const batchCancelTask: (
dataset_id: string,
data: any,
loading?: Ref<boolean>,
) => Promise<Result<boolean>> = (dataset_id, data, loading) => {
return put(`${prefix}/${dataset_id}/document/cancel_task/_batch`, data, undefined, loading)
}
export default {
postSplitDocument,
getDocument,
getAllDocument,
postDocument,
putDocument,
delDocument,
delMulDocument,
getDocumentDetail,
listSplitPattern,
putDocumentRefresh,
putDocumentSync,
delMulSyncDocument,
postWebDocument,
putMigrateMulDocument,
batchEditHitHandling,
exportQATemplate,
exportTableTemplate,
postQADocument,
postTableDocument,
exportDocument,
batchRefresh,
batchGenerateRelated,
cancelTask,
exportDocumentZip,
batchCancelTask,
putLarkDocumentSync,
delMulLarkSyncDocument,
}

View File

@ -21,7 +21,7 @@ const getKnowledgeByFolder: (
}
/**
*
*
* @param
* param {
"folder_id": "string",
@ -43,7 +43,66 @@ const getKnowledgeList: (
)
}
/**
*
* @param
*/
// const getAllDataset: (loading?: Ref<boolean>) => Promise<Result<any[]>> = (loading) => {
// return get(`${prefix}`, undefined, loading)
// }
/**
*
* @param knowledge_id
* @query sync_type // 同步类型->replace:替换同步,complete:完整同步
*/
const putSyncWebKnowledge: (
wordspace_id: string,
knowledge_id: string,
sync_type: string,
loading?: Ref<boolean>,
) => Promise<Result<any>> = (wordspace_id, knowledge_id, sync_type, loading) => {
return put(
`${prefix}/${wordspace_id}/knowledge/${knowledge_id}/sync`,
undefined,
{ sync_type },
loading,
)
}
/**
*
* @param knowledge_id
*/
const putReEmbeddingDataset: (
wordspace_id: string,
knowledge_id: string,
loading?: Ref<boolean>,
) => Promise<Result<any>> = (wordspace_id, knowledge_id, loading) => {
return put(
`${prefix}/${wordspace_id}/knowledge/${knowledge_id}/embedding`,
undefined,
undefined,
loading,
)
}
/**
*
* @param knowledge_id
*/
const getKnowledgeDetail: (
wordspace_id: string,
knowledge_id: string,
loading?: Ref<boolean>,
) => Promise<Result<any>> = (wordspace_id, knowledge_id, loading) => {
return get(`${prefix}/${wordspace_id}/knowledge/${knowledge_id}`, undefined, loading)
}
export default {
getKnowledgeByFolder,
getKnowledgeList,
putReEmbeddingDataset,
putSyncWebKnowledge,
getKnowledgeDetail,
}

View File

@ -0,0 +1,9 @@
interface knowledgeData {
name: String
desc: String
documents?: Array<any>
type?: String
embedding_mode_id?: String
}
export type { knowledgeData }

View File

@ -0,0 +1,55 @@
import { h } from 'vue'
export default {
'app-document': {
iconReader: () => {
return h('i', [
h(
'svg',
{
viewBox: '0 0 20 20',
version: '1.1',
xmlns: 'http://www.w3.org/2000/svg',
},
[
h('path', {
d: 'M13.3333 2.50016H4.16667V17.5002H15.8333V5.01641H13.75C13.6395 5.01641 13.5335 4.97251 13.4554 4.89437C13.3772 4.81623 13.3333 4.71025 13.3333 4.59975V2.50016ZM3.33333 0.833496H14.2379C14.3474 0.833465 14.4558 0.855013 14.557 0.896908C14.6582 0.938804 14.7501 1.00023 14.8275 1.07766L17.2563 3.50725C17.4124 3.66356 17.5001 3.87548 17.5 4.09641V18.3335C17.5 18.5545 17.4122 18.7665 17.2559 18.9228C17.0996 19.079 16.8877 19.1668 16.6667 19.1668H3.33333C3.11232 19.1668 2.90036 19.079 2.74408 18.9228C2.5878 18.7665 2.5 18.5545 2.5 18.3335V1.66683C2.5 1.44582 2.5878 1.23385 2.74408 1.07757C2.90036 0.921293 3.11232 0.833496 3.33333 0.833496ZM6.66667 8.3335H13.3333C13.4438 8.3335 13.5498 8.3774 13.628 8.45554C13.7061 8.53368 13.75 8.63966 13.75 8.75016V9.5835C13.75 9.694 13.7061 9.79998 13.628 9.87812C13.5498 9.95626 13.4438 10.0002 13.3333 10.0002H6.66667C6.55616 10.0002 6.45018 9.95626 6.37204 9.87812C6.2939 9.79998 6.25 9.694 6.25 9.5835V8.75016C6.25 8.63966 6.2939 8.53368 6.37204 8.45554C6.45018 8.3774 6.55616 8.3335 6.66667 8.3335ZM6.66667 12.5002H10.4167C10.4714 12.5002 10.5256 12.5109 10.5761 12.5319C10.6267 12.5528 10.6726 12.5835 10.7113 12.6222C10.75 12.6609 10.7807 12.7068 10.8016 12.7574C10.8226 12.8079 10.8333 12.8621 10.8333 12.9168V13.7502C10.8333 13.8049 10.8226 13.8591 10.8016 13.9096C10.7807 13.9602 10.75 14.0061 10.7113 14.0448C10.6726 14.0835 10.6267 14.1142 10.5761 14.1351C10.5256 14.1561 10.4714 14.1668 10.4167 14.1668H6.66667C6.55616 14.1668 6.45018 14.1229 6.37204 14.0448C6.2939 13.9667 6.25 13.8607 6.25 13.7502V12.9168C6.25 12.8063 6.2939 12.7003 6.37204 12.6222C6.45018 12.5441 6.55616 12.5002 6.66667 12.5002Z',
fill: 'currentColor',
}),
],
),
])
},
},
'app-document-active': {
iconReader: () => {
return h('i', [
h(
'svg',
{
viewBox: '0 0 20 20',
version: '1.1',
xmlns: 'http://www.w3.org/2000/svg',
},
[
h('path', {
d: 'M3.3335 2.08333C3.3335 1.6231 3.70659 1.25 4.16683 1.25H12.3842C12.4959 1.25 12.603 1.29489 12.6813 1.37459L16.5473 5.30784C16.6239 5.38576 16.6668 5.49065 16.6668 5.59992V17.9167C16.6668 18.3769 16.2937 18.75 15.8335 18.75H4.16683C3.70659 18.75 3.3335 18.3769 3.3335 17.9167V2.08333Z',
fill: 'currentColor',
}),
h('path', {
d: 'M12.5 1.2666C12.568 1.28633 12.6306 1.32327 12.6812 1.37472L16.5472 5.30797C16.5788 5.34017 16.6047 5.37698 16.6242 5.4168H13.4459C12.9235 5.4168 12.5 4.99328 12.5 4.47085V1.2666Z',
fill: '#2B5FD9',
}),
h('path', {
d: 'M6.71305 7.72705C6.48293 7.72705 6.29639 7.9136 6.29639 8.14372V8.82554C6.29639 9.05565 6.48294 9.2422 6.71305 9.2422H13.2871C13.5172 9.2422 13.7038 9.05565 13.7038 8.82554V8.14372C13.7038 7.9136 13.5172 7.72705 13.2871 7.72705H6.71305Z',
fill: 'white',
}),
h('path', {
d: 'M6.71305 11.5149C6.48293 11.5149 6.29639 11.7015 6.29639 11.9316V12.6134C6.29639 12.8435 6.48294 13.0301 6.71305 13.0301H9.58342C9.81354 13.0301 10.0001 12.8435 10.0001 12.6134V11.9316C10.0001 11.7015 9.81354 11.5149 9.58342 11.5149H6.71305Z',
fill: 'white',
}),
],
),
])
},
},
}

View File

@ -0,0 +1,57 @@
import { h } from 'vue'
export default {
'app-vectorization': {
iconReader: () => {
return h('i', [
h(
'svg',
{
style: { height: '100%', width: '100%' },
viewBox: '0 0 1024 1024',
version: '1.1',
xmlns: 'http://www.w3.org/2000/svg',
},
[
h('path', {
d: 'M512 170.666667a85.333333 85.333333 0 0 1 85.333333-85.333334h256a85.333333 85.333333 0 0 1 85.333334 85.333334v256a85.333333 85.333333 0 0 1-85.333334 85.333333h-256a85.333333 85.333333 0 0 1-85.333333-85.333333V170.666667z m85.333333 0v256h256V170.666667h-256zM85.333333 597.333333a85.333333 85.333333 0 0 1 85.333334-85.333333h256a85.333333 85.333333 0 0 1 85.333333 85.333333v256a85.333333 85.333333 0 0 1-85.333333 85.333334H170.666667a85.333333 85.333333 0 0 1-85.333334-85.333334v-256z m85.333334 0v256h256v-256H170.666667zM128 298.666667a213.333333 213.333333 0 0 1 213.333333-213.333334h85.333334v85.333334H341.333333a128 128 0 0 0-128 128h57.514667a12.8 12.8 0 0 1 9.728 21.12l-100.181333 116.906666a12.8 12.8 0 0 1-19.456 0l-100.181334-116.906666A12.8 12.8 0 0 1 70.485333 298.666667H128zM896 725.333333a213.333333 213.333333 0 0 1-213.333333 213.333334h-85.333334v-85.333334h85.333334a128 128 0 0 0 128-128v-21.333333h-57.514667a12.8 12.8 0 0 1-9.728-21.12l100.181333-116.906667a12.8 12.8 0 0 1 19.456 0l100.181334 116.906667a12.8 12.8 0 0 1-9.728 21.12H896v21.333333z',
fill: 'currentColor',
}),
],
),
])
},
},
'app-all-menu-active': {
iconReader: () => {
return h('i', [
h(
'svg',
{
style: { height: '100%', width: '100%' },
viewBox: '0 0 20 20',
version: '1.1',
xmlns: 'http://www.w3.org/2000/svg',
},
[
h('path', {
d: 'M8.33317 1.6665H2.49984C2.0396 1.6665 1.6665 2.0396 1.6665 2.49984V8.33317C1.6665 8.79341 2.0396 9.1665 2.49984 9.1665H8.33317C8.79341 9.1665 9.1665 8.79341 9.1665 8.33317V2.49984C9.1665 2.0396 8.79341 1.6665 8.33317 1.6665Z',
fill: 'currentColor',
}),
h('path', {
d: 'M8.33317 10.8332H2.49984C2.0396 10.8332 1.6665 11.2063 1.6665 11.6665V17.4998C1.6665 17.9601 2.0396 18.3332 2.49984 18.3332H8.33317C8.79341 18.3332 9.1665 17.9601 9.1665 17.4998V11.6665C9.1665 11.2063 8.79341 10.8332 8.33317 10.8332Z',
fill: 'currentColor',
}),
h('path', {
d: 'M17.4998 1.6665H11.6665C11.2063 1.6665 10.8332 2.0396 10.8332 2.49984V8.33317C10.8332 8.79341 11.2063 9.1665 11.6665 9.1665H17.4998C17.9601 9.1665 18.3332 8.79341 18.3332 8.33317V2.49984C18.3332 2.0396 17.9601 1.6665 17.4998 1.6665Z',
fill: 'currentColor',
}),
h('path', {
d: 'M17.4508 10.8332H11.7155C11.2282 10.8332 10.8332 11.2282 10.8332 11.7155V17.4508C10.8332 17.9381 11.2282 18.3332 11.7155 18.3332H17.4508C17.9381 18.3332 18.3332 17.9381 18.3332 17.4508V11.7155C18.3332 11.2282 17.9381 10.8332 17.4508 10.8332Z',
fill: 'currentColor',
}),
],
),
])
},
},
}

View File

@ -0,0 +1,159 @@
<template>
<div class="app-table" :class="quickCreate ? 'table-quick-append' : ''">
<el-table :max-height="tableHeight" v-bind="$attrs" ref="appTableRef">
<template #append v-if="quickCreate">
<div v-if="showInput">
<el-input
ref="quickInputRef"
v-model="inputValue"
:placeholder="`${$t('common.inputPlaceholder')} ${quickCreateName}`"
class="w-500 mr-12"
autofocus
:maxlength="quickCreateMaxlength || '-'"
:show-word-limit="quickCreateMaxlength ? true : false"
@keydown.enter="submitHandle"
clearable
/>
<el-button type="primary" @click="submitHandle" :disabled="loading">{{$t('common.create')}}</el-button>
<el-button @click="showInput = false" :disabled="loading">{{$t('common.cancel')}}</el-button>
</div>
<div v-else @click="quickCreateHandle" class="w-full">
<el-button type="primary" link class="quich-button">
<el-icon><Plus /></el-icon>
<span class="ml-4">{{ quickCreatePlaceholder }}</span>
</el-button>
</div>
</template>
<slot></slot>
</el-table>
<div class="app-table__pagination mt-16" v-if="$slots.pagination || paginationConfig">
<slot name="pagination">
<el-pagination
v-model:current-page="paginationConfig.current_page"
v-model:page-size="paginationConfig.page_size"
:page-sizes="pageSizes"
:total="paginationConfig.total"
layout="total, prev, pager, next, sizes"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</slot>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, nextTick, watch, computed, onMounted } from 'vue'
import { MsgError } from '@/utils/message'
import { t } from '@/locales'
defineOptions({ name: 'AppTable' })
import useStore from '@/stores'
const { common } = useStore()
const props = defineProps({
paginationConfig: {
type: Object,
default: () => {}
},
quickCreate: {
type: Boolean,
default: false
},
quickCreateName: {
type: String,
default: t('components.quickCreateName')
},
quickCreatePlaceholder: {
type: String,
default: t('components.quickCreatePlaceholder')
},
quickCreateMaxlength: {
type: Number,
default: () => 0
},
storeKey: String
})
const emit = defineEmits(['changePage', 'sizeChange', 'creatQuick'])
const paginationConfig = computed(() => props.paginationConfig)
const pageSizes = [10, 20, 50, 100]
const quickInputRef = ref()
const appTableRef = ref()
const loading = ref(false)
const showInput = ref(false)
const inputValue = ref('')
const tableHeight = ref(0)
watch(showInput, (bool) => {
if (!bool) {
inputValue.value = ''
}
})
function submitHandle() {
if (inputValue.value) {
loading.value = true
emit('creatQuick', inputValue.value)
setTimeout(() => {
showInput.value = false
loading.value = false
}, 200)
} else {
MsgError(`${props.quickCreateName} ${t('dynamicsForm.tip.requiredMessage')}`)
}
}
function quickCreateHandle() {
showInput.value = true
nextTick(() => {
quickInputRef.value?.focus()
})
}
function handleSizeChange() {
emit('sizeChange')
if (props.storeKey) {
common.savePage(props.storeKey, props.paginationConfig)
}
}
function handleCurrentChange() {
emit('changePage')
if (props.storeKey) {
common.savePage(props.storeKey, props.paginationConfig)
}
}
function clearSelection() {
appTableRef.value?.clearSelection()
}
defineExpose({
clearSelection
})
onMounted(() => {
tableHeight.value = window.innerHeight - 300
window.onresize = () => {
return (() => {
tableHeight.value = window.innerHeight - 300
})()
}
})
</script>
<style lang="scss" scoped>
.app-table {
&__pagination {
display: flex;
justify-content: flex-end;
}
.quich-button {
&:hover {
color: var(--el-button-text-color);
}
}
}
</style>

View File

@ -0,0 +1,31 @@
<template>
<el-button class="back-button cursor mr-4" text @click="jump">
<el-icon :size="20">
<Back />
</el-icon>
</el-button>
</template>
<script setup lang="ts">
import { useRouter, type RouteLocationRaw } from 'vue-router'
defineOptions({ name: 'BackButton' })
const router = useRouter()
const props = defineProps({
to: String
})
const emit = defineEmits(['click'])
/* 上一层路由 */
const back: any = router.options.history.state.back
function jump() {
if (props.to === '-1') {
back ? router.push(back) : router.go(-1)
} else if (props.to) {
router.push(props.to as RouteLocationRaw)
} else {
emit('click')
}
}
</script>
<style lang="scss"></style>

View File

@ -9,6 +9,8 @@ import ContentContainer from './layout-container/ContentContainer.vue'
import CardBox from './card-box/index.vue'
import FolderTree from './folder-tree/index.vue'
import CommonList from './common-list/index.vue'
import BackButton from './back-button/index.vue'
import AppTable from './app-table/index.vue'
export default {
install(app: App) {
app.component('LogoFull', LogoFull)
@ -21,5 +23,7 @@ export default {
app.component('CardBox', CardBox)
app.component('FolderTree', FolderTree)
app.component('CommonList', CommonList)
app.component('BackButton', BackButton)
app.component('AppTable', AppTable)
},
}

16
ui/src/enums/common.ts Normal file
View File

@ -0,0 +1,16 @@
export enum DeviceType {
Mobile = 'Mobile',
Desktop = 'Desktop'
}
export enum ValidType {
Application = 'application',
Dataset = 'dataset',
User = 'user'
}
export enum ValidCount {
Application = 5,
Dataset = 50,
User = 2
}

View File

@ -10,7 +10,7 @@
>
<div class="breadcrumb-hover flex-between cursor">
<div class="flex align-center">
<AppAvatar
<el-avatar
v-if="isApplication && isAppIcon(current?.icon)"
shape="square"
:size="24"
@ -18,8 +18,8 @@
class="mr-8"
>
<img :src="current?.icon" alt="" />
</AppAvatar>
<AppAvatar
</el-avatar>
<el-avatar
v-else-if="isApplication"
:name="current?.name"
pinyinColor
@ -28,15 +28,15 @@
:size="24"
/>
<AppAvatar
<el-avatar
v-else-if="isDataset && current?.type === '1'"
class="mr-8 avatar-purple"
shape="square"
:size="24"
>
<img src="@/assets/icon_web.svg" style="width: 58%" alt="" />
</AppAvatar>
<AppAvatar
<img src="@/assets/knowledge/icon_web.svg" style="width: 58%" alt="" />
</el-avatar>
<el-avatar
v-else-if="isDataset && current?.type === '2'"
class="mr-8 avatar-purple"
shape="square"
@ -44,10 +44,10 @@
style="background: none"
>
<img src="@/assets/knowledge/logo_lark.svg" style="width: 100%" alt="" />
</AppAvatar>
<AppAvatar v-else class="mr-8 avatar-blue" shape="square" :size="24">
</el-avatar>
<el-avatar v-else class="mr-8 avatar-blue" shape="square" :size="24">
<img src="@/assets/knowledge/icon_document.svg" style="width: 58%" alt="" />
</AppAvatar>
</el-avatar>
<div class="ellipsis" :title="current?.name">{{ current?.name }}</div>
</div>
@ -63,7 +63,7 @@
<div :class="item.id === id ? 'dropdown-active' : ''">
<el-dropdown-item :command="item.id">
<div class="flex align-center">
<AppAvatar
<el-avatar
v-if="isApplication && isAppIcon(item?.icon)"
shape="square"
:size="24"
@ -71,9 +71,9 @@
class="mr-8"
>
<img :src="item?.icon" alt="" />
</AppAvatar>
</el-avatar>
<AppAvatar
<el-avatar
v-else-if="isApplication"
:name="item.name"
pinyinColor
@ -87,7 +87,7 @@
shape="square"
:size="24"
>
<img src="@/assets/icon_web.svg" style="width: 58%" alt="" />
<img src="@/assets/knowledge/icon_web.svg" style="width: 58%" alt="" />
</AppAvatar>
<AppAvatar
v-else-if="isDataset && item.type === '2'"
@ -96,10 +96,10 @@
:size="24"
style="background: none"
>
<img src="@/assets/logo_lark.svg" style="width: 100%" alt="" />
<img src="@/assets/knowledge/logo_lark.svg" style="width: 100%" alt="" />
</AppAvatar>
<AppAvatar v-else class="mr-12 avatar-blue" shape="square" :size="24">
<img src="@/assets/icon_document.svg" style="width: 58%" alt="" />
<img src="@/assets/knowledge/icon_document.svg" style="width: 58%" alt="" />
</AppAvatar>
<span class="ellipsis" :title="item?.name"> {{ item?.name }}</span>
</div>

View File

@ -47,7 +47,7 @@ const props = defineProps<{
const router = useRouter()
const route = useRoute()
const {
params: { id, type }
params: { id, type },
} = route as any
function showMenu() {
@ -79,7 +79,7 @@ const menuIcon = computed(() => {
margin-top: -2px;
}
.el-menu-item {
padding: 13px 12px 13px 16px !important;
padding: 13px 12px 13px 8px !important;
font-weight: 500;
border-radius: 4px;
&:hover {

View File

@ -23,16 +23,13 @@ import { computed } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { getChildRouteListByPathAndName } from '@/router/index'
import SidebarItem from './SidebarItem.vue'
// import AppBreadcrumb from './../breadcrumb/index.vue'
import AppBreadcrumb from './../breadcrumb/index.vue'
const route = useRoute()
const showBreadcrumb = computed(() => {
const { meta } = route as any
return (
meta?.activeMenu &&
(meta?.activeMenu.includes('dataset') || meta?.activeMenu.includes('application'))
)
return meta?.breadcrumb
})
const subMenuList = computed(() => {

View File

@ -5,6 +5,7 @@
v-hasPermission="menu.meta?.permission"
v-for="(menu, index) in topMenuList"
:key="index"
v-show="!menu.hidden"
>
</MenuItem>
</div>
@ -22,5 +23,4 @@ const topMenuList = computed(() => {
.top-menu-container {
line-height: var(--app-header-height);
}
</style>

View File

@ -8,9 +8,7 @@
<template #left>
<Sidebar />
</template>
<template #right>
<AppMain />
</template>
<AppMain />
</layout-container>
</div>
</div>

View File

@ -34,12 +34,16 @@ export default {
public: 'Public',
private: 'Private',
paramSetting: 'Parameter Settings',
name: 'Name',
creator: 'Creator',
author: 'Author',
debug: 'Debug',
required: 'Required',
noData: 'No data',
result: 'Result',
searchBar: {
placeholder: 'Search by name',
},
fileUpload: {
document: 'Documents',
image: 'Image',

View File

@ -3,9 +3,7 @@ export default {
provider: 'Provider',
providerPlaceholder: 'Select Provider',
addModel: 'Add Model',
searchBar: {
placeholder: 'Search by name',
},
delete: {
confirmTitle: 'Delete Model',
confirmMessage: 'Are you sure you want to delete the model:',

View File

@ -35,12 +35,16 @@ export default {
public: '公有',
private: '私有',
paramSetting: '参数设置',
name: '名称',
creator: '创建者',
author: '作者',
debug: '调试',
required: '必填',
noData: '暂无数据',
result: '结果',
searchBar: {
placeholder: '按名称搜索',
},
fileUpload: {
document: '文档',
image: '图片',

View File

@ -0,0 +1,176 @@
export default {
uploadDocument: '上传文档',
importDocument: '导入文档',
syncDocument: '同步文档',
selected: '已选',
items: '项',
searchBar: {
placeholder: '按 文档名称 搜索'
},
setting: {
migration: '迁移',
cancelGenerateQuestion: '取消生成问题',
cancelVectorization: '取消向量化',
cancelGenerate: '取消生成',
export: '导出'
},
tip: {
saveMessage: '当前的更改尚未保存,确认退出吗?',
cancelSuccess: '批量取消成功',
sendMessage: '发送成功',
vectorizationSuccess: '批量向量化成功',
nameMessage: '文件名称不能为空!',
importMessage: '导入成功',
migrationSuccess: '迁移成功'
},
upload: {
selectFile: '选择文件',
selectFiles: '选择文件夹',
uploadMessage: '拖拽文件至此上传或',
formats: '支持格式:',
requiredMessage: '请上传文件',
errorMessage1: '文件大小超过 100MB',
errorMessage2: '文件格式不支持',
errorMessage3: '文件不能为空',
errorMessage4: '每次最多上传50个文件',
template: '模版',
download: '下载'
},
fileType: {
txt: {
label: '文本文件',
tip1: '1、文件上传前建议规范文件的分段标识',
tip2: '2、每次最多上传 50 个文件,每个文件不超过 100MB'
},
table: {
label: '表格',
tip1: '1、点击下载对应模版并完善信息',
tip2: '2、第一行必须是列标题且列标题必须是有意义的术语表中每条记录将作为一个分段',
tip3: '3、上传的表格文件中每个 sheet 会作为一个文档sheet名称为文档名称',
tip4: '4、每次最多上传 50 个文件,每个文件不超过 100MB'
},
QA: {
label: 'QA 问答对',
tip1: '1、点击下载对应模版并完善信息',
tip2: '2、上传的表格文件中每个 sheet 会作为一个文档sheet名称为文档名称',
tip3: '3、每次最多上传 50 个文件,每个文件不超过 100MB'
},
lark: {}
},
setRules: {
title: {
setting: '设置分段规则',
preview: '分段预览'
},
intelligent: {
label: '智能分段(推荐)',
text: '不了解如何设置分段规则推荐使用智能分段'
},
advanced: {
label: '高级分段',
text: '用户可根据文档规范自行设置分段标识符、分段长度以及清洗规则'
},
patterns: {
label: '分段标识',
tooltip: '按照所选符号先后顺序做递归分割,分割结果超出分段长度将截取至分段长度。',
placeholder: '请选择'
},
limit: {
label: '分段长度'
},
with_filter: {
label: '自动清洗',
text: '去掉重复多余符号空格、空行、制表符'
},
checkedConnect: {
label: '导入时添加分段标题为关联问题(适用于标题为问题的问答对)'
}
},
buttons: {
prev: '上一步',
next: '下一步',
import: '开始导入',
preview: '生成预览'
},
table: {
name: '文件名称',
char_length: '字符数',
paragraph: '分段',
all: '全部',
updateTime: '更新时间'
},
fileStatus: {
label: '文件状态',
SUCCESS: '成功',
FAILURE: '失败',
EMBEDDING: '索引中',
PENDING: '排队中',
GENERATE: '生成中',
SYNC: '同步中',
REVOKE: '取消中',
finish: '完成'
},
enableStatus: {
label: '启用状态',
enable: '开启',
close: '关闭'
},
sync: {
label: '同步',
confirmTitle: '确认同步文档?',
confirmMessage1: '同步将删除已有数据重新获取新数据,请谨慎操作。',
confirmMessage2: '无法同步,请先去设置文档 URL地址',
successMessage: '同步文档成功'
},
delete: {
confirmTitle1: '是否批量删除',
confirmTitle2: '个文档?',
confirmMessage: '所选文档中的分段会跟随删除,请谨慎操作。',
successMessage: '批量删除成功',
confirmTitle3: '是否删除文档:',
confirmMessage1: '此文档下的',
confirmMessage2: '个分段都会被删除,请谨慎操作。'
},
form: {
source_url: {
label: '文档地址',
placeholder: '请输入文档地址,一行一个,地址不正确文档会导入失败。',
requiredMessage: '请输入文档地址'
},
selector: {
label: '选择器',
placeholder: '默认为 body可输入 .classname/#idname/tagname'
},
hit_handling_method: {
label: '命中处理方式',
tooltip: '用户提问时,命中文档下的分段时按照设置的方式进行处理。'
},
similarity: {
label: '相似度高于',
placeholder: '直接返回分段内容',
requiredMessage: '请输入相似度'
}
},
hitHandlingMethod: {
optimization: '模型优化',
directly_return: '直接回答'
},
generateQuestion: {
title: '生成问题',
successMessage: '生成问题成功',
tip1: '提示词中的 {data} 为分段内容的占位符,执行时替换为分段内容发送给 AI 模型;',
tip2: 'AI 模型根据分段内容生成相关问题,请将生成的问题放至',
tip3: '标签中,系统会自动关联标签中的问题;',
tip4: '生成效果依赖于所选模型和提示词,用户可自行调整至最佳效果。',
prompt1: `内容:{data}\n\n请总结上面的内容并根据内容总结生成 5 个问题。\n回答要求\n- 请只输出问题;\n- 请将每个问题放置`,
prompt2: `标签中。`
},
feishu: {
selectDocument: '选择文档',
tip1: '支持文档和表格类型包含TXT、Markdown、PDF、DOCX、HTML、XLS、XLSX、CSV、ZIP格式',
tip2: '系统不存储原始文档,导入文档前,建议规范文档的分段标识。',
allCheck: '全选',
errorMessage1: '请选择文档'
}
}

View File

@ -2,6 +2,7 @@ import login from './login'
import model from './model'
import knowledge from './knowledge'
import tool from './tool'
import document from './document'
// import notFound from './404'
// import application from './application'
// import applicationOverview from './application-overview'
@ -10,7 +11,6 @@ import tool from './tool'
// import user from './user'
// import team from './team'
// import document from './document'
// import paragraph from './paragraph'
// import problem from './problem'
// import log from './log'
@ -22,13 +22,13 @@ export default {
model,
knowledge,
tool,
document,
// notFound,
// application,
// applicationOverview,
// system,
// user,
// team,
// document,
// paragraph,
// problem,
// log,

View File

@ -2,4 +2,8 @@ export default {
title: '知识库',
document_count: '文档数',
relatedApp_count: '关联应用',
setting: {
vectorization: '向量化',
sync: '同步',
},
}

View File

@ -3,9 +3,6 @@ export default {
provider: '供应商',
providerPlaceholder: '选择供应商',
addModel: '添加模型',
searchBar: {
placeholder: '按名称搜索',
},
delete: {
confirmTitle: '删除模型',
confirmMessage: '是否删除模型:',

View File

@ -34,12 +34,16 @@ export default {
public: '公有',
private: '私有',
paramSetting: '參數設定',
name: '名稱',
creator: '建立者',
author: '作者',
debug: '調試',
required: '必填',
noData: '暂无数据',
result: '結果',
searchBar: {
placeholder: '按名稱搜尋',
},
fileUpload: {
document: '文檔',
image: '圖片',
@ -73,6 +77,6 @@ export default {
custom: '自訂',
sizeTip: '建議尺寸 32*32支援 JPG、PNG、GIF大小不超過 10 MB',
fileSizeExceeded: '檔案大小超過 10 MB',
uploadImagePrompt: '請上傳一張圖片'
uploadImagePrompt: '請上傳一張圖片',
},
}

View File

@ -3,9 +3,6 @@ export default {
provider: '供應商',
providerPlaceholder: '選擇供應商',
addModel: '新增模型',
searchBar: {
placeholder: '按名稱搜尋',
},
delete: {
confirmTitle: '刪除模型',
confirmMessage: '是否刪除模型:',

View File

@ -9,9 +9,9 @@ const ModelRouter = {
path: '/knowledge',
name: 'knowledge-index',
meta: { title: '知识库主页', activeMenu: '/knowledge' },
component: () => import('@/views/knowledge/index.vue')
}
]
component: () => import('@/views/knowledge/index.vue'),
},
],
}
export default ModelRouter

View File

@ -0,0 +1,62 @@
const ModelRouter = {
path: '/knowledge/:id',
name: 'DatasetDetail',
meta: { title: 'common.fileUpload.document', activeMenu: '/knowledge', breadcrumb: true },
component: () => import('@/layout/layout-template/MainLayout.vue'),
hidden: true,
children: [
{
path: 'document',
name: 'Document',
meta: {
icon: 'app-document',
iconActive: 'app-document-active',
title: 'common.fileUpload.document',
active: 'document',
parentPath: '/knowledge/:id',
parentName: 'DatasetDetail',
},
component: () => import('@/views/document/index.vue'),
},
// {
// path: 'problem',
// name: 'Problem',
// meta: {
// icon: 'app-problems',
// iconActive: 'QuestionFilled',
// title: 'views.problem.title',
// active: 'problem',
// parentPath: '/dataset/:id',
// parentName: 'DatasetDetail'
// },
// component: () => import('@/views/problem/index.vue')
// },
// {
// path: 'hit-test',
// name: 'DatasetHitTest',
// meta: {
// icon: 'app-hit-test',
// title: 'views.application.hitTest.title',
// active: 'hit-test',
// parentPath: '/dataset/:id',
// parentName: 'DatasetDetail'
// },
// component: () => import('@/views/hit-test/index.vue')
// },
// {
// path: 'setting',
// name: 'DatasetSetting',
// meta: {
// icon: 'app-setting',
// iconActive: 'app-setting-active',
// title: 'common.setting',
// active: 'setting',
// parentPath: '/dataset/:id',
// parentName: 'DatasetDetail'
// },
// component: () => import('@/views/dataset/DatasetSetting.vue')
// }
],
}
export default ModelRouter

View File

@ -6,7 +6,7 @@ export const routes: Array<RouteRecordRaw> = [
{
path: '/',
name: 'home',
redirect: '/model',
redirect: '/knowledge',
children: [...rolesRoutes],
},

View File

@ -1,13 +1,17 @@
import useCommonStore from './modules/common'
import useLoginStore from './modules/login'
import useUserStore from './modules/user'
import useFolderStore from './modules/folder'
import useThemeStore from './modules/theme'
import useKnowledgeStore from './modules/knowledge'
const useStore = () => ({
common: useCommonStore(),
login: useLoginStore(),
user: useUserStore(),
folder: useFolderStore(),
theme: useThemeStore(),
knowledge: useKnowledgeStore(),
})
export default useStore

View File

@ -0,0 +1,52 @@
import { defineStore } from 'pinia'
import { DeviceType, ValidType } from '@/enums/common'
// import type { Ref } from 'vue'
// import userApi from '@/api/user'
export interface commonTypes {
breadcrumb: any
paginationConfig: any | null
search: any
device: string
}
const useCommonStore = defineStore('common',{
state: (): commonTypes => ({
breadcrumb: null,
// 搜索和分页缓存
paginationConfig: {},
search: {},
device: DeviceType.Desktop
}),
actions: {
saveBreadcrumb(data: any) {
this.breadcrumb = data
},
savePage(val: string, data: any) {
this.paginationConfig[val] = data
},
saveCondition(val: string, data: any) {
this.search[val] = data
},
toggleDevice(value: DeviceType) {
this.device = value
},
isMobile() {
return this.device === DeviceType.Mobile
},
// async asyncGetValid(valid_type: ValidType, valid_count: number, loading?: Ref<boolean>) {
// return new Promise((resolve, reject) => {
// userApi
// .getValid(valid_type, valid_count, loading)
// .then((data) => {
// resolve(data)
// })
// .catch((error) => {
// reject(error)
// })
// })
// }
}
})
export default useCommonStore

View File

@ -0,0 +1,77 @@
import { defineStore } from 'pinia'
import type { knowledgeData } from '@/api/type/knowledge'
import type { UploadUserFile } from 'element-plus'
import knowledgeApi from '@/api/knowledge/knowledge'
import { type Ref } from 'vue'
export interface knowledgeStateTypes {
baseInfo: knowledgeData | null
webInfo: any
documentsType: string
documentsFiles: UploadUserFile[]
}
const useKnowledgeStore = defineStore('knowledge', {
state: (): knowledgeStateTypes => ({
baseInfo: null,
webInfo: null,
documentsType: '',
documentsFiles: [],
}),
actions: {
saveBaseInfo(info: knowledgeData | null) {
this.baseInfo = info
},
saveWebInfo(info: any) {
this.webInfo = info
},
saveDocumentsType(val: string) {
this.documentsType = val
},
saveDocumentsFile(file: UploadUserFile[]) {
this.documentsFiles = file
},
// async asyncGetAllDataset(loading?: Ref<boolean>) {
// return new Promise((resolve, reject) => {
// knowledgeApi
// .getAllDataset(loading)
// .then((data) => {
// resolve(data)
// })
// .catch((error) => {
// reject(error)
// })
// })
// },
async asyncGetDatasetDetail(
workspace_id: string,
knowledge_id: string,
loading?: Ref<boolean>,
) {
return new Promise((resolve, reject) => {
knowledgeApi
.getKnowledgeDetail(workspace_id, knowledge_id, loading)
.then((data) => {
resolve(data)
})
.catch((error) => {
reject(error)
})
})
},
async asyncSyncDataset(id: string, sync_type: string, loading?: Ref<boolean>) {
return new Promise((resolve, reject) => {
knowledgeApi
.putSyncWebKnowledge(id, sync_type, loading)
.then((data) => {
resolve(data)
})
.catch((error) => {
reject(error)
})
})
},
},
})
export default useKnowledgeStore

View File

@ -1,4 +1,3 @@
$primary-color: #3370ff;
@font-face {
font-family: AlibabaPuHuiTi;
src:
@ -374,9 +373,6 @@ h5 {
}
// 颜色
.color-primary-1 {
background: rgba($primary-color, 0.1);
}
.color-primary {
color: var(--el-color-primary);
}

View File

@ -55,3 +55,32 @@
line-height: 30px;
}
}
/*
表格第一行插入自定义行
*/
.table-quick-append {
background: #ffffff;
.el-table__append-wrapper {
position: absolute;
top: 0;
border-bottom: var(--el-table-border);
width: 100%;
height: 49px;
box-sizing: border-box;
align-items: center;
display: flex;
padding: 0 12px;
background: #ffffff;
cursor: pointer;
z-index: 2;
&:hover {
background: var(--el-color-primary-light-9);
z-index: 1;
}
}
.el-table__body {
margin-top: 49px;
}
}

View File

@ -1,10 +1,9 @@
$primary-color: #3370ff;
:root {
--el-color-primary: #3370ff;
--el-color-success: #34c724;
--el-text-color-primary: #1f2329;
--el-border-radius-base: 6px;
// --el-menu-item-height: 45px;
--el-menu-item-height: 45px;
// --el-box-shadow-light: 0px 2px 4px 0px rgba(31, 35, 41, 0.12);
// --el-border-color: #dee0e3;
// --el-color-info: #8f959e !important;
@ -36,7 +35,7 @@ $primary-color: #3370ff;
}
}
.el-tree--highlight-current .el-tree-node.is-current > .el-tree-node__content {
background: rgba($primary-color, 0.1);
background: var(--el-color-primary-light-9);
color: var(--el-color-primary);
}
.el-tree-node__expand-icon {

View File

@ -1,3 +1,4 @@
$primary-color: #3370ff;
:root {
--app-base-px: 8px;
--app-layout-bg-color: #f5f6f7;

68
ui/src/utils/status.ts Normal file
View File

@ -0,0 +1,68 @@
import { type Dict } from '@/api/type/common'
interface TaskTypeInterface {
// 向量化
EMBEDDING: number
// 生成问题
GENERATE_PROBLEM: number
// 同步
SYNC: number
}
interface StateInterface {
// 等待
PENDING: '0'
// 执行中
STARTED: '1'
// 成功
SUCCESS: '2'
// 失败
FAILURE: '3'
// 取消任务
REVOKE: '4'
// 取消成功
REVOKED: '5'
IGNORED: 'n'
}
const TaskType: TaskTypeInterface = {
EMBEDDING: 1,
GENERATE_PROBLEM: 2,
SYNC: 3
}
const State: StateInterface = {
// 等待
PENDING: '0',
// 执行中
STARTED: '1',
// 成功
SUCCESS: '2',
// 失败
FAILURE: '3',
// 取消任务
REVOKE: '4',
// 取消成功
REVOKED: '5',
IGNORED: 'n'
}
class Status {
task_status: Dict<any>
constructor(status?: string) {
if (!status) {
status = ''
}
status = status.split('').reverse().join('')
this.task_status = {}
for (let key in TaskType) {
const value = TaskType[key as keyof TaskTypeInterface]
const index = value - 1
this.task_status[value] = status[index] ? status[index] : 'n'
}
}
toString() {
const r = []
for (let key in TaskType) {
const value = TaskType[key as keyof TaskTypeInterface]
r.push(this.task_status[value])
}
return r.reverse().join('')
}
}
export { Status, State, TaskType, type TaskTypeInterface, type StateInterface }

View File

@ -0,0 +1,48 @@
<template>
<el-dialog
v-model="dialogVisible"
:title="$t('components.selectParagraph.title')"
:before-close="close"
width="450"
>
<el-radio-group v-model="state" class="radio-block">
<el-radio value="error" size="large">{{
$t('components.selectParagraph.error')
}}</el-radio>
<el-radio value="all" size="large">{{ $t('components.selectParagraph.all') }}</el-radio>
</el-radio-group>
<template #footer>
<div class="dialog-footer">
<el-button @click="close">{{ $t('common.cancel') }} </el-button>
<el-button type="primary" @click="submit"> {{ $t('common.submit') }} </el-button>
</div>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const dialogVisible = ref<boolean>(false)
const state = ref<'all' | 'error'>('error')
const stateMap = {
all: ['0', '1', '2', '3', '4', '5', 'n'],
error: ['0', '1', '3', '4', '5', 'n']
}
const submit_handle = ref<(stateList: Array<string>) => void>()
const submit = () => {
if (submit_handle.value) {
submit_handle.value(stateMap[state.value])
}
close()
}
const open = (handle: (stateList: Array<string>) => void) => {
submit_handle.value = handle
dialogVisible.value = true
}
const close = () => {
submit_handle.value = undefined
dialogVisible.value = false
}
defineExpose({ open, close })
</script>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,233 @@
<template>
<el-dialog
:title="title"
v-model="dialogVisible"
:close-on-click-modal="false"
:close-on-press-escape="false"
:destroy-on-close="true"
width="550"
>
<el-form
label-position="top"
ref="webFormRef"
:rules="rules"
:model="form"
require-asterisk-position="right"
>
<el-form-item
:label="$t('views.document.form.source_url.label')"
prop="source_url"
v-if="isImport"
>
<el-input
v-model="form.source_url"
:placeholder="$t('views.document.form.source_url.placeholder')"
:rows="10"
type="textarea"
/>
</el-form-item>
<el-form-item
v-else-if="!isImport && documentType === '1'"
:label="$t('views.document.form.source_url.label')"
prop="source_url"
>
<el-input
v-model="form.source_url"
:placeholder="$t('views.document.form.source_url.requiredMessage')"
/>
</el-form-item>
<el-form-item :label="$t('views.document.form.selector.label')" v-if="documentType === '1'">
<el-input
v-model="form.selector"
:placeholder="$t('views.document.form.selector.placeholder')"
/>
</el-form-item>
<el-form-item v-if="!isImport">
<template #label>
<div class="flex align-center">
<span class="mr-4">{{ $t('views.document.form.hit_handling_method.label') }}</span>
<el-tooltip
effect="dark"
:content="$t('views.document.form.hit_handling_method.tooltip')"
placement="right"
>
<AppIcon iconName="app-warning" class="app-warning-icon"></AppIcon>
</el-tooltip>
</div>
</template>
<el-radio-group v-model="form.hit_handling_method" class="radio-block mt-4">
<template v-for="(value, key) of hitHandlingMethod" :key="key">
<el-radio :value="key">{{ $t(value) }} </el-radio>
</template>
</el-radio-group>
</el-form-item>
<el-form-item
prop="directly_return_similarity"
v-if="!isImport && form.hit_handling_method === 'directly_return'"
>
<div class="lighter w-full" style="margin-top: -20px">
<span>{{ $t('views.document.form.similarity.label') }}</span>
<el-input-number
v-model="form.directly_return_similarity"
:min="0"
:max="1"
:precision="3"
:step="0.1"
:value-on-clear="0"
controls-position="right"
size="small"
class="ml-4 mr-4"
/><span>{{ $t('views.document.form.similarity.placeholder') }}</span>
</div>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click.prevent="dialogVisible = false"> {{ $t('common.cancel') }} </el-button>
<el-button type="primary" @click="submit(webFormRef)" :loading="loading">
{{ $t('common.confirm') }}
</el-button>
</span>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { ref, reactive, watch } from 'vue'
import { useRoute } from 'vue-router'
import type { FormInstance } from 'element-plus'
import documentApi from '@/api/knowledge/document'
import { MsgSuccess } from '@/utils/message'
// import { hitHandlingMethod } from '@/enums/document'
import { t } from '@/locales'
const route = useRoute()
const {
params: { id }
} = route as any
const props = defineProps({
title: String
})
const emit = defineEmits(['refresh'])
const webFormRef = ref()
const loading = ref<boolean>(false)
const isImport = ref<boolean>(false)
const form = ref<any>({
source_url: '',
selector: '',
hit_handling_method: 'optimization',
directly_return_similarity: 0.9
})
//
const documentId = ref('')
const documentType = ref<string | number>('') //1: web0:
//
const documentList = ref<Array<string>>([])
const rules = reactive({
source_url: [
{
required: true,
message: t('views.document.form.source_url.requiredMessage'),
trigger: 'blur'
}
],
directly_return_similarity: [
{
required: true,
message: t('views.document.form.similarity.requiredMessage'),
trigger: 'blur'
}
]
})
const dialogVisible = ref<boolean>(false)
watch(dialogVisible, (bool) => {
if (!bool) {
form.value = {
source_url: '',
selector: '',
hit_handling_method: 'optimization',
directly_return_similarity: 0.9
}
isImport.value = false
documentType.value = ''
documentId.value = ''
documentList.value = []
}
})
const open = (row: any, list: Array<string>) => {
if (row) {
documentType.value = row.type
documentId.value = row.id
form.value = {
hit_handling_method: row.hit_handling_method,
directly_return_similarity: row.directly_return_similarity,
...row.meta
}
isImport.value = false
} else if (list) {
//
documentList.value = list
} else {
// web
documentType.value = '1'
isImport.value = true
}
dialogVisible.value = true
}
const submit = async (formEl: FormInstance | undefined) => {
if (!formEl) return
await formEl.validate((valid) => {
if (valid) {
if (isImport.value) {
const obj = {
source_url_list: form.value.source_url.split('\n'),
selector: form.value.selector
}
documentApi.postWebDocument(id, obj, loading).then(() => {
MsgSuccess(t('views.document.tip.importMessage'))
emit('refresh')
dialogVisible.value = false
})
} else {
if (documentId.value) {
const obj = {
hit_handling_method: form.value.hit_handling_method,
directly_return_similarity: form.value.directly_return_similarity,
meta: {
source_url: form.value.source_url,
selector: form.value.selector
}
}
documentApi.putDocument(id, documentId.value, obj, loading).then(() => {
MsgSuccess(t('common.settingSuccess'))
emit('refresh')
dialogVisible.value = false
})
} else if (documentList.value.length > 0) {
//
const obj = {
hit_handling_method: form.value.hit_handling_method,
directly_return_similarity: form.value.directly_return_similarity,
id_list: documentList.value
}
documentApi.batchEditHitHandling(id, obj, loading).then(() => {
MsgSuccess(t('common.settingSuccess'))
emit('refresh')
dialogVisible.value = false
})
}
}
}
})
}
defineExpose({ open })
</script>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,142 @@
<template>
<el-dialog
:title="$t('views.log.selectDataset')"
v-model="dialogVisible"
width="600"
class="select-dataset-dialog"
:close-on-click-modal="false"
:close-on-press-escape="false"
>
<template #header="{ titleId, titleClass }">
<div class="my-header flex">
<h4 :id="titleId" :class="titleClass">{{ $t('views.log.selectDataset') }}</h4>
<el-button link class="ml-16" @click="refresh">
<el-icon class="mr-4"><Refresh /></el-icon>{{ $t('common.refresh') }}
</el-button>
</div>
</template>
<div class="content-height">
<el-radio-group v-model="selectDataset" class="card__radio">
<el-scrollbar height="500">
<div class="p-16">
<el-row :gutter="12" v-loading="loading">
<el-col :span="12" v-for="(item, index) in datasetList" :key="index" class="mb-16">
<el-card shadow="never" :class="item.id === selectDataset ? 'active' : ''">
<el-radio :value="item.id" size="large">
<div class="flex align-center">
<AppAvatar
v-if="item?.type === '0'"
class="mr-8 avatar-blue"
shape="square"
:size="32"
>
<img src="@/assets/knowledge/icon_document.svg" style="width: 58%" alt="" />
</AppAvatar>
<AppAvatar
v-if="item?.type === '1'"
class="mr-8 avatar-purple"
shape="square"
:size="32"
>
<img src="@/assets/knowledge/icon_web.svg" style="width: 58%" alt="" />
</AppAvatar>
<AppAvatar
v-if="item?.type === '2'"
class="mr-8 avatar-purple"
shape="square"
:size="32"
style="background: none"
>
<img src="@/assets/knowledge/logo_lark.svg" style="width: 100%" alt="" />
</AppAvatar>
<span class="ellipsis" :title="item.name">
{{ item.name }}
</span>
</div>
</el-radio>
</el-card>
</el-col>
</el-row>
</div>
</el-scrollbar>
</el-radio-group>
</div>
<template #footer>
<span class="dialog-footer">
<el-button @click.prevent="dialogVisible = false"> {{ $t('common.cancel') }} </el-button>
<el-button type="primary" @click="submitHandle" :disabled="!selectDataset || loading">
{{ $t('common.confirm') }}
</el-button>
</span>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { ref, watch } from 'vue'
import { useRoute } from 'vue-router'
import documentApi from '@/api/knowledge/document'
import useStore from '@/stores'
const { dataset } = useStore()
const route = useRoute()
const {
params: { id } // iddatasetID
} = route as any
const emit = defineEmits(['refresh'])
const loading = ref<boolean>(false)
const dialogVisible = ref<boolean>(false)
const selectDataset = ref('')
const datasetList = ref<any>([])
const documentList = ref<any>([])
watch(dialogVisible, (bool) => {
if (!bool) {
selectDataset.value = ''
datasetList.value = []
documentList.value = []
}
})
const open = (list: any) => {
documentList.value = list
getDataset()
dialogVisible.value = true
}
const submitHandle = () => {
documentApi
.putMigrateMulDocument(id, selectDataset.value, documentList.value, loading)
.then((res) => {
emit('refresh')
dialogVisible.value = false
})
}
function getDataset() {
dataset.asyncGetAllDataset(loading).then((res: any) => {
datasetList.value = res.data?.filter((v: any) => v.id !== id)
})
}
const refresh = () => {
getDataset()
}
defineExpose({ open })
</script>
<style lang="scss">
.select-dataset-dialog {
padding: 0;
.el-dialog__header {
padding: 24px 24px 0 24px;
}
.el-dialog__body {
padding: 8px !important;
}
.el-dialog__footer {
padding: 0 24px 24px;
}
}
</style>

View File

@ -0,0 +1,87 @@
<template>
<el-popover
v-model:visible="visible"
placement="top"
trigger="hover"
:popper-style="{ width: 'auto' }"
>
<template #default
><StatusTable
v-if="visible"
:status="status"
:statusMeta="statusMeta"
:taskTypeMap="taskTypeMap"
:stateMap="stateMap"
></StatusTable>
</template>
<template #reference>
<el-text v-if="aggStatus?.value === State.SUCCESS || aggStatus?.value === State.REVOKED">
<el-icon class="success"><SuccessFilled /></el-icon>
{{ stateMap[aggStatus.value](aggStatus.key) }}
</el-text>
<el-text v-else-if="aggStatus?.value === State.FAILURE">
<el-icon class="danger"><CircleCloseFilled /></el-icon>
{{ stateMap[aggStatus.value](aggStatus.key) }}
</el-text>
<el-text v-else-if="aggStatus?.value === State.STARTED">
<el-icon class="is-loading primary"><Loading /></el-icon>
{{ stateMap[aggStatus.value](aggStatus.key) }}
</el-text>
<el-text v-else-if="aggStatus?.value === State.PENDING">
<el-icon class="is-loading primary"><Loading /></el-icon>
{{ stateMap[aggStatus.value](aggStatus.key) }}
</el-text>
<el-text v-else-if="aggStatus?.value === State.REVOKE">
<el-icon class="is-loading primary"><Loading /></el-icon>
{{ stateMap[aggStatus.value](aggStatus.key) }}
</el-text>
</template>
</el-popover>
</template>
<script setup lang="ts">
import { computed, ref } from 'vue'
import { TaskType, State } from '@/utils/status'
import StatusTable from '@/views/document/component/StatusTable.vue'
import { t } from '@/locales'
const props = defineProps<{ status: string; statusMeta: any }>()
const visible = ref<boolean>(false)
const checkList: Array<string> = [
State.REVOKE,
State.STARTED,
State.PENDING,
State.FAILURE,
State.REVOKED,
State.SUCCESS
]
const aggStatus = computed(() => {
let obj = { key: 0, value: '' }
for (const i in checkList) {
const state = checkList[i]
const index = props.status.indexOf(state)
if (index > -1) {
obj = { key: props.status.length - index, value: state }
break
}
}
return obj
})
const startedMap = {
[TaskType.EMBEDDING]: t('views.document.fileStatus.EMBEDDING'),
[TaskType.GENERATE_PROBLEM]: t('views.document.fileStatus.GENERATE'),
[TaskType.SYNC]: t('views.document.fileStatus.SYNC')
}
const taskTypeMap = {
[TaskType.EMBEDDING]: t('views.dataset.setting.vectorization'),
[TaskType.GENERATE_PROBLEM]: t('views.document.generateQuestion.title'),
[TaskType.SYNC]: t('views.dataset.setting.sync')
}
const stateMap: any = {
[State.PENDING]: (type: number) => t('views.document.fileStatus.PENDING'),
[State.STARTED]: (type: number) => startedMap[type],
[State.REVOKE]: (type: number) => t('views.document.fileStatus.REVOKE'),
[State.REVOKED]: (type: number) => t('views.document.fileStatus.SUCCESS'),
[State.FAILURE]: (type: number) => t('views.document.fileStatus.FAILURE'),
[State.SUCCESS]: (type: number) => t('views.document.fileStatus.SUCCESS'),
}
</script>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,109 @@
<template>
<div v-for="status in statusTable" :key="status.type" >
<span> {{ taskTypeMap[status.type] }}</span>
<span>
<el-text v-if="status.state === State.SUCCESS || status.state === State.REVOKED">
<el-icon class="success"><SuccessFilled /></el-icon>
{{ stateMap[status.state](status.type) }}
</el-text>
<el-text v-else-if="status.state === State.FAILURE">
<el-icon class="danger"><CircleCloseFilled /></el-icon>
{{ stateMap[status.state](status.type) }}
</el-text>
<el-text v-else-if="status.state === State.STARTED">
<el-icon class="is-loading primary"><Loading /></el-icon>
{{ stateMap[status.state](status.type) }}
</el-text>
<el-text v-else-if="status.state === State.PENDING">
<el-icon class="is-loading primary"><Loading /></el-icon>
{{ stateMap[status.state](status.type) }}
</el-text>
<el-text v-else-if="status.state === State.REVOKE">
<el-icon class="is-loading primary"><Loading /></el-icon>
{{ stateMap[status.state](status.type) }}
</el-text>
</span>
<span
class="ml-8 lighter"
:style="{ color: [State.FAILURE, State.REVOKED].includes(status.state) ? '#F54A45' : '' }"
>
{{ $t('views.document.fileStatus.finish') }}
{{
Object.keys(status.aggs ? status.aggs : {})
.filter((k) => k == State.SUCCESS)
.map((k) => status.aggs[k])
.reduce((x: any, y: any) => x + y, 0)
}}/{{
Object.values(status.aggs ? status.aggs : {}).reduce((x: any, y: any) => x + y, 0)
}}</span
>
<el-text type="info" class="ml-12">
{{
status.time
? status.time[status.state == State.REVOKED ? State.REVOKED : State.PENDING]?.substring(
0,
19
)
: undefined
}}
</el-text>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { Status, TaskType, State, type TaskTypeInterface } from '@/utils/status'
import { mergeWith } from 'lodash'
const props = defineProps<{ status: string; statusMeta: any; stateMap: any; taskTypeMap: any }>()
const parseAgg = (agg: { count: number; status: string }) => {
const status = new Status(agg.status)
return Object.keys(TaskType)
.map((key) => {
const value = TaskType[key as keyof TaskTypeInterface]
return { [value]: { [status.task_status[value]]: agg.count } }
})
.reduce((x, y) => ({ ...x, ...y }), {})
}
const customizer: (x: any, y: any) => any = (objValue: any, srcValue: any) => {
if (objValue == undefined && srcValue) {
return srcValue
}
if (srcValue == undefined && objValue) {
return objValue
}
//
if (typeof objValue === 'object' && typeof srcValue === 'object') {
// object
return mergeWith(objValue, srcValue, customizer)
} else {
//
return objValue + srcValue
}
}
const aggs = computed(() => {
return (props.statusMeta.aggs ? props.statusMeta.aggs : [])
.map((agg: any) => {
return parseAgg(agg)
})
.reduce((x: any, y: any) => {
return mergeWith(x, y, customizer)
}, {})
})
const statusTable = computed(() => {
return Object.keys(TaskType)
.map((key) => {
const value = TaskType[key as keyof TaskTypeInterface]
const parseStatus = new Status(props.status)
return {
type: value,
state: parseStatus.task_status[value],
aggs: aggs.value[value],
time: props.statusMeta.state_time[value]
}
})
.filter((item) => item.state !== State.IGNORED)
})
</script>
<style lang="scss"></style>

View File

@ -0,0 +1,953 @@
<template>
<div class="main-calc-height">
<div class="p-24">
<div class="flex-between">
<div>
<el-button
v-if="datasetDetail.type === '0'"
type="primary"
@click="router.push({ path: '/dataset/upload', query: { id: id } })"
>{{ $t('views.document.uploadDocument') }}
</el-button>
<el-button v-if="datasetDetail.type === '1'" type="primary" @click="importDoc"
>{{ $t('views.document.importDocument') }}
</el-button>
<el-button
@click="syncMulDocument"
:disabled="multipleSelection.length === 0"
v-if="datasetDetail.type === '1'"
>{{ $t('views.document.syncDocument') }}
</el-button>
<el-button
v-if="datasetDetail.type === '2'"
type="primary"
@click="
router.push({
path: '/dataset/import',
query: { id: id, folder_token: datasetDetail.meta.folder_token },
})
"
>{{ $t('views.document.importDocument') }}
</el-button>
<el-button
@click="syncLarkMulDocument"
:disabled="multipleSelection.length === 0"
v-if="datasetDetail.type === '2'"
>{{ $t('views.document.syncDocument') }}
</el-button>
<el-button @click="openDatasetDialog()" :disabled="multipleSelection.length === 0">
{{ $t('views.document.setting.migration') }}
</el-button>
<el-button @click="batchRefresh" :disabled="multipleSelection.length === 0">
{{ $t('views.knowledge.setting.vectorization') }}
</el-button>
<el-button @click="openGenerateDialog()" :disabled="multipleSelection.length === 0">
{{ $t('views.document.generateQuestion.title') }}
</el-button>
<el-button @click="openBatchEditDocument" :disabled="multipleSelection.length === 0">
{{ $t('common.setting') }}
</el-button>
<el-button @click="deleteMulDocument" :disabled="multipleSelection.length === 0">
{{ $t('common.delete') }}
</el-button>
</div>
<el-input
v-model="filterText"
:placeholder="$t('common.searchBar.placeholder')"
prefix-icon="Search"
class="w-240"
@change="getList"
clearable
/>
</div>
<app-table
ref="multipleTableRef"
class="mt-16"
:data="documentData"
:pagination-config="paginationConfig"
:quick-create="datasetDetail.type === '0'"
@sizeChange="handleSizeChange"
@changePage="getList"
@cell-mouse-enter="cellMouseEnter"
@cell-mouse-leave="cellMouseLeave"
@creatQuick="creatQuickHandle"
@row-click="rowClickHandle"
@selection-change="handleSelectionChange"
@sort-change="handleSortChange"
v-loading="loading"
:row-key="(row: any) => row.id"
:storeKey="storeKey"
>
<el-table-column type="selection" width="55" :reserve-selection="true" />
<el-table-column prop="name" :label="$t('views.document.table.name')" min-width="280">
<template #default="{ row }">
<ReadWrite
@change="editName($event, row.id)"
:data="row.name"
:showEditIcon="row.id === currentMouseId"
/>
</template>
</el-table-column>
<el-table-column
prop="char_length"
:label="$t('views.document.table.char_length')"
align="right"
min-width="90"
sortable
>
<template #default="{ row }">
{{ numberFormat(row.char_length) }}
</template>
</el-table-column>
<el-table-column
prop="paragraph_count"
:label="$t('views.document.table.paragraph')"
align="right"
min-width="90"
sortable
/>
<el-table-column prop="status" :label="$t('views.document.fileStatus.label')" width="130">
<template #header>
<div>
<span>{{ $t('views.document.fileStatus.label') }}</span>
<el-dropdown trigger="click" @command="dropdownHandle">
<el-button
style="margin-top: 1px"
link
:type="filterMethod['status'] ? 'primary' : ''"
>
<el-icon>
<Filter />
</el-icon>
</el-button>
<template #dropdown>
<el-dropdown-menu style="width: 100px">
<el-dropdown-item
:class="filterMethod['status'] ? '' : 'is-active'"
:command="beforeCommand('status', '')"
class="justify-center"
>{{ $t('views.document.table.all') }}
</el-dropdown-item>
<el-dropdown-item
:class="filterMethod['status'] === State.SUCCESS ? 'is-active' : ''"
class="justify-center"
:command="beforeCommand('status', State.SUCCESS)"
>{{ $t('views.document.fileStatus.SUCCESS') }}
</el-dropdown-item>
<el-dropdown-item
:class="filterMethod['status'] === State.FAILURE ? 'is-active' : ''"
class="justify-center"
:command="beforeCommand('status', State.FAILURE)"
>{{ $t('views.document.fileStatus.FAILURE') }}
</el-dropdown-item>
<el-dropdown-item
:class="
filterMethod['status'] === State.STARTED &&
filterMethod['task_type'] == TaskType.EMBEDDING
? 'is-active'
: ''
"
class="justify-center"
:command="beforeCommand('status', State.STARTED, TaskType.EMBEDDING)"
>{{ $t('views.document.fileStatus.EMBEDDING') }}
</el-dropdown-item>
<el-dropdown-item
:class="filterMethod['status'] === State.PENDING ? 'is-active' : ''"
class="justify-center"
:command="beforeCommand('status', State.PENDING)"
>{{ $t('views.document.fileStatus.PENDING') }}
</el-dropdown-item>
<el-dropdown-item
:class="
filterMethod['status'] === State.STARTED &&
filterMethod['task_type'] === TaskType.GENERATE_PROBLEM
? 'is-active'
: ''
"
class="justify-center"
:command="beforeCommand('status', State.STARTED, TaskType.GENERATE_PROBLEM)"
>{{ $t('views.document.fileStatus.GENERATE') }}
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</template>
<template #default="{ row }">
<StatusVlue :status="row.status" :status-meta="row.status_meta"></StatusVlue>
</template>
</el-table-column>
<el-table-column width="130">
<template #header>
<div>
<span>{{ $t('views.document.enableStatus.label') }}</span>
<el-dropdown trigger="click" @command="dropdownHandle">
<el-button
style="margin-top: 1px"
link
:type="filterMethod['is_active'] ? 'primary' : ''"
>
<el-icon>
<Filter />
</el-icon>
</el-button>
<template #dropdown>
<el-dropdown-menu style="width: 100px">
<el-dropdown-item
:class="filterMethod['is_active'] === '' ? 'is-active' : ''"
:command="beforeCommand('is_active', '')"
class="justify-center"
>{{ $t('views.document.table.all') }}
</el-dropdown-item>
<el-dropdown-item
:class="filterMethod['is_active'] === true ? 'is-active' : ''"
class="justify-center"
:command="beforeCommand('is_active', true)"
>{{ $t('views.document.enableStatus.enable') }}
</el-dropdown-item>
<el-dropdown-item
:class="filterMethod['is_active'] === false ? 'is-active' : ''"
class="justify-center"
:command="beforeCommand('is_active', false)"
>{{ $t('views.document.enableStatus.close') }}
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</template>
<template #default="{ row }">
<div @click.stop>
<el-switch
:loading="loading"
size="small"
v-model="row.is_active"
:before-change="() => changeState(row)"
/>
</div>
</template>
</el-table-column>
<el-table-column width="170">
<template #header>
<div>
<span>{{ $t('views.document.form.hit_handling_method.label') }}</span>
<el-dropdown trigger="click" @command="dropdownHandle">
<el-button
style="margin-top: 1px"
link
:type="filterMethod['hit_handling_method'] ? 'primary' : ''"
>
<el-icon>
<Filter />
</el-icon>
</el-button>
<template #dropdown>
<el-dropdown-menu style="width: 150px">
<el-dropdown-item
:class="filterMethod['hit_handling_method'] ? '' : 'is-active'"
:command="beforeCommand('hit_handling_method', '')"
class="justify-center"
>{{ $t('views.document.table.all') }}
</el-dropdown-item>
<template v-for="(value, key) of hitHandlingMethod" :key="key">
<el-dropdown-item
:class="filterMethod['hit_handling_method'] === key ? 'is-active' : ''"
class="justify-center"
:command="beforeCommand('hit_handling_method', key)"
>{{ $t(value) }}
</el-dropdown-item>
</template>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</template>
<template #default="{ row }">
{{ $t(hitHandlingMethod[row.hit_handling_method as keyof typeof hitHandlingMethod]) }}
</template>
</el-table-column>
<el-table-column prop="create_time" :label="$t('common.createTime')" width="175" sortable>
<template #default="{ row }">
{{ datetimeFormat(row.create_time) }}
</template>
</el-table-column>
<el-table-column
prop="update_time"
:label="$t('views.document.table.updateTime')"
width="175"
sortable
>
<template #default="{ row }">
{{ datetimeFormat(row.update_time) }}
</template>
</el-table-column>
<el-table-column :label="$t('common.operation')" align="left" width="110" fixed="right">
<template #default="{ row }">
<div v-if="datasetDetail.type === '0'">
<span class="mr-4">
<el-tooltip
effect="dark"
v-if="
([State.STARTED, State.PENDING] as Array<string>).includes(
getTaskState(row.status, TaskType.EMBEDDING),
)
"
:content="$t('views.document.setting.cancelVectorization')"
placement="top"
>
<el-button type="primary" text @click.stop="cancelTask(row, TaskType.EMBEDDING)">
<AppIcon iconName="app-close" style="font-size: 16px"></AppIcon>
</el-button>
</el-tooltip>
<el-tooltip
v-else
effect="dark"
:content="$t('views.dataset.setting.vectorization')"
placement="top"
>
<el-button type="primary" text @click.stop="refreshDocument(row)">
<AppIcon iconName="app-document-refresh" style="font-size: 16px"></AppIcon>
</el-button>
</el-tooltip>
</span>
<span class="mr-4">
<el-tooltip effect="dark" :content="$t('common.setting')" placement="top">
<el-button type="primary" text @click.stop="settingDoc(row)">
<el-icon><Setting /></el-icon>
</el-button>
</el-tooltip>
</span>
<span @click.stop>
<el-dropdown trigger="click">
<el-button text type="primary">
<el-icon><MoreFilled /></el-icon>
</el-button>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item
v-if="
([State.STARTED, State.PENDING] as Array<string>).includes(
getTaskState(row.status, TaskType.GENERATE_PROBLEM),
)
"
@click="cancelTask(row, TaskType.GENERATE_PROBLEM)"
>
<el-icon><Connection /></el-icon>
{{ $t('views.document.setting.cancelGenerateQuestion') }}
</el-dropdown-item>
<el-dropdown-item v-else @click="openGenerateDialog(row)">
<el-icon><Connection /></el-icon>
{{ $t('views.document.generateQuestion.title') }}
</el-dropdown-item>
<el-dropdown-item @click="openDatasetDialog(row)">
<AppIcon iconName="app-migrate"></AppIcon>
{{ $t('views.document.setting.migration') }}
</el-dropdown-item>
<el-dropdown-item @click="exportDocument(row)">
<AppIcon iconName="app-export"></AppIcon>
{{ $t('views.document.setting.export') }} Excel
</el-dropdown-item>
<el-dropdown-item @click="exportDocumentZip(row)">
<AppIcon iconName="app-export"></AppIcon>
{{ $t('views.document.setting.export') }} Zip
</el-dropdown-item>
<el-dropdown-item icon="Delete" @click.stop="deleteDocument(row)">{{
$t('common.delete')
}}</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</span>
</div>
<div v-if="datasetDetail.type === '1' || datasetDetail.type === '2'">
<span class="mr-4">
<el-tooltip
effect="dark"
:content="$t('views.dataset.setting.sync')"
placement="top"
>
<el-button type="primary" text @click.stop="syncDocument(row)">
<el-icon><Refresh /></el-icon>
</el-button>
</el-tooltip>
</span>
<span class="mr-4">
<el-tooltip
effect="dark"
v-if="
([State.STARTED, State.PENDING] as Array<string>).includes(
getTaskState(row.status, TaskType.EMBEDDING),
)
"
:content="$t('views.document.setting.cancelVectorization')"
placement="top"
>
<el-button type="primary" text @click.stop="cancelTask(row, TaskType.EMBEDDING)">
<AppIcon iconName="app-close" style="font-size: 16px"></AppIcon>
</el-button>
</el-tooltip>
<el-tooltip
effect="dark"
v-else
:content="$t('views.dataset.setting.vectorization')"
placement="top"
>
<el-button type="primary" text @click.stop="refreshDocument(row)">
<AppIcon iconName="app-document-refresh" style="font-size: 16px"></AppIcon>
</el-button>
</el-tooltip>
</span>
<span @click.stop>
<el-dropdown trigger="click">
<el-button text type="primary">
<el-icon><MoreFilled /></el-icon>
</el-button>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item icon="Setting" @click="settingDoc(row)">{{
$t('common.setting')
}}</el-dropdown-item>
<el-dropdown-item
v-if="
([State.STARTED, State.PENDING] as Array<string>).includes(
getTaskState(row.status, TaskType.GENERATE_PROBLEM),
)
"
@click="cancelTask(row, TaskType.GENERATE_PROBLEM)"
>
<el-icon><Connection /></el-icon>
{{ $t('views.document.setting.cancelGenerateQuestion') }}
</el-dropdown-item>
<el-dropdown-item v-else @click="openGenerateDialog(row)">
<el-icon><Connection /></el-icon>
{{ $t('views.document.generateQuestion.title') }}
</el-dropdown-item>
<el-dropdown-item @click="openDatasetDialog(row)">
<AppIcon iconName="app-migrate"></AppIcon>
{{ $t('views.document.setting.migration') }}</el-dropdown-item
>
<el-dropdown-item @click="exportDocument(row)">
<AppIcon iconName="app-export"></AppIcon>
{{ $t('views.document.setting.export') }} Excel
</el-dropdown-item>
<el-dropdown-item @click="exportDocumentZip(row)">
<AppIcon iconName="app-export"></AppIcon>
{{ $t('views.document.setting.export') }} Zip
</el-dropdown-item>
<el-dropdown-item icon="Delete" @click.stop="deleteDocument(row)">{{
$t('common.delete')
}}</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</span>
</div>
</template>
</el-table-column>
</app-table>
</div>
<ImportDocumentDialog ref="ImportDocumentDialogRef" :title="title" @refresh="refresh" />
<SyncWebDialog ref="SyncWebDialogRef" @refresh="refresh" />
<!-- 选择知识库 -->
<SelectDatasetDialog ref="SelectDatasetDialogRef" @refresh="refreshMigrate" />
<GenerateRelatedDialog ref="GenerateRelatedDialogRef" @refresh="getList" />
</div>
<div class="mul-operation w-full flex" v-if="multipleSelection.length !== 0">
<el-button :disabled="multipleSelection.length === 0" @click="cancelTaskHandle(1)">
{{ $t('views.document.setting.cancelVectorization') }}
</el-button>
<el-button :disabled="multipleSelection.length === 0" @click="cancelTaskHandle(2)">
{{ $t('views.document.setting.cancelGenerate') }}
</el-button>
<el-text type="info" class="secondary ml-24">
{{ $t('views.document.selected') }} {{ multipleSelection.length }}
{{ $t('views.document.items') }}
</el-text>
<el-button class="ml-16" type="primary" link @click="clearSelection">
{{ $t('common.clear') }}
</el-button>
</div>
<EmbeddingContentDialog ref="embeddingContentDialogRef"></EmbeddingContentDialog>
</template>
<script setup lang="ts">
import { ref, onMounted, onBeforeUnmount, computed } from 'vue'
import { useRouter, useRoute, onBeforeRouteLeave, onBeforeRouteUpdate } from 'vue-router'
import { ElTable } from 'element-plus'
import documentApi from '@/api/knowledge/document'
import ImportDocumentDialog from './component/ImportDocumentDialog.vue'
import SyncWebDialog from '@/views/knowledge/component/SyncWebDialog.vue'
import SelectDatasetDialog from './component/SelectDatasetDialog.vue'
// import { numberFormat } from '@/utils/utils'
// import { datetimeFormat } from '@/utils/time'
// import { hitHandlingMethod } from '@/enums/document'
import { MsgSuccess, MsgConfirm, MsgError } from '@/utils/message'
import useStore from '@/stores'
import StatusVlue from '@/views/document/component/Status.vue'
// import GenerateRelatedDialog from '@/components/generate-related-dialog/index.vue'
import EmbeddingContentDialog from '@/views/document/component/EmbeddingContentDialog.vue'
// import { TaskType, State } from '@/utils/status'
import { t } from '@/locales'
const router = useRouter()
const route = useRoute()
const {
params: { id }, // iddatasetID
} = route as any
const { common, knowledge, document } = useStore()
const storeKey = 'documents'
const getTaskState = (status: string, taskType: number) => {
const statusList = status.split('').reverse()
return taskType - 1 > statusList.length + 1 ? 'n' : statusList[taskType - 1]
}
onBeforeRouteUpdate(() => {
common.savePage(storeKey, null)
common.saveCondition(storeKey, null)
})
onBeforeRouteLeave((to: any) => {
if (to.name !== 'Paragraph') {
common.savePage(storeKey, null)
common.saveCondition(storeKey, null)
} else {
common.saveCondition(storeKey, {
filterText: filterText.value,
filterMethod: filterMethod.value,
})
}
})
const beforePagination = computed(() => common.paginationConfig[storeKey])
const beforeSearch = computed(() => common.search[storeKey])
const embeddingContentDialogRef = ref<InstanceType<typeof EmbeddingContentDialog>>()
const SyncWebDialogRef = ref()
const loading = ref(false)
let interval: any
const filterText = ref('')
const filterMethod = ref<any>({})
const orderBy = ref<string>('')
const documentData = ref<any[]>([])
const currentMouseId = ref(null)
const datasetDetail = ref<any>({})
const paginationConfig = ref({
current_page: 1,
page_size: 10,
total: 0,
})
const ImportDocumentDialogRef = ref()
const multipleTableRef = ref<InstanceType<typeof ElTable>>()
const multipleSelection = ref<any[]>([])
const title = ref('')
const SelectDatasetDialogRef = ref()
const exportDocument = (document: any) => {
documentApi.exportDocument(document.name, document.dataset_id, document.id, loading).then(() => {
MsgSuccess(t('common.exportSuccess'))
})
}
const exportDocumentZip = (document: any) => {
documentApi
.exportDocumentZip(document.name, document.dataset_id, document.id, loading)
.then(() => {
MsgSuccess(t('common.exportSuccess'))
})
}
function cancelTaskHandle(val: any) {
const arr: string[] = []
multipleSelection.value.map((v) => {
if (v) {
arr.push(v.id)
}
})
const obj = {
id_list: arr,
type: val,
}
documentApi.batchCancelTask(id, obj, loading).then(() => {
MsgSuccess(t('views.document.tip.cancelSuccess'))
multipleTableRef.value?.clearSelection()
})
}
function clearSelection() {
multipleTableRef.value?.clearSelection()
}
function openDatasetDialog(row?: any) {
const arr: string[] = []
if (row) {
arr.push(row.id)
} else {
multipleSelection.value.map((v) => {
if (v) {
arr.push(v.id)
}
})
}
SelectDatasetDialogRef.value.open(arr)
}
function dropdownHandle(obj: any) {
filterMethod.value[obj.attr] = obj.command
if (obj.attr == 'status') {
filterMethod.value['task_type'] = obj.task_type
}
getList()
}
function beforeCommand(attr: string, val: any, task_type?: number) {
return {
attr: attr,
command: val,
task_type,
}
}
const cancelTask = (row: any, task_type: number) => {
documentApi.cancelTask(row.dataset_id, row.id, { type: task_type }).then(() => {
MsgSuccess(t('views.document.tip.sendMessage'))
})
}
function importDoc() {
title.value = t('views.document.importDocument')
ImportDocumentDialogRef.value.open()
}
function settingDoc(row: any) {
title.value = t('common.setting')
ImportDocumentDialogRef.value.open(row)
}
const handleSelectionChange = (val: any[]) => {
multipleSelection.value = val
}
function openBatchEditDocument() {
title.value = t('common.setting')
const arr: string[] = multipleSelection.value.map((v) => v.id)
ImportDocumentDialogRef.value.open(null, arr)
}
/**
* 初始化轮询
*/
const initInterval = () => {
interval = setInterval(() => {
getList(true)
}, 6000)
}
/**
* 关闭轮询
*/
const closeInterval = () => {
if (interval) {
clearInterval(interval)
}
}
function syncDocument(row: any) {
console.log('row', row)
if (row.type === '1') {
syncWebDocument(row)
} else {
syncLarkDocument(row)
}
}
function syncLarkDocument(row: any) {
MsgConfirm(t('views.document.sync.confirmTitle'), t('views.document.sync.confirmMessage1'), {
confirmButtonText: t('views.document.sync.label'),
confirmButtonClass: 'danger',
})
.then(() => {
documentApi.putLarkDocumentSync(id, row.id).then(() => {
getList()
})
})
.catch(() => {})
}
function syncWebDocument(row: any) {
if (row.meta?.source_url) {
MsgConfirm(t('views.document.sync.confirmTitle'), t('views.document.sync.confirmMessage1'), {
confirmButtonText: t('views.document.sync.label'),
confirmButtonClass: 'danger',
})
.then(() => {
documentApi.putDocumentSync(row.dataset_id, row.id).then(() => {
getList()
})
})
.catch(() => {})
} else {
MsgConfirm(t('common.tip'), t('views.document.sync.confirmMessage2'), {
confirmButtonText: t('common.confirm'),
type: 'warning',
})
.then(() => {})
.catch(() => {})
}
}
function refreshDocument(row: any) {
const embeddingDocument = (stateList: Array<string>) => {
return documentApi.putDocumentRefresh(row.dataset_id, row.id, stateList).then(() => {
getList()
})
}
embeddingContentDialogRef.value?.open(embeddingDocument)
}
function rowClickHandle(row: any, column: any) {
if (column && column.type === 'selection') {
return
}
router.push({ path: `/dataset/${id}/${row.id}` })
}
/*
快速创建空白文档
*/
function creatQuickHandle(val: string) {
loading.value = true
const obj = [{ name: val }]
document
.asyncPostDocument(id, obj)
.then(() => {
getList()
MsgSuccess(t('common.createSuccess'))
})
.catch(() => {
loading.value = false
})
}
function syncMulDocument() {
const arr: string[] = []
multipleSelection.value.map((v) => {
if (v) {
arr.push(v.id)
}
})
documentApi.delMulSyncDocument(id, arr, loading).then(() => {
MsgSuccess(t('views.document.sync.successMessage'))
getList()
})
}
function syncLarkMulDocument() {
const arr: string[] = []
multipleSelection.value.map((v) => {
if (v) {
arr.push(v.id)
}
})
documentApi.delMulLarkSyncDocument(id, arr, loading).then(() => {
MsgSuccess(t('views.document.sync.successMessage'))
getList()
})
}
function deleteMulDocument() {
MsgConfirm(
`${t('views.document.delete.confirmTitle1')} ${multipleSelection.value.length} ${t('views.document.delete.confirmTitle2')}`,
t('views.document.delete.confirmMessage'),
{
confirmButtonText: t('common.confirm'),
confirmButtonClass: 'danger',
},
)
.then(() => {
const arr: string[] = []
multipleSelection.value.map((v) => {
if (v) {
arr.push(v.id)
}
})
documentApi.delMulDocument(id, arr, loading).then(() => {
MsgSuccess(t('views.document.delete.successMessage'))
multipleTableRef.value?.clearSelection()
getList()
})
})
.catch(() => {})
}
function batchRefresh() {
const arr: string[] = multipleSelection.value.map((v) => v.id)
const embeddingBatchDocument = (stateList: Array<string>) => {
documentApi.batchRefresh(id, arr, stateList, loading).then(() => {
MsgSuccess(t('views.document.tip.vectorizationSuccess'))
multipleTableRef.value?.clearSelection()
})
}
embeddingContentDialogRef.value?.open(embeddingBatchDocument)
}
function deleteDocument(row: any) {
MsgConfirm(
`${t('views.document.delete.confirmTitle3')} ${row.name} ?`,
`${t('views.document.delete.confirmMessage1')} ${row.paragraph_count} ${t('views.document.delete.confirmMessage2')}`,
{
confirmButtonText: t('common.confirm'),
confirmButtonClass: 'danger',
},
)
.then(() => {
documentApi.delDocument(id, row.id, loading).then(() => {
MsgSuccess(t('common.deleteSuccess'))
getList()
})
})
.catch(() => {})
}
/*
更新名称或状态
*/
function updateData(documentId: string, data: any, msg: string) {
documentApi
.putDocument(id, documentId, data, loading)
.then((res) => {
const index = documentData.value.findIndex((v) => v.id === documentId)
documentData.value.splice(index, 1, res.data)
MsgSuccess(msg)
return true
})
.catch(() => {
return false
})
}
function changeState(row: any) {
const obj = {
is_active: !row.is_active,
}
const str = !row.is_active ? t('common.status.enableSuccess') : t('common.status.disableSuccess')
currentMouseId.value && updateData(row.id, obj, str)
}
function editName(val: string, id: string) {
if (val) {
const obj = {
name: val,
}
updateData(id, obj, t('common.modifySuccess'))
} else {
MsgError(t('views.document.tip.nameMessage'))
}
}
function cellMouseEnter(row: any) {
currentMouseId.value = row.id
}
function cellMouseLeave() {
currentMouseId.value = null
}
function handleSizeChange() {
paginationConfig.value.current_page = 1
getList()
}
function handleSortChange({ prop, order }: { prop: string; order: string }) {
orderBy.value = order === 'ascending' ? prop : `-${prop}`
getList()
}
function getList(bool?: boolean) {
const param = {
...(filterText.value && { name: filterText.value }),
...filterMethod.value,
order_by: orderBy.value,
}
documentApi
.getDocument('default', id as string, paginationConfig.value, param, bool ? undefined : loading)
.then((res) => {
documentData.value = res.data.records
paginationConfig.value.total = res.data.total
})
}
function getDetail() {
knowledge.asyncGetDatasetDetail('default', id, loading).then((res: any) => {
datasetDetail.value = res.data
})
}
function refreshMigrate() {
multipleTableRef.value?.clearSelection()
getList()
}
function refresh() {
paginationConfig.value.current_page = 1
getList()
}
const GenerateRelatedDialogRef = ref()
function openGenerateDialog(row?: any) {
const arr: string[] = []
if (row) {
arr.push(row.id)
} else {
multipleSelection.value.map((v) => {
if (v) {
arr.push(v.id)
}
})
}
GenerateRelatedDialogRef.value.open(arr, 'document')
}
onMounted(() => {
getDetail()
if (beforePagination.value) {
paginationConfig.value = beforePagination.value
}
if (beforeSearch.value) {
filterText.value = beforeSearch.value['filterText']
filterMethod.value = beforeSearch.value['filterMethod']
}
getList()
//
// initInterval()
})
onBeforeUnmount(() => {
//
closeInterval()
})
</script>
<style lang="scss" scoped>
.document-main {
box-sizing: border-box;
.mul-operation {
position: fixed;
margin-left: var(--sidebar-width);
bottom: 0;
right: 24px;
width: calc(100% - var(--sidebar-width) - 48px);
padding: 16px 24px;
box-sizing: border-box;
background: #ffffff;
z-index: 22;
box-shadow: 0px -2px 4px 0px rgba(31, 35, 41, 0.08);
}
}
</style>

View File

@ -0,0 +1,147 @@
<template>
<el-form
ref="FormRef"
:model="form"
:rules="rules"
label-position="top"
require-asterisk-position="right"
v-loading="loading"
>
<el-form-item :label="$t('views.dataset.datasetForm.form.datasetName.label')" prop="name">
<el-input
v-model="form.name"
:placeholder="$t('views.dataset.datasetForm.form.datasetName.placeholder')"
maxlength="64"
show-word-limit
@blur="form.name = form.name.trim()"
/>
</el-form-item>
<el-form-item
:label="$t('views.dataset.datasetForm.form.datasetDescription.label')"
prop="desc"
>
<el-input
v-model="form.desc"
type="textarea"
:placeholder="$t('views.dataset.datasetForm.form.datasetDescription.placeholder')"
maxlength="256"
show-word-limit
:autosize="{ minRows: 3 }"
@blur="form.desc = form.desc.trim()"
/>
</el-form-item>
<el-form-item
:label="$t('views.dataset.datasetForm.form.EmbeddingModel.label')"
prop="embedding_mode_id"
>
<ModelSelect
v-model="form.embedding_mode_id"
:placeholder="$t('views.dataset.datasetForm.form.EmbeddingModel.placeholder')"
:options="modelOptions"
:model-type="'EMBEDDING'"
showFooter
></ModelSelect>
</el-form-item>
</el-form>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted, onUnmounted, computed, watch } from 'vue'
import { groupBy } from 'lodash'
import useStore from '@/stores'
import type { datasetData } from '@/api/type/knowledge'
import { t } from '@/locales'
const props = defineProps({
data: {
type: Object,
default: () => {}
}
})
const { model } = useStore()
const form = ref<datasetData>({
name: '',
desc: '',
embedding_mode_id: ''
})
const rules = reactive({
name: [
{
required: true,
message: t('views.dataset.datasetForm.form.datasetName.requiredMessage'),
trigger: 'blur'
}
],
desc: [
{
required: true,
message: t('views.dataset.datasetForm.form.datasetDescription.requiredMessage'),
trigger: 'blur'
}
],
embedding_mode_id: [
{
required: true,
message: t('views.dataset.datasetForm.form.EmbeddingModel.requiredMessage'),
trigger: 'change'
}
]
})
const FormRef = ref()
const loading = ref(false)
const modelOptions = ref<any>([])
watch(
() => props.data,
(value) => {
if (value && JSON.stringify(value) !== '{}') {
form.value.name = value.name
form.value.desc = value.desc
form.value.embedding_mode_id = value.embedding_mode_id
}
},
{
immediate: true
}
)
/*
表单校验
*/
function validate() {
if (!FormRef.value) return
return FormRef.value.validate((valid: any) => {
return valid
})
}
function getModel() {
loading.value = true
model
.asyncGetModel({ model_type: 'EMBEDDING' })
.then((res: any) => {
modelOptions.value = groupBy(res?.data, 'provider')
loading.value = false
})
.catch(() => {
loading.value = false
})
}
onMounted(() => {
getModel()
})
onUnmounted(() => {
form.value = {
name: '',
desc: '',
embedding_mode_id: ''
}
FormRef.value?.clearValidate()
})
defineExpose({
validate,
form
})
</script>
<style scoped lang="scss"></style>

View File

@ -0,0 +1,329 @@
<template>
<el-dialog
:title="$t('views.dataset.createDataset')"
v-model="dialogVisible"
width="720"
append-to-body
:close-on-click-modal="false"
:close-on-press-escape="false"
>
<!-- 基本信息 -->
<BaseForm ref="BaseFormRef" v-if="dialogVisible" />
<el-form
ref="DatasetFormRef"
:rules="rules"
:model="datasetForm"
label-position="top"
require-asterisk-position="right"
>
<el-form-item :label="$t('views.dataset.datasetForm.form.datasetType.label')" required>
<el-radio-group v-model="datasetForm.type" class="card__radio" @change="radioChange">
<el-row :gutter="20">
<el-col :span="12">
<el-card
shadow="never"
class="mb-16"
:class="datasetForm.type === '0' ? 'active' : ''"
@click="datasetForm.type = '0'"
>
<div class="flex-between">
<div class="flex align-center">
<AppAvatar class="mr-8 avatar-blue" shape="square" :size="32">
<img src="@/assets/icon_document.svg" style="width: 58%" alt="" />
</AppAvatar>
<div>
<p>
<el-text>{{ $t('views.dataset.general') }}</el-text>
</p>
<el-text type="info">{{
$t('views.dataset.datasetForm.form.datasetType.generalInfo')
}}</el-text>
</div>
</div>
<el-radio value="0" size="large" style="width: 16px"></el-radio>
</div>
</el-card>
</el-col>
<el-col :span="12">
<el-card
shadow="never"
class="mb-16"
:class="datasetForm.type === '1' ? 'active' : ''"
@click="datasetForm.type = '1'"
>
<div class="flex-between">
<div class="flex align-center">
<AppAvatar class="mr-8 avatar-purple" shape="square" :size="32">
<img src="@/assets/icon_web.svg" style="width: 58%" alt="" />
</AppAvatar>
<div>
<p>
<el-text>{{ $t('views.dataset.web') }}</el-text>
</p>
<el-text type="info">{{
$t('views.dataset.datasetForm.form.datasetType.webInfo')
}}</el-text>
</div>
</div>
<el-radio value="1" size="large" style="width: 16px"></el-radio>
</div>
</el-card>
</el-col>
</el-row>
<el-row :gutter="20" v-hasPermission="new ComplexPermission([], ['x-pack'], 'OR')">
<el-col :span="12">
<el-card
shadow="never"
class="mb-16"
:class="datasetForm.type === '2' ? 'active' : ''"
@click="datasetForm.type = '2'"
>
<div class="flex-between">
<div class="flex align-center">
<AppAvatar shape="square" :size="32" style="background: none">
<img src="@/assets/logo_lark.svg" style="width: 100%" alt="" />
</AppAvatar>
<div>
<p>
<el-text>{{ $t('views.dataset.lark') }}</el-text>
</p>
<el-text type="info">{{
$t('views.dataset.datasetForm.form.datasetType.larkInfo')
}}</el-text>
</div>
</div>
<el-radio value="2" size="large" style="width: 16px"></el-radio>
</div>
</el-card>
</el-col>
<el-col :span="12">
<!-- <el-card-->
<!-- shadow="never"-->
<!-- class="mb-16"-->
<!-- :class="datasetForm.type === '3' ? 'active' : ''"-->
<!-- @click="datasetForm.type = '3'"-->
<!-- >-->
<!-- <div class="flex-between">-->
<!-- <div class="flex align-center">-->
<!-- <AppAvatar class="mr-8" :size="32">-->
<!-- <img src="@/assets/icon_web.svg" style="width: 100%" alt="" />-->
<!-- </AppAvatar>-->
<!-- <div>-->
<!-- <p>-->
<!-- <el-text>{{ $t('views.dataset.yuque') }}</el-text>-->
<!-- </p>-->
<!-- <el-text type="info">{{-->
<!-- $t('views.dataset.datasetForm.form.datasetType.yuqueInfo')-->
<!-- }}</el-text>-->
<!-- </div>-->
<!-- </div>-->
<!-- <el-radio value="3" size="large" style="width: 16px"></el-radio>-->
<!-- </div>-->
<!-- </el-card>-->
</el-col>
</el-row>
</el-radio-group>
</el-form-item>
<el-form-item
:label="$t('views.dataset.datasetForm.form.source_url.label')"
prop="source_url"
v-if="datasetForm.type === '1'"
>
<el-input
v-model="datasetForm.source_url"
:placeholder="$t('views.dataset.datasetForm.form.source_url.placeholder')"
@blur="datasetForm.source_url = datasetForm.source_url.trim()"
/>
</el-form-item>
<el-form-item
:label="$t('views.dataset.datasetForm.form.selector.label')"
v-if="datasetForm.type === '1'"
>
<el-input
v-model="datasetForm.selector"
:placeholder="$t('views.dataset.datasetForm.form.selector.placeholder')"
@blur="datasetForm.selector = datasetForm.selector.trim()"
/>
</el-form-item>
<el-form-item label="App ID" prop="app_id" v-if="datasetForm.type === '2'">
<el-input
v-model="datasetForm.app_id"
:placeholder="$t('views.application.applicationAccess.larkSetting.appIdPlaceholder')"
/>
</el-form-item>
<el-form-item label="App Secret" prop="app_secret" v-if="datasetForm.type === '2'">
<el-input
v-model="datasetForm.app_secret"
type="password"
show-password
:placeholder="$t('views.application.applicationAccess.larkSetting.appSecretPlaceholder')"
/>
</el-form-item>
<el-form-item label="Folder Token" prop="folder_token" v-if="datasetForm.type === '2'">
<el-input
v-model="datasetForm.folder_token"
:placeholder="
$t('views.application.applicationAccess.larkSetting.folderTokenPlaceholder')
"
/>
</el-form-item>
<el-form-item label="User ID" prop="user_id" v-if="datasetForm.type === '3'">
<el-input
v-model="datasetForm.user_id"
:placeholder="
$t('views.application.applicationAccess.larkSetting.folderTokenPlaceholder')
"
/>
</el-form-item>
<el-form-item label="Token" prop="token" v-if="datasetForm.type === '3'">
<el-input
v-model="datasetForm.token"
:placeholder="
$t('views.application.applicationAccess.larkSetting.folderTokenPlaceholder')
"
/>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click.prevent="dialogVisible = false" :loading="loading">
{{ $t('common.cancel') }}
</el-button>
<el-button type="primary" @click="submitHandle" :loading="loading">
{{ $t('common.create') }}
</el-button>
</span>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { ref, watch, reactive } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import BaseForm from './BaseForm.vue'
import datasetApi from '@/api/dataset'
import { MsgSuccess, MsgAlert } from '@/utils/message'
import { t } from '@/locales'
import { ComplexPermission } from '@/utils/permission/type'
const emit = defineEmits(['refresh'])
const router = useRouter()
const BaseFormRef = ref()
const DatasetFormRef = ref()
const loading = ref(false)
const dialogVisible = ref<boolean>(false)
const datasetForm = ref<any>({
type: '0',
source_url: '',
selector: '',
app_id: '',
app_secret: '',
folder_token: ''
})
const rules = reactive({
source_url: [
{
required: true,
message: t('views.dataset.datasetForm.form.source_url.requiredMessage'),
trigger: 'blur'
}
],
app_id: [
{
required: true,
message: t('views.application.applicationAccess.larkSetting.appIdPlaceholder'),
trigger: 'blur'
}
],
app_secret: [
{
required: true,
message: t('views.application.applicationAccess.larkSetting.appSecretPlaceholder'),
trigger: 'blur'
}
],
folder_token: [
{
required: true,
message: t('views.application.applicationAccess.larkSetting.folderTokenPlaceholder'),
trigger: 'blur'
}
],
user_id: [
{
required: true,
message: t('views.dataset.datasetForm.form.user_id.requiredMessage'),
trigger: 'blur'
}
],
token: [
{
required: true,
message: t('views.dataset.datasetForm.form.token.requiredMessage'),
trigger: 'blur'
}
]
})
watch(dialogVisible, (bool) => {
if (!bool) {
datasetForm.value = {
type: '0',
source_url: '',
selector: ''
}
DatasetFormRef.value?.clearValidate()
}
})
const open = () => {
dialogVisible.value = true
}
const submitHandle = async () => {
if (await BaseFormRef.value?.validate()) {
await DatasetFormRef.value.validate((valid: any) => {
if (valid) {
if (datasetForm.value.type === '0') {
const obj = {
...BaseFormRef.value.form,
type: datasetForm.value.type
}
datasetApi.postDataset(obj, loading).then((res) => {
MsgSuccess(t('common.createSuccess'))
router.push({ path: `/dataset/${res.data.id}/document` })
emit('refresh')
})
} else if (datasetForm.value.type === '1') {
const obj = { ...BaseFormRef.value.form, ...datasetForm.value }
datasetApi.postWebDataset(obj, loading).then((res) => {
MsgSuccess(t('common.createSuccess'))
router.push({ path: `/dataset/${res.data.id}/document` })
emit('refresh')
})
} else if (datasetForm.value.type === '2') {
const obj = { ...BaseFormRef.value.form, ...datasetForm.value }
datasetApi.postLarkDataset(obj, loading).then((res) => {
MsgSuccess(t('common.createSuccess'))
router.push({ path: `/dataset/${res.data.id}/document` })
emit('refresh')
})
}
} else {
return false
}
})
} else {
return false
}
}
function radioChange() {
datasetForm.value.source_url = ''
datasetForm.value.selector = ''
}
defineExpose({ open })
</script>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,135 @@
<template>
<el-dialog
:title="$t('views.paragraph.editParagraph')"
v-model="dialogVisible"
width="80%"
destroy-on-close
class="paragraph-dialog"
:close-on-click-modal="false"
:close-on-press-escape="false"
>
<el-row v-if="isConnect">
<el-col :span="18" class="p-24">
<ParagraphForm ref="paragraphFormRef" :data="detail" :isEdit="true" />
</el-col>
<el-col :span="6" class="border-l" style="width: 300px">
<p class="bold title p-24" style="padding-bottom: 0">
<span class="flex align-center">
<span>{{ $t('views.paragraph.relatedProblem.title') }}</span>
<el-divider direction="vertical" class="mr-4" />
<el-button text @click="addProblem">
<el-icon><Plus /></el-icon>
</el-button>
</span>
</p>
<el-scrollbar height="500px">
<div class="p-24" style="padding-top: 16px">
<el-input
v-if="isAddProblem"
v-model="problemValue"
:placeholder="$t('views.paragraph.relatedProblem.placeholder')"
@change="addProblemHandle"
@blur="isAddProblem = false"
ref="inputRef"
class="mb-8"
/>
<template v-for="(item, index) in detail.problem_list" :key="index">
<TagEllipsis
@close="delProblemHandle(item, index)"
class="question-tag"
type="info"
effect="plain"
closable
>
<auto-tooltip :content="item.content">
{{ item.content }}
</auto-tooltip>
</TagEllipsis>
</template>
</div>
</el-scrollbar>
</el-col>
</el-row>
<div v-else class="p-24">
<ParagraphForm ref="paragraphFormRef" :data="detail" :isEdit="true" />
</div>
<template #footer>
<span class="dialog-footer">
<el-button @click.prevent="dialogVisible = false"> {{ $t('common.cancel') }} </el-button>
<el-button type="primary" @click="submitHandle"> {{ $t('common.save') }} </el-button>
</span>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { ref, watch, nextTick } from 'vue'
import { cloneDeep } from 'lodash'
import ParagraphForm from '@/views/paragraph/component/ParagraphForm.vue'
const props = defineProps({
isConnect: Boolean
})
const emit = defineEmits(['updateContent'])
const dialogVisible = ref<boolean>(false)
const detail = ref<any>({})
const paragraphFormRef = ref()
const inputRef = ref()
const isAddProblem = ref(false)
const problemValue = ref('')
watch(dialogVisible, (bool) => {
if (!bool) {
detail.value = {}
}
})
const open = (data: any) => {
detail.value = cloneDeep(data)
dialogVisible.value = true
}
function delProblemHandle(item: any, index: number) {
detail.value.problem_list.splice(index, 1)
}
function addProblemHandle() {
if (problemValue.value.trim()) {
if (
!detail.value?.problem_list.some((item: any) => item.content === problemValue.value.trim())
) {
detail.value?.problem_list?.push({
content: problemValue.value.trim()
})
}
problemValue.value = ''
isAddProblem.value = false
}
}
function addProblem() {
isAddProblem.value = true
nextTick(() => {
inputRef.value?.focus()
})
}
const submitHandle = async () => {
if (await paragraphFormRef.value?.validate()) {
emit('updateContent', {
problem_list: detail.value.problem_list,
...paragraphFormRef.value?.form
})
dialogVisible.value = false
}
}
defineExpose({ open })
</script>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,109 @@
<template>
<div>
<InfiniteScroll
:size="paragraph_list.length"
:total="modelValue.length"
:page_size="page_size"
v-model:current_page="current_page"
@load="next()"
:loading="loading"
>
<el-card
v-for="(child, cIndex) in paragraph_list"
:key="cIndex"
shadow="never"
class="card-never mb-16"
>
<div class="flex-between">
<span>{{ child.title || '-' }}</span>
<div>
<!-- 编辑分段按钮 -->
<el-button link @click="editHandle(child, cIndex)">
<el-icon><EditPen /></el-icon>
</el-button>
<!-- 删除分段按钮 -->
<el-button link @click="deleteHandle(child, cIndex)">
<el-icon><Delete /></el-icon>
</el-button>
</div>
</div>
<div class="lighter mt-12">
{{ child.content }}
</div>
<div class="lighter mt-12">
<el-text type="info">
{{ child.content.length }} {{ $t('views.paragraph.character_count') }}
</el-text>
</div>
</el-card>
</InfiniteScroll>
<EditParagraphDialog
ref="EditParagraphDialogRef"
@updateContent="updateContent"
:isConnect="isConnect"
/>
</div>
</template>
<script setup lang="ts">
import { cloneDeep } from 'lodash'
import { ref, computed } from 'vue'
import EditParagraphDialog from './EditParagraphDialog.vue'
import { MsgConfirm } from '@/utils/message'
import { t } from '@/locales'
const page_size = ref<number>(30)
const current_page = ref<number>(1)
const currentCIndex = ref<number>(0)
const EditParagraphDialogRef = ref()
const emit = defineEmits(['update:modelValue'])
const loading = ref<boolean>(false)
const editHandle = (item: any, cIndex: number) => {
currentCIndex.value = cIndex
EditParagraphDialogRef.value.open(item)
}
const props = defineProps<{ modelValue: Array<any>; isConnect: boolean }>()
const paragraph_list = computed(() => {
return props.modelValue.slice(0, page_size.value * (current_page.value - 1) + page_size.value)
})
const next = () => {
loading.value = true
current_page.value += 1
loading.value = false
}
const updateContent = (data: any) => {
const new_value = [...props.modelValue]
if (
props.isConnect &&
data.title &&
!data?.problem_list.some((item: any) => item.content === data.title.trim())
) {
data['problem_list'].push({
content: data.title.trim()
})
}
new_value[currentCIndex.value] = cloneDeep(data)
emit('update:modelValue', new_value)
}
const deleteHandle = (item: any, cIndex: number) => {
MsgConfirm(
`${t('views.paragraph.delete.confirmTitle')}${item.title || '-'} ?`,
t('views.paragraph.delete.confirmMessage'),
{
confirmButtonText: t('common.confirm'),
confirmButtonClass: 'danger'
}
)
.then(() => {
const new_value = [...props.modelValue]
new_value.splice(cIndex, 1)
emit('update:modelValue', new_value)
})
.catch(() => {})
}
</script>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,70 @@
<template>
<el-tabs v-model="activeName" class="paragraph-tabs">
<template v-for="(item, index) in data" :key="index">
<el-tab-pane :label="item.name" :name="index">
<template #label>
<div class="flex-center">
<img :src="getImgUrl(item && item?.name)" alt="" height="16" />
<span class="ml-4">{{ item?.name }}</span>
</div>
</template>
<div class="mb-16">
<el-text type="info"
>{{ item.content.length }} {{ $t('views.paragraph.title') }}</el-text
>
</div>
<div class="paragraph-list" v-if="activeName == index">
<el-scrollbar>
<ParagraphList v-model="item.content" :isConnect="isConnect"> </ParagraphList>
</el-scrollbar>
</div>
</el-tab-pane>
</template>
</el-tabs>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { getImgUrl } from '@/utils/utils'
import ParagraphList from './ParagraphList.vue'
defineProps({
data: {
type: Array<any>,
default: () => []
},
isConnect: Boolean
})
const activeName = ref(0)
</script>
<style scoped lang="scss">
.paragraph-tabs {
:deep(.el-tabs__item) {
background: var(--app-text-color-light-1);
margin: 4px;
border-radius: 4px;
padding: 5px 10px 5px 8px !important;
height: auto;
&:nth-child(2) {
margin-left: 0;
}
&:last-child {
margin-right: 0;
}
&.is-active {
border: 1px solid var(--el-color-primary);
background: var(--el-color-primary-light-9);
color: var(--el-text-color-primary);
}
}
:deep(.el-tabs__nav-wrap::after) {
display: none;
}
:deep(.el-tabs__active-bar) {
display: none;
}
}
.paragraph-list {
height: calc(var(--create-dataset-height) - 101px);
}
</style>

View File

@ -0,0 +1,90 @@
<template>
<el-scrollbar>
<el-result icon="success" :title="`🎉 ${$t('views.dataset.ResultSuccess.title')} 🎉`">
<template #sub-title>
<div class="mt-8">
<span class="bold">{{ data?.document_list.length || 0 }}</span>
<el-text type="info" class="ml-4">{{ $t('common.fileUpload.document') }}</el-text>
<el-divider direction="vertical" />
<span class="bold">{{ paragraph_count || 0 }}</span>
<el-text type="info" class="ml-4">{{
$t('views.dataset.ResultSuccess.paragraph')
}}</el-text>
<el-divider direction="vertical" />
<span class="bold">{{ numberFormat(char_length) || 0 }}</span>
<el-text type="info" class="ml-4">{{ $t('common.character') }} </el-text>
</div>
</template>
<template #extra>
<el-button @click="router.push({ path: `/dataset` })">{{
$t('views.dataset.ResultSuccess.buttons.toDataset')
}}</el-button>
<el-button type="primary" @click="router.push({ path: `/dataset/${data?.id}/document` })">{{
$t('views.dataset.ResultSuccess.buttons.toDocument')
}}</el-button>
</template>
</el-result>
<div class="result-success">
<p class="bolder">{{ $t('views.dataset.ResultSuccess.documentList') }}</p>
<el-card
shadow="never"
class="file-List-card mt-8"
v-for="(item, index) in data?.document_list"
:key="index"
>
<div class="flex-between">
<div class="flex">
<img :src="getImgUrl(item && item?.name)" alt="" width="40" />
<div class="ml-8">
<p>{{ item && item?.name }}</p>
<el-text type="info" size="small">{{ filesize(item && item?.char_length) }}</el-text>
</div>
</div>
<div>
<el-text type="info" class="mr-16"
>{{ item && item?.paragraph_count }}
{{ $t('views.dataset.ResultSuccess.paragraph_count') }}</el-text
>
<el-text v-if="item.status === '1'">
<el-icon class="success"><SuccessFilled /></el-icon>
</el-text>
<el-text v-else-if="item.status === '2'">
<el-icon class="danger"><CircleCloseFilled /></el-icon>
</el-text>
<el-text v-else-if="item.status === '0'">
<el-icon class="is-loading primary"><Loading /></el-icon> {{ $t('views.dataset.ResultSuccess.loading') }}...
</el-text>
</div>
</div>
</el-card>
</div>
</el-scrollbar>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { useRouter } from 'vue-router'
import { numberFormat } from '@/utils/utils'
import { filesize, getImgUrl } from '@/utils/utils'
const props = defineProps({
data: {
type: Object,
default: () => {}
}
})
const router = useRouter()
const paragraph_count = computed(() =>
props.data?.document_list.reduce((sum: number, obj: any) => (sum += obj.paragraph_count), 0)
)
const char_length = computed(
() =>
props.data?.document_list.reduce((sum: number, obj: any) => (sum += obj.char_length), 0) || 0
)
</script>
<style scoped lang="scss">
.result-success {
width: 70%;
margin: 0 auto;
margin-bottom: 30px;
}
</style>

View File

@ -0,0 +1,279 @@
<template>
<div class="set-rules">
<el-row>
<el-col :span="10" class="p-24">
<h4 class="title-decoration-1 mb-16">{{ $t('views.document.setRules.title.setting') }}</h4>
<div class="set-rules__right">
<el-scrollbar>
<div class="left-height" @click.stop>
<el-radio-group v-model="radio" class="set-rules__radio">
<el-card shadow="never" class="mb-16" :class="radio === '1' ? 'active' : ''">
<el-radio value="1" size="large">
<p class="mb-4">{{ $t('views.document.setRules.intelligent.label') }}</p>
<el-text type="info">{{
$t('views.document.setRules.intelligent.text')
}}</el-text>
</el-radio>
</el-card>
<el-card shadow="never" class="mb-16" :class="radio === '2' ? 'active' : ''">
<el-radio value="2" size="large">
<p class="mb-4">{{ $t('views.document.setRules.advanced.label') }}</p>
<el-text type="info">
{{ $t('views.document.setRules.advanced.text') }}
</el-text>
</el-radio>
<el-card
v-if="radio === '2'"
shadow="never"
class="card-never mt-16"
style="margin-left: 30px"
>
<div class="set-rules__form">
<div class="form-item mb-16">
<div class="title flex align-center mb-8">
<span style="margin-right: 4px">{{
$t('views.document.setRules.patterns.label')
}}</span>
<el-tooltip
effect="dark"
:content="$t('views.document.setRules.patterns.tooltip')"
placement="right"
>
<AppIcon iconName="app-warning" class="app-warning-icon"></AppIcon>
</el-tooltip>
</div>
<div @click.stop>
<el-select
v-model="form.patterns"
multiple
allow-create
default-first-option
filterable
:placeholder="$t('views.document.setRules.patterns.placeholder')"
>
<el-option
v-for="(item, index) in splitPatternList"
:key="index"
:label="item.key"
:value="item.value"
>
</el-option>
</el-select>
</div>
</div>
<div class="form-item mb-16">
<div class="title mb-8">
{{ $t('views.document.setRules.limit.label') }}
</div>
<el-slider
v-model="form.limit"
show-input
:show-input-controls="false"
:min="50"
:max="100000"
/>
</div>
<div class="form-item mb-16">
<div class="title mb-8">
{{ $t('views.document.setRules.with_filter.label') }}
</div>
<el-switch size="small" v-model="form.with_filter" />
<div style="margin-top: 4px">
<el-text type="info">
{{ $t('views.document.setRules.with_filter.text') }}</el-text
>
</div>
</div>
</div>
</el-card>
</el-card>
</el-radio-group>
</div>
</el-scrollbar>
<div>
<el-checkbox
v-model="checkedConnect"
@change="changeHandle"
style="white-space: normal"
>
{{ $t('views.document.setRules.checkedConnect.label') }}
</el-checkbox>
</div>
<div class="text-right mt-8">
<el-button @click="splitDocument">
{{ $t('views.document.buttons.preview') }}</el-button
>
</div>
</div>
</el-col>
<el-col :span="14" class="p-24 border-l">
<div v-loading="loading">
<h4 class="title-decoration-1 mb-8">{{ $t('views.document.setRules.title.preview') }}</h4>
<ParagraphPreview v-model:data="paragraphList" :isConnect="checkedConnect" />
</div>
</el-col>
</el-row>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted, reactive, watch } from 'vue'
import ParagraphPreview from '@/views/dataset/component/ParagraphPreview.vue'
import { cutFilename } from '@/utils/utils'
import documentApi from '@/api/document'
import useStore from '@/stores'
import type { KeyValue } from '@/api/type/common'
const { dataset } = useStore()
const documentsFiles = computed(() => dataset.documentsFiles)
const splitPatternList = ref<Array<KeyValue<string, string>>>([])
const radio = ref('1')
const loading = ref(false)
const paragraphList = ref<any[]>([])
const patternLoading = ref<boolean>(false)
const checkedConnect = ref<boolean>(false)
const firstChecked = ref(true)
const form = reactive<{
patterns: Array<string>
limit: number
with_filter: boolean
[propName: string]: any
}>({
patterns: [],
limit: 500,
with_filter: true
})
function changeHandle(val: boolean) {
if (val && firstChecked.value) {
paragraphList.value = paragraphList.value.map((item: any) => ({
...item,
content: item.content.map((v: any) => ({
...v,
problem_list: v.title.trim()
? [
{
content: v.title.trim()
}
]
: []
}))
}))
firstChecked.value = false
}
}
function splitDocument() {
loading.value = true
let fd = new FormData()
documentsFiles.value.forEach((item) => {
if (item?.raw) {
fd.append('file', item?.raw)
}
})
if (radio.value === '2') {
Object.keys(form).forEach((key) => {
if (key == 'patterns') {
form.patterns.forEach((item) => fd.append('patterns', item))
} else {
fd.append(key, form[key])
}
})
}
documentApi
.postSplitDocument(fd)
.then((res: any) => {
const list = res.data
list.map((item: any) => {
if (item.name.length > 128) {
item.name = cutFilename(item.name, 128)
}
if (checkedConnect.value) {
item.content.map((v: any) => {
v['problem_list'] = v.title.trim()
? [
{
content: v.title.trim()
}
]
: []
})
}
})
paragraphList.value = list
loading.value = false
})
.catch(() => {
loading.value = false
})
}
const initSplitPatternList = () => {
documentApi.listSplitPattern(patternLoading).then((ok) => {
splitPatternList.value = ok.data
})
}
watch(radio, () => {
if (radio.value === '2') {
initSplitPatternList()
}
})
onMounted(() => {
splitDocument()
})
defineExpose({
paragraphList,
checkedConnect,
loading
})
</script>
<style scoped lang="scss">
.set-rules {
width: 100%;
.left-height {
max-height: calc(var(--create-dataset-height) - 110px);
overflow-x: hidden;
}
&__radio {
width: 100%;
display: block;
.el-radio {
white-space: break-spaces;
width: 100%;
height: 100%;
line-height: 22px;
color: var(--app-text-color);
}
:deep(.el-radio__label) {
padding-left: 30px;
width: 100%;
}
:deep(.el-radio__input) {
position: absolute;
top: 16px;
}
.active {
border: 1px solid var(--el-color-primary);
}
}
&__form {
.title {
font-size: 14px;
font-weight: 400;
}
}
}
</style>

View File

@ -0,0 +1,87 @@
<template>
<el-dialog
:title="$t('views.dataset.syncWeb.title')"
v-model="dialogVisible"
width="600px"
:close-on-click-modal="false"
:close-on-press-escape="false"
:destroy-on-close="true"
>
<p class="mb-8">{{ $t('views.dataset.syncWeb.syncMethod') }}</p>
<el-radio-group v-model="method" class="card__radio">
<el-card shadow="never" class="mb-16" :class="method === 'replace' ? 'active' : ''">
<el-radio value="replace" size="large">
<p class="mb-4">{{ $t('views.dataset.syncWeb.replace') }}</p>
<el-text type="info">{{ $t('views.dataset.syncWeb.replaceText') }}</el-text>
</el-radio>
</el-card>
<el-card shadow="never" class="mb-16" :class="method === 'complete' ? 'active' : ''">
<el-radio value="complete" size="large">
<p class="mb-4">{{ $t('views.dataset.syncWeb.complete') }}</p>
<el-text type="info">{{ $t('views.dataset.syncWeb.completeText') }}</el-text>
</el-radio>
</el-card>
</el-radio-group>
<p class="danger">{{ $t('views.dataset.syncWeb.tip') }}</p>
<template #footer>
<span class="dialog-footer">
<el-button @click.prevent="dialogVisible = false"> {{ $t('common.cancel') }} </el-button>
<el-button type="primary" @click="submit" :loading="loading">
{{ $t('common.confirm') }}
</el-button>
</span>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { ref, watch } from 'vue'
import useStore from '@/stores'
const { dataset } = useStore()
const emit = defineEmits(['refresh'])
const loading = ref<boolean>(false)
const method = ref('replace')
const datasetId = ref('')
const dialogVisible = ref<boolean>(false)
watch(dialogVisible, (bool) => {
if (!bool) {
method.value = 'replace'
}
})
const open = (id: string) => {
datasetId.value = id
dialogVisible.value = true
}
const submit = () => {
dataset.asyncSyncDataset(datasetId.value, method.value, loading).then((res: any) => {
emit('refresh', res.data)
dialogVisible.value = false
})
}
defineExpose({ open })
</script>
<style lang="scss" scoped>
.select-provider {
font-size: 16px;
color: rgba(100, 106, 115, 1);
font-weight: 400;
line-height: 24px;
cursor: pointer;
&:hover {
color: var(--el-color-primary);
}
}
.active-breadcrumb {
font-size: 16px;
color: rgba(31, 35, 41, 1);
font-weight: 500;
line-height: 24px;
}
</style>

View File

@ -0,0 +1,327 @@
<template>
<h4 class="title-decoration-1 mb-8">{{ $t('views.document.uploadDocument') }}</h4>
<el-form
ref="FormRef"
:model="form"
:rules="rules"
label-position="top"
require-asterisk-position="right"
>
<div class="mt-16 mb-16">
<el-radio-group v-model="form.fileType" @change="radioChange" class="app-radio-button-group">
<el-radio-button value="txt">{{ $t('views.document.fileType.txt.label') }}</el-radio-button>
<el-radio-button value="table">{{
$t('views.document.fileType.table.label')
}}</el-radio-button>
<el-radio-button value="QA">{{ $t('views.document.fileType.QA.label') }}</el-radio-button>
</el-radio-group>
</div>
<el-form-item prop="fileList" v-if="form.fileType === 'QA'">
<div class="update-info flex p-8-12 border-r-4 mb-16 w-full">
<div class="mt-4">
<AppIcon iconName="app-warning-colorful" style="font-size: 16px"></AppIcon>
</div>
<div class="ml-16 lighter">
<p>
{{ $t('views.document.fileType.QA.tip1') }}
<el-button type="primary" link @click="downloadTemplate('excel')">
{{ $t('views.document.upload.download') }} Excel
{{ $t('views.document.upload.template') }}
</el-button>
<el-button type="primary" link @click="downloadTemplate('csv')">
{{ $t('views.document.upload.download') }} CSV
{{ $t('views.document.upload.template') }}
</el-button>
</p>
<p>{{ $t('views.document.fileType.QA.tip2') }}</p>
<p>{{ $t('views.document.fileType.QA.tip3') }}</p>
</div>
</div>
<el-upload
:webkitdirectory="false"
class="w-full mb-4"
drag
multiple
v-model:file-list="form.fileList"
action="#"
:auto-upload="false"
:show-file-list="false"
accept=".xlsx, .xls, .csv,.zip"
:limit="50"
:on-exceed="onExceed"
:on-change="fileHandleChange"
@click.prevent="handlePreview(false)"
>
<img src="@/assets/upload-icon.svg" alt="" />
<div class="el-upload__text">
<p>
{{ $t('views.document.upload.uploadMessage') }}
<em class="hover" @click.prevent="handlePreview(false)">
{{ $t('views.document.upload.selectFile') }}
</em>
<em class="hove ml-4" @click.prevent="handlePreview(true)">
{{ $t('views.document.upload.selectFiles') }}
</em>
</p>
<div class="upload__decoration">
<p>{{ $t('views.document.upload.formats') }}XLSXLSXCSVZIP</p>
</div>
</div>
</el-upload>
</el-form-item>
<el-form-item prop="fileList" v-else-if="form.fileType === 'table'">
<div class="update-info flex p-8-12 border-r-4 mb-16 w-full">
<div class="mt-4">
<AppIcon iconName="app-warning-colorful" style="font-size: 16px"></AppIcon>
</div>
<div class="ml-16 lighter">
<p>
{{ $t('views.document.fileType.table.tip1') }}
<el-button type="primary" link @click="downloadTableTemplate('excel')">
{{ $t('views.document.upload.download') }} Excel
{{ $t('views.document.upload.template') }}
</el-button>
<el-button type="primary" link @click="downloadTableTemplate('csv')">
{{ $t('views.document.upload.download') }} CSV
{{ $t('views.document.upload.template') }}
</el-button>
</p>
<p>{{ $t('views.document.fileType.table.tip2') }}</p>
<p>{{ $t('views.document.fileType.table.tip3') }}</p>
<p>{{ $t('views.document.fileType.table.tip4') }}</p>
</div>
</div>
<el-upload
:webkitdirectory="false"
class="w-full mb-4"
drag
multiple
v-model:file-list="form.fileList"
action="#"
:auto-upload="false"
:show-file-list="false"
accept=".xlsx, .xls, .csv"
:limit="50"
:on-exceed="onExceed"
:on-change="fileHandleChange"
@click.prevent="handlePreview(false)"
>
<img src="@/assets/upload-icon.svg" alt="" />
<div class="el-upload__text">
<p>
{{ $t('views.document.upload.uploadMessage') }}
<em class="hover" @click.prevent="handlePreview(false)">
{{ $t('views.document.upload.selectFile') }}
</em>
<em class="hover ml-4" @click.prevent="handlePreview(true)">
{{ $t('views.document.upload.selectFiles') }}
</em>
</p>
<div class="upload__decoration">
<p>{{ $t('views.document.upload.formats') }}XLSXLSXCSV</p>
</div>
</div>
</el-upload>
</el-form-item>
<el-form-item prop="fileList" v-else>
<div class="update-info flex p-8-12 border-r-4 mb-16 w-full">
<div class="mt-4">
<AppIcon iconName="app-warning-colorful" style="font-size: 16px"></AppIcon>
</div>
<div class="ml-16 lighter">
<p>{{ $t('views.document.fileType.txt.tip1') }}</p>
<p>{{ $t('views.document.fileType.txt.tip2') }}</p>
</div>
</div>
<el-upload
:webkitdirectory="false"
class="w-full"
drag
multiple
v-model:file-list="form.fileList"
action="#"
:auto-upload="false"
:show-file-list="false"
accept=".txt, .md, .log, .docx, .pdf, .html,.zip,.xlsx,.xls,.csv"
:limit="50"
:on-exceed="onExceed"
:on-change="fileHandleChange"
@click.prevent="handlePreview(false)"
>
<img src="@/assets/upload-icon.svg" alt="" />
<div class="el-upload__text">
<p>
{{ $t('views.document.upload.uploadMessage') }}
<em class="hover" @click.prevent="handlePreview(false)">
{{ $t('views.document.upload.selectFile') }}
</em>
<em class="hover ml-4" @click.prevent="handlePreview(true)">
{{ $t('views.document.upload.selectFiles') }}
</em>
</p>
<div class="upload__decoration">
<p>
{{
$t('views.document.upload.formats')
}}TXTMarkdownPDFDOCXHTMLXLSXLSXCSVZIP
</p>
</div>
</div>
</el-upload>
</el-form-item>
</el-form>
<el-row :gutter="8" v-if="form.fileList?.length">
<template v-for="(item, index) in form.fileList" :key="index">
<el-col :span="12" class="mb-8">
<el-card shadow="never" class="file-List-card">
<div class="flex-between">
<div class="flex">
<img :src="getImgUrl(item && item?.name)" alt="" width="40" />
<div class="ml-8">
<p>{{ item && item?.name }}</p>
<el-text type="info" size="small">{{
filesize(item && item?.size) || '0K'
}}</el-text>
</div>
</div>
<el-button text @click="deleteFile(index)">
<el-icon><Delete /></el-icon>
</el-button>
</div>
</el-card>
</el-col>
</template>
</el-row>
</template>
<script setup lang="ts">
import { ref, reactive, onUnmounted, onMounted, computed, watch, nextTick } from 'vue'
import type { UploadFiles } from 'element-plus'
import { filesize, getImgUrl, isRightType } from '@/utils/utils'
import { MsgError } from '@/utils/message'
import documentApi from '@/api/document'
import useStore from '@/stores'
import { t } from '@/locales'
const { dataset } = useStore()
const documentsFiles = computed(() => dataset.documentsFiles)
const documentsType = computed(() => dataset.documentsType)
const form = ref({
fileType: 'txt',
fileList: [] as any
})
const rules = reactive({
fileList: [
{ required: true, message: t('views.document.upload.requiredMessage'), trigger: 'change' }
]
})
const FormRef = ref()
watch(form.value, (value) => {
dataset.saveDocumentsType(value.fileType)
dataset.saveDocumentsFile(value.fileList)
})
function downloadTemplate(type: string) {
documentApi.exportQATemplate(
`${type}${t('views.document.upload.template')}.${type == 'csv' ? type : 'xlsx'}`,
type
)
}
function downloadTableTemplate(type: string) {
documentApi.exportTableTemplate(
`${type}${t('views.document.upload.template')}.${type == 'csv' ? type : 'xlsx'}`,
type
)
}
function radioChange() {
form.value.fileList = []
}
function deleteFile(index: number) {
form.value.fileList.splice(index, 1)
}
// on-change
const fileHandleChange = (file: any, fileList: UploadFiles) => {
//1100M
const isLimit = file?.size / 1024 / 1024 < 100
if (!isLimit) {
MsgError(t('views.document.upload.errorMessage1'))
fileList.splice(-1, 1) //
return false
}
if (!isRightType(file?.name, form.value.fileType)) {
if (file?.name !== '.DS_Store') {
MsgError(t('views.document.upload.errorMessage2'))
}
fileList.splice(-1, 1)
return false
}
if (file?.size === 0) {
MsgError(t('views.document.upload.errorMessage3'))
fileList.splice(-1, 1)
return false
}
}
const onExceed = () => {
MsgError(t('views.document.upload.errorMessage4'))
}
const handlePreview = (bool: boolean) => {
let inputDom: any = null
nextTick(() => {
if (document.querySelector('.el-upload__input') != null) {
inputDom = document.querySelector('.el-upload__input')
inputDom.webkitdirectory = bool
}
})
}
/*
表单校验
*/
function validate() {
if (!FormRef.value) return
return FormRef.value.validate((valid: any) => {
return valid
})
}
onMounted(() => {
if (documentsType.value) {
form.value.fileType = documentsType.value
}
if (documentsFiles.value) {
form.value.fileList = documentsFiles.value
}
})
onUnmounted(() => {
form.value = {
fileType: 'txt',
fileList: []
}
})
defineExpose({
validate,
form
})
</script>
<style scoped lang="scss">
.upload__decoration {
font-size: 12px;
line-height: 20px;
color: var(--el-text-color-secondary);
}
.el-upload__text {
.hover:hover {
color: var(--el-color-primary-light-5);
}
}
</style>

View File

@ -6,13 +6,44 @@
:data="folderList"
:currentNodeKey="currentFolder?.id"
@handleNodeClick="folderClickHandel"
class="p-8"
/>
</template>
<ContentContainer>
<div class="flex-between mb-16">
<h4>{{ currentFolder?.name }}</h4>
<div class="flex-between"></div>
</div>
<ContentContainer :header="currentFolder?.name">
<template #search>
<div class="flex">
<div class="flex-between complex-search">
<el-select
class="complex-search__left"
v-model="search_type"
style="width: 120px"
@change="search_type_change"
>
<el-option :label="$t('common.creator')" value="create_user" />
<el-option :label="$t('common.name')" value="name" />
</el-select>
<el-input
v-if="search_type === 'name'"
v-model="search_form.name"
@change="getList"
:placeholder="$t('common.searchBar.placeholder')"
style="width: 220px"
clearable
/>
<el-select
v-else-if="search_type === 'create_user'"
v-model="search_form.create_user"
@change="getList"
clearable
style="width: 220px"
>
<el-option v-for="u in user_options" :key="u.id" :value="u.id" :label="u.username" />
</el-select>
</div>
<el-button class="ml-16" type="primary"> {{ $t('common.create') }}</el-button>
</div>
</template>
<div>
<el-row v-if="datasetList.length > 0 || datasetFolderList.length > 0" :gutter="15">
<template v-for="(item, index) in datasetFolderList" :key="index">
@ -37,7 +68,12 @@
</template>
<template v-for="(item, index) in datasetList" :key="index">
<el-col :xs="24" :sm="12" :md="12" :lg="8" :xl="6" class="mb-16">
<CardBox :title="item.name" :description="item.desc" class="cursor">
<CardBox
:title="item.name"
:description="item.desc"
class="cursor"
@click="router.push({ path: `/knowledge/${item.id}/document` })"
>
<template #icon>
<el-avatar
v-if="item.type === '1'"
@ -86,6 +122,53 @@
</div>
</div>
</template>
<template #mouseEnter>
<div @click.stop>
<el-dropdown trigger="click">
<el-button text @click.stop>
<el-icon><MoreFilled /></el-icon>
</el-button>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item
icon="Refresh"
@click.stop="syncDataset(item)"
v-if="item.type === 1"
>{{ $t('views.knowledge.setting.sync') }}</el-dropdown-item
>
<el-dropdown-item @click.stop="reEmbeddingDataset(item)">
<AppIcon iconName="app-vectorization"></AppIcon>
{{ $t('views.knowledge.setting.vectorization') }}
</el-dropdown-item>
<!--
<el-dropdown-item
icon="Connection"
@click.stop="openGenerateDialog(item)"
>{{ $t('views.document.generateQuestion.title') }}</el-dropdown-item
>
<el-dropdown-item
icon="Setting"
@click.stop="router.push({ path: `/dataset/${item.id}/setting` })"
>
{{ $t('common.setting') }}</el-dropdown-item
>
<el-dropdown-item @click.stop="export_dataset(item)">
<AppIcon iconName="app-export"></AppIcon
>{{ $t('views.document.setting.export') }} Excel</el-dropdown-item
>
<el-dropdown-item @click.stop="export_zip_dataset(item)">
<AppIcon iconName="app-export"></AppIcon
>{{ $t('views.document.setting.export') }} ZIP</el-dropdown-item
>
<el-dropdown-item icon="Delete" @click.stop="deleteDataset(item)">{{
$t('common.delete')
}}</el-dropdown-item> -->
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</template>
</CardBox>
</el-col>
</template>
@ -99,12 +182,28 @@
<script lang="ts" setup>
import { onMounted, ref, reactive, computed } from 'vue'
import KnowledgeApi from '@/api/knowledge/knowledge'
import { MsgSuccess, MsgConfirm } from '@/utils/message'
import useStore from '@/stores'
import { numberFormat } from '@/utils/common'
import { t } from '@/locales'
import { useRouter } from 'vue-router'
const router = useRouter()
const { folder } = useStore()
const loading = ref(false)
const search_type = ref('name')
const search_form = ref<{
name: string
create_user: string
}>({
name: '',
create_user: '',
})
const user_options = ref<any[]>([])
const paginationConfig = reactive({
current_page: 1,
page_size: 30,
@ -116,6 +215,21 @@ const datasetList = ref<any[]>([])
const datasetFolderList = ref<any[]>([])
const currentFolder = ref<any>({})
function reEmbeddingDataset(row: any) {
KnowledgeApi.putReEmbeddingDataset('default', row.id).then(() => {
MsgSuccess(t('common.submitSuccess'))
})
}
const SyncWebDialogRef = ref()
function syncDataset(row: any) {
SyncWebDialogRef.value.open(row.id)
}
const search_type_change = () => {
search_form.value = { name: '', create_user: '' }
}
function getList() {
const params = {
folder_id: currentFolder.value?.id || 'root',
@ -137,9 +251,9 @@ function getFolder() {
}
function folderClickHandel(row: any) {
// currentFolder.value = row
// toolList.value = []
// getList()
currentFolder.value = row
datasetFolderList.value = []
getList()
}
onMounted(() => {

View File

@ -31,7 +31,7 @@
v-if="search_type === 'name'"
v-model="model_search_form.name"
@change="list_model"
:placeholder="$t('views.model.searchBar.placeholder')"
:placeholder="$t('common.searchBar.placeholder')"
style="width: 220px"
clearable
/>

View File

@ -27,7 +27,7 @@
v-if="search_type === 'name'"
v-model="search_form.name"
@change="getList"
:placeholder="$t('views.model.searchBar.placeholder')"
:placeholder="$t('common.searchBar.placeholder')"
style="width: 220px"
clearable
/>
@ -41,9 +41,7 @@
<el-option v-for="u in user_options" :key="u.id" :value="u.id" :label="u.username" />
</el-select>
</div>
<!-- <el-button class="ml-16" type="primary" @click="openCreateModel(active_provider)">
{{ $t('views.model.addModel') }}</el-button
> -->
<el-button class="ml-16" type="primary"> {{ $t('common.create') }}</el-button>
</div>
</template>
@ -327,9 +325,9 @@ function refresh(data: any) {
}
function folderClickHandel(row: any) {
// currentFolder.value = row
// toolList.value = []
// getList()
currentFolder.value = row
toolList.value = []
getList()
}
onMounted(() => {