feat: 增强知识库分块策略流程
- 增加导入分析预览提交与预览态缓存键 - 支持知识库分块策略配置与分块预览 - 重构知识库导入与确认导入前端流程
This commit is contained in:
@@ -1,99 +1,141 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { computed } from 'vue';
|
||||
|
||||
import { $t } from '@easyflow/locales';
|
||||
|
||||
import { ElTable, ElTableColumn, ElTag } from 'element-plus';
|
||||
import {
|
||||
ElCard,
|
||||
ElDescriptions,
|
||||
ElDescriptionsItem,
|
||||
ElEmpty,
|
||||
ElTable,
|
||||
ElTableColumn,
|
||||
ElTag,
|
||||
} from 'element-plus';
|
||||
|
||||
import { api } from '#/api/request';
|
||||
|
||||
const props = defineProps({
|
||||
filesList: {
|
||||
default: () => [],
|
||||
type: Array<any>,
|
||||
},
|
||||
splitterParams: {
|
||||
default: () => {},
|
||||
type: Object,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['loadingFinish']);
|
||||
|
||||
const route = useRoute();
|
||||
|
||||
const knowledgeIdRef = ref<string>((route.query.id as string) || '');
|
||||
const localFilesList = ref<any[]>([]);
|
||||
watch(
|
||||
() => props.filesList,
|
||||
(newVal) => {
|
||||
localFilesList.value = [...newVal];
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
defineExpose({
|
||||
handleSave() {
|
||||
localFilesList.value.forEach((file, index) => {
|
||||
localFilesList.value[index].progressUpload = 'loading';
|
||||
saveDoc(file.filePath, 'saveText', file.fileName, index);
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
function saveDoc(
|
||||
filePath: string,
|
||||
operation: string,
|
||||
fileOriginName: string,
|
||||
index: number,
|
||||
) {
|
||||
api
|
||||
.post('/api/v1/document/saveText', {
|
||||
filePath,
|
||||
operation,
|
||||
knowledgeId: knowledgeIdRef.value,
|
||||
fileOriginName,
|
||||
...props.splitterParams,
|
||||
})
|
||||
.then((res) => {
|
||||
if (res.errorCode === 0) {
|
||||
localFilesList.value[index].progressUpload = 'success';
|
||||
emit('loadingFinish');
|
||||
}
|
||||
/* if (index === localFilesList.value.length - 1) {
|
||||
emit('loadingFinish');
|
||||
}*/
|
||||
});
|
||||
interface PreviewItem {
|
||||
fileName: string;
|
||||
previewSessionId: string;
|
||||
totalChunks?: number;
|
||||
}
|
||||
|
||||
interface CommitResultItem {
|
||||
chunkCount?: number;
|
||||
fileName?: string;
|
||||
reason?: string;
|
||||
success?: boolean;
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
commitResults?: CommitResultItem[];
|
||||
loading?: boolean;
|
||||
previewItems?: PreviewItem[];
|
||||
}>();
|
||||
|
||||
const summary = computed(() => {
|
||||
const results = props.commitResults ?? [];
|
||||
const successCount = results.filter((item) => item.success).length;
|
||||
const errorCount = results.length - successCount;
|
||||
let totalCount = 0;
|
||||
if (results.length > 0) {
|
||||
totalCount = results.length;
|
||||
} else if (props.previewItems && props.previewItems.length > 0) {
|
||||
totalCount = props.previewItems.length;
|
||||
}
|
||||
return {
|
||||
errorCount,
|
||||
successCount,
|
||||
totalCount,
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="import-doc-file-list">
|
||||
<ElTable :data="localFilesList" size="large" style="width: 100%">
|
||||
<div class="confirm-shell">
|
||||
<ElCard shadow="never" class="confirm-card">
|
||||
<ElDescriptions :column="3" border>
|
||||
<ElDescriptionsItem
|
||||
:label="$t('documentCollection.faq.import.totalCount')"
|
||||
>
|
||||
{{ summary.totalCount }}
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem
|
||||
:label="$t('documentCollection.faq.import.successCount')"
|
||||
>
|
||||
{{ summary.successCount }}
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem
|
||||
:label="$t('documentCollection.faq.import.errorCount')"
|
||||
>
|
||||
{{ summary.errorCount }}
|
||||
</ElDescriptionsItem>
|
||||
</ElDescriptions>
|
||||
</ElCard>
|
||||
|
||||
<ElEmpty
|
||||
v-if="!previewItems || previewItems.length === 0"
|
||||
:description="$t('documentCollection.importDoc.resultEmpty')"
|
||||
/>
|
||||
|
||||
<ElTable
|
||||
v-else
|
||||
:data="
|
||||
commitResults && commitResults.length > 0 ? commitResults : previewItems
|
||||
"
|
||||
size="large"
|
||||
>
|
||||
<ElTableColumn
|
||||
prop="fileName"
|
||||
:label="$t('documentCollection.importDoc.fileName')"
|
||||
width="250"
|
||||
min-width="260"
|
||||
/>
|
||||
<ElTableColumn
|
||||
prop="progressUpload"
|
||||
:label="$t('documentCollection.splitterDoc.uploadStatus')"
|
||||
prop="chunkCount"
|
||||
:label="$t('documentCollection.importDoc.chunkCount')"
|
||||
width="120"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<ElTag type="success" v-if="row.progressUpload === 'success'">
|
||||
{{ row.chunkCount ?? row.totalChunks ?? '-' }}
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn
|
||||
:label="$t('documentCollection.splitterDoc.uploadStatus')"
|
||||
width="140"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<ElTag v-if="row.success === true" type="success" effect="plain">
|
||||
{{ $t('documentCollection.splitterDoc.completed') }}
|
||||
</ElTag>
|
||||
<ElTag type="primary" v-else>
|
||||
{{ $t('documentCollection.splitterDoc.pendingUpload') }}
|
||||
<ElTag v-else-if="row.success === false" type="danger" effect="plain">
|
||||
{{ $t('documentCollection.importDoc.importFailed') }}
|
||||
</ElTag>
|
||||
<ElTag v-else type="info" effect="plain">
|
||||
{{
|
||||
loading
|
||||
? $t('documentCollection.splitterDoc.uploading')
|
||||
: $t('documentCollection.splitterDoc.pendingUpload')
|
||||
}}
|
||||
</ElTag>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn
|
||||
prop="reason"
|
||||
:label="$t('documentCollection.faq.import.reason')"
|
||||
min-width="280"
|
||||
/>
|
||||
</ElTable>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.import-doc-file-list {
|
||||
width: 100%;
|
||||
.confirm-shell {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.confirm-card {
|
||||
border: 1px solid var(--el-border-color-light);
|
||||
border-radius: 16px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,189 +1,215 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import { computed, ref } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
|
||||
import { $t } from '@easyflow/locales';
|
||||
|
||||
import { Back } from '@element-plus/icons-vue';
|
||||
import {
|
||||
ElButton,
|
||||
ElMessage,
|
||||
ElPagination,
|
||||
ElStep,
|
||||
ElSteps,
|
||||
} from 'element-plus';
|
||||
import { ElButton, ElMessage, ElStep, ElSteps } from 'element-plus';
|
||||
|
||||
import { api } from '#/api/request';
|
||||
import ComfirmImportDocument from '#/views/ai/documentCollection/ComfirmImportDocument.vue';
|
||||
import ImportKnowledgeFileContainer from '#/views/ai/documentCollection/ImportKnowledgeFileContainer.vue';
|
||||
import SegmenterDoc from '#/views/ai/documentCollection/SegmenterDoc.vue';
|
||||
import SplitterDocPreview from '#/views/ai/documentCollection/SplitterDocPreview.vue';
|
||||
|
||||
interface UploadFileItem {
|
||||
fileName: string;
|
||||
filePath: string;
|
||||
}
|
||||
|
||||
interface AnalyzeItem {
|
||||
fileName: string;
|
||||
filePath: string;
|
||||
strategyConfig: Record<string, any>;
|
||||
}
|
||||
|
||||
interface PreviewItem {
|
||||
fileName: string;
|
||||
previewSessionId: string;
|
||||
totalChunks?: number;
|
||||
}
|
||||
|
||||
const emits = defineEmits(['importBack']);
|
||||
const back = () => {
|
||||
emits('importBack');
|
||||
};
|
||||
const files = ref([]);
|
||||
const splitterParams = ref({});
|
||||
const route = useRoute();
|
||||
const knowledgeId = computed(() => (route.query.id as string) || '');
|
||||
|
||||
const fileUploadRef = ref<InstanceType<typeof ImportKnowledgeFileContainer>>();
|
||||
const segmenterDocRef = ref<InstanceType<typeof SegmenterDoc>>();
|
||||
|
||||
const activeStep = ref(0);
|
||||
const fileUploadRef = ref();
|
||||
const confirmImportRef = ref();
|
||||
const segmenterDocRef = ref();
|
||||
const pagination = ref({
|
||||
pageSize: 10,
|
||||
currentPage: 1,
|
||||
total: 0,
|
||||
});
|
||||
const goToNextStep = () => {
|
||||
const files = ref<UploadFileItem[]>([]);
|
||||
const analysisItems = ref<AnalyzeItem[]>([]);
|
||||
const previewItems = ref<PreviewItem[]>([]);
|
||||
const commitResults = ref<any[]>([]);
|
||||
|
||||
const analyzing = ref(false);
|
||||
const previewing = ref(false);
|
||||
const committing = ref(false);
|
||||
|
||||
const canGoPrevious = computed(() => activeStep.value > 0 && !committing.value);
|
||||
|
||||
function back() {
|
||||
emits('importBack');
|
||||
}
|
||||
|
||||
function getUploadedFiles() {
|
||||
return fileUploadRef.value?.getFilesData?.() || [];
|
||||
}
|
||||
|
||||
async function goToNextStep() {
|
||||
if (activeStep.value === 0) {
|
||||
if (fileUploadRef.value.getFilesData().length === 0) {
|
||||
const currentFiles = getUploadedFiles();
|
||||
if (currentFiles.length === 0) {
|
||||
ElMessage.error($t('message.uploadFileFirst'));
|
||||
return;
|
||||
}
|
||||
files.value = fileUploadRef.value.getFilesData();
|
||||
files.value = currentFiles;
|
||||
await runAnalyze();
|
||||
activeStep.value = 1;
|
||||
return;
|
||||
}
|
||||
if (activeStep.value === 1 && segmenterDocRef.value) {
|
||||
splitterParams.value = segmenterDocRef.value.getSplitterFormValues();
|
||||
|
||||
if (activeStep.value === 1) {
|
||||
await runPreview();
|
||||
activeStep.value = 2;
|
||||
return;
|
||||
}
|
||||
|
||||
if (activeStep.value === 2) {
|
||||
activeStep.value = 3;
|
||||
}
|
||||
}
|
||||
|
||||
function goToPreviousStep() {
|
||||
if (!canGoPrevious.value) {
|
||||
return;
|
||||
}
|
||||
activeStep.value += 1;
|
||||
};
|
||||
const goToPreviousStep = () => {
|
||||
activeStep.value -= 1;
|
||||
};
|
||||
const handleSizeChange = (val: number) => {
|
||||
pagination.value.pageSize = val;
|
||||
};
|
||||
const handleCurrentChange = (val: number) => {
|
||||
pagination.value.currentPage = val;
|
||||
};
|
||||
const handleTotalUpdate = (newTotal: number) => {
|
||||
pagination.value.total = newTotal; // 同步到父组件的 pagination.total
|
||||
};
|
||||
const loadingSave = ref(false);
|
||||
const confirmImport = () => {
|
||||
loadingSave.value = true;
|
||||
// 确认导入
|
||||
confirmImportRef.value.handleSave();
|
||||
};
|
||||
const finishImport = () => {
|
||||
loadingSave.value = false;
|
||||
ElMessage.success($t('documentCollection.splitterDoc.importSuccess'));
|
||||
emits('importBack');
|
||||
};
|
||||
}
|
||||
|
||||
async function runAnalyze() {
|
||||
analyzing.value = true;
|
||||
try {
|
||||
const res = await api.post('/api/v1/document/import/analyze', {
|
||||
files: files.value.map((item) => ({
|
||||
fileName: item.fileName,
|
||||
filePath: item.filePath,
|
||||
})),
|
||||
knowledgeId: knowledgeId.value,
|
||||
});
|
||||
analysisItems.value = res.data?.items || [];
|
||||
} finally {
|
||||
analyzing.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function runPreview() {
|
||||
const previewRequestItems =
|
||||
segmenterDocRef.value?.getPreviewRequestItems?.() || [];
|
||||
if (previewRequestItems.length === 0) {
|
||||
ElMessage.error($t('documentCollection.importDoc.previewEmpty'));
|
||||
return;
|
||||
}
|
||||
previewing.value = true;
|
||||
try {
|
||||
const res = await api.post('/api/v1/document/import/preview', {
|
||||
files: previewRequestItems,
|
||||
knowledgeId: knowledgeId.value,
|
||||
});
|
||||
previewItems.value = res.data?.items || [];
|
||||
commitResults.value = [];
|
||||
} finally {
|
||||
previewing.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function confirmImport() {
|
||||
if (previewItems.value.length === 0) {
|
||||
ElMessage.error($t('documentCollection.importDoc.previewEmpty'));
|
||||
return;
|
||||
}
|
||||
committing.value = true;
|
||||
try {
|
||||
const res = await api.post('/api/v1/document/import/commit', {
|
||||
knowledgeId: knowledgeId.value,
|
||||
previewSessionIds: previewItems.value.map(
|
||||
(item) => item.previewSessionId,
|
||||
),
|
||||
});
|
||||
commitResults.value = res.data?.results || [];
|
||||
if ((res.data?.errorCount || 0) === 0) {
|
||||
ElMessage.success($t('documentCollection.splitterDoc.importSuccess'));
|
||||
}
|
||||
} finally {
|
||||
committing.value = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="imp-doc-kno-container">
|
||||
<div class="imp-doc-header">
|
||||
<ElButton @click="back" :icon="Back">
|
||||
<ElButton :icon="Back" @click="back">
|
||||
{{ $t('button.back') }}
|
||||
</ElButton>
|
||||
</div>
|
||||
|
||||
<div class="imp-doc-kno-content">
|
||||
<div class="rounded-lg bg-[var(--table-header-bg-color)] py-5">
|
||||
<div class="step-card">
|
||||
<ElSteps :active="activeStep" align-center>
|
||||
<ElStep>
|
||||
<template #icon>
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="h-8 w-8 rounded-full bg-[var(--step-item-bg)]">
|
||||
<span class="text-accent-foreground text-sm/8">1</span>
|
||||
</div>
|
||||
<span class="text-base">{{
|
||||
$t('documentCollection.importDoc.fileUpload')
|
||||
}}</span>
|
||||
</div>
|
||||
</template>
|
||||
</ElStep>
|
||||
<ElStep>
|
||||
<template #icon>
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="h-8 w-8 rounded-full bg-[var(--step-item-bg)]">
|
||||
<span class="text-accent-foreground text-sm/8">2</span>
|
||||
</div>
|
||||
<span class="text-base">{{
|
||||
$t('documentCollection.importDoc.parameterSettings')
|
||||
}}</span>
|
||||
</div>
|
||||
</template>
|
||||
</ElStep>
|
||||
<ElStep>
|
||||
<template #icon>
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="h-8 w-8 rounded-full bg-[var(--step-item-bg)]">
|
||||
<span class="text-accent-foreground text-sm/8">3</span>
|
||||
</div>
|
||||
<span class="text-base">{{
|
||||
$t('documentCollection.importDoc.segmentedPreview')
|
||||
}}</span>
|
||||
</div>
|
||||
</template>
|
||||
</ElStep>
|
||||
<ElStep>
|
||||
<template #icon>
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="h-8 w-8 rounded-full bg-[var(--step-item-bg)]">
|
||||
<span class="text-accent-foreground text-sm/8">4</span>
|
||||
</div>
|
||||
<span class="text-base">{{
|
||||
$t('documentCollection.importDoc.confirmImport')
|
||||
}}</span>
|
||||
</div>
|
||||
</template>
|
||||
</ElStep>
|
||||
<ElStep :title="$t('documentCollection.importDoc.fileUpload')" />
|
||||
<ElStep
|
||||
:title="$t('documentCollection.importDoc.strategyAnalysis')"
|
||||
/>
|
||||
<ElStep
|
||||
:title="$t('documentCollection.importDoc.segmentedPreview')"
|
||||
/>
|
||||
<ElStep :title="$t('documentCollection.importDoc.confirmImport')" />
|
||||
</ElSteps>
|
||||
</div>
|
||||
|
||||
<div style="margin-top: 20px">
|
||||
<!-- 文件上传导入-->
|
||||
<div class="knw-file-upload" v-if="activeStep === 0">
|
||||
<ImportKnowledgeFileContainer ref="fileUploadRef" />
|
||||
</div>
|
||||
<!-- 分割参数设置-->
|
||||
<div class="knw-file-splitter" v-if="activeStep === 1">
|
||||
<SegmenterDoc ref="segmenterDocRef" />
|
||||
</div>
|
||||
<!-- 分割预览-->
|
||||
<div class="knw-file-preview" v-if="activeStep === 2">
|
||||
<SplitterDocPreview
|
||||
:flies-list="files"
|
||||
:splitter-params="splitterParams"
|
||||
:page-number="pagination.currentPage"
|
||||
:page-size="pagination.pageSize"
|
||||
@update-total="handleTotalUpdate"
|
||||
/>
|
||||
</div>
|
||||
<!-- 确认导入-->
|
||||
<div class="knw-file-confirm" v-if="activeStep === 3">
|
||||
<ComfirmImportDocument
|
||||
:splitter-params="splitterParams"
|
||||
:files-list="files"
|
||||
ref="confirmImportRef"
|
||||
@loading-finish="finishImport"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="height: 40px"></div>
|
||||
<div class="imp-doc-footer">
|
||||
<div v-if="activeStep === 2" class="imp-doc-page-container">
|
||||
<ElPagination
|
||||
:page-sizes="[10, 20]"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
:total="pagination.total"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
<div class="step-body">
|
||||
<ImportKnowledgeFileContainer
|
||||
v-if="activeStep === 0"
|
||||
ref="fileUploadRef"
|
||||
/>
|
||||
<SegmenterDoc
|
||||
v-else-if="activeStep === 1"
|
||||
ref="segmenterDocRef"
|
||||
:analysis-items="analysisItems"
|
||||
/>
|
||||
<SplitterDocPreview
|
||||
v-else-if="activeStep === 2"
|
||||
:preview-items="previewItems"
|
||||
/>
|
||||
<ComfirmImportDocument
|
||||
v-else
|
||||
:preview-items="previewItems"
|
||||
:commit-results="commitResults"
|
||||
:loading="committing"
|
||||
/>
|
||||
</div>
|
||||
<ElButton @click="goToPreviousStep" type="primary" v-if="activeStep >= 1">
|
||||
</div>
|
||||
|
||||
<div class="imp-doc-footer">
|
||||
<ElButton v-if="canGoPrevious" @click="goToPreviousStep">
|
||||
{{ $t('button.previousStep') }}
|
||||
</ElButton>
|
||||
<ElButton @click="goToNextStep" type="primary" v-if="activeStep < 3">
|
||||
<ElButton
|
||||
v-if="activeStep < 3"
|
||||
type="primary"
|
||||
:loading="analyzing || previewing"
|
||||
@click="goToNextStep"
|
||||
>
|
||||
{{ $t('button.nextStep') }}
|
||||
</ElButton>
|
||||
<ElButton
|
||||
@click="confirmImport"
|
||||
v-else
|
||||
type="primary"
|
||||
v-if="activeStep === 3"
|
||||
:loading="loadingSave"
|
||||
:disabled="loadingSave"
|
||||
:loading="committing"
|
||||
:disabled="committing"
|
||||
@click="confirmImport"
|
||||
>
|
||||
{{ $t('button.startImport') }}
|
||||
</ElButton>
|
||||
@@ -194,60 +220,41 @@ const finishImport = () => {
|
||||
<style scoped>
|
||||
.imp-doc-kno-container {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
background-color: var(--el-bg-color);
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
height: 100%;
|
||||
flex-direction: column;
|
||||
padding: 24px;
|
||||
border-radius: 16px;
|
||||
background: var(--el-bg-color);
|
||||
}
|
||||
|
||||
.imp-doc-kno-content {
|
||||
flex: 1;
|
||||
padding-top: 20px;
|
||||
overflow: auto;
|
||||
}
|
||||
.imp-doc-footer {
|
||||
position: absolute;
|
||||
bottom: 20px;
|
||||
right: 20px;
|
||||
display: flex;
|
||||
height: 40px;
|
||||
background-color: var(--el-bg-color);
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
.knw-file-preview {
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
padding-top: 16px;
|
||||
overflow: auto;
|
||||
}
|
||||
.imp-doc-page-container {
|
||||
margin-right: 12px;
|
||||
}
|
||||
.knw-file-confirm {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
:deep(.el-step__head) {
|
||||
--step-item-bg: rgba(0, 0, 0, 0.06);
|
||||
--step-item-solid-bg: rgba(0, 0, 0, 0.15);
|
||||
--accent-foreground: rgba(0, 0, 0, 0.45);
|
||||
.step-card {
|
||||
padding: 20px 24px;
|
||||
border: 1px solid var(--el-border-color-light);
|
||||
border-radius: 16px;
|
||||
background: var(--el-fill-color-blank);
|
||||
}
|
||||
:deep(.el-step__head:where(.dark, .dark *)) {
|
||||
--step-item-bg: var(--el-text-color-placeholder);
|
||||
--step-item-solid-bg: var(--el-text-color-placeholder);
|
||||
--accent-foreground: var(--primary-foreground);
|
||||
|
||||
.step-body {
|
||||
flex: 1;
|
||||
padding-bottom: 72px;
|
||||
}
|
||||
:deep(.el-step__head.is-finish) {
|
||||
--step-item-bg: hsl(var(--primary));
|
||||
--step-item-solid-bg: hsl(var(--primary));
|
||||
--accent-foreground: var(--primary-foreground);
|
||||
}
|
||||
:deep(.el-step__icon.is-icon) {
|
||||
width: 120px;
|
||||
background-color: var(--table-header-bg-color);
|
||||
}
|
||||
:deep(.el-step__line) {
|
||||
background-color: var(--step-item-solid-bg);
|
||||
|
||||
.imp-doc-footer {
|
||||
position: absolute;
|
||||
right: 24px;
|
||||
bottom: 24px;
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -20,7 +20,7 @@ const fileData = ref<FileInfo[]>([]);
|
||||
const filesPath = ref([]);
|
||||
defineExpose({
|
||||
getFilesData() {
|
||||
return fileData.value;
|
||||
return fileData.value.filter((item) => item.filePath);
|
||||
},
|
||||
});
|
||||
function handleSuccess(response: any) {
|
||||
|
||||
@@ -1,189 +1,373 @@
|
||||
<script setup lang="ts">
|
||||
import { reactive, ref } from 'vue';
|
||||
import { computed, reactive, watch } from 'vue';
|
||||
|
||||
import { $t } from '@easyflow/locales';
|
||||
|
||||
import {
|
||||
ElAlert,
|
||||
ElCard,
|
||||
ElCol,
|
||||
ElForm,
|
||||
ElFormItem,
|
||||
ElInput,
|
||||
ElOption,
|
||||
ElRow,
|
||||
ElSelect,
|
||||
ElSlider,
|
||||
ElTag,
|
||||
} from 'element-plus';
|
||||
|
||||
const formRef = ref();
|
||||
const form = reactive({
|
||||
fileType: 'doc',
|
||||
splitterName: 'SimpleDocumentSplitter',
|
||||
chunkSize: 512,
|
||||
overlapSize: 128,
|
||||
regex: '',
|
||||
rowsPerChunk: 0,
|
||||
mdSplitterLevel: 1,
|
||||
});
|
||||
const fileTypes = [
|
||||
interface StrategyConfig {
|
||||
chunkSize?: number;
|
||||
mdSplitterLevel?: number;
|
||||
overlapSize?: number;
|
||||
regex?: string;
|
||||
rowsPerChunk?: number;
|
||||
strategyCode?: string;
|
||||
}
|
||||
|
||||
interface StrategyCandidate {
|
||||
score?: number;
|
||||
strategyCode: string;
|
||||
strategyLabel: string;
|
||||
}
|
||||
|
||||
interface AnalysisResult {
|
||||
candidateStrategies?: StrategyCandidate[];
|
||||
confidence?: number;
|
||||
reasons?: string[];
|
||||
recommendedStrategyCode?: string;
|
||||
recommendedStrategyLabel?: string;
|
||||
recommendedStructureType?: string;
|
||||
}
|
||||
|
||||
interface AnalyzeItem {
|
||||
analysis?: AnalysisResult;
|
||||
fileName: string;
|
||||
filePath: string;
|
||||
strategyConfig?: StrategyConfig;
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
analysisItems?: AnalyzeItem[];
|
||||
}>();
|
||||
|
||||
const strategyOptions = [
|
||||
{
|
||||
label: $t('documentCollection.splitterDoc.document'),
|
||||
value: 'doc',
|
||||
label: $t('documentCollection.splitterDoc.autoStrategy'),
|
||||
value: 'AUTO',
|
||||
},
|
||||
{
|
||||
label: $t('documentCollection.splitterDoc.markdownSection'),
|
||||
value: 'MARKDOWN_SECTION',
|
||||
},
|
||||
{
|
||||
label: $t('documentCollection.splitterDoc.outlineSection'),
|
||||
value: 'OUTLINE_SECTION',
|
||||
},
|
||||
{
|
||||
label: $t('documentCollection.splitterDoc.qaPair'),
|
||||
value: 'QA_PAIR',
|
||||
},
|
||||
{
|
||||
label: $t('documentCollection.splitterDoc.paragraphLength'),
|
||||
value: 'PARAGRAPH_LENGTH',
|
||||
},
|
||||
{
|
||||
label: $t('documentCollection.splitterDoc.customRegex'),
|
||||
value: 'CUSTOM_REGEX',
|
||||
},
|
||||
];
|
||||
const splitterNames = [
|
||||
{
|
||||
label: $t('documentCollection.splitterDoc.simpleDocumentSplitter'),
|
||||
value: 'SimpleDocumentSplitter',
|
||||
|
||||
const mdLevels = [1, 2, 3, 4, 5, 6];
|
||||
|
||||
const formMap = reactive<Record<string, StrategyConfig>>({});
|
||||
|
||||
watch(
|
||||
() => props.analysisItems,
|
||||
(items) => {
|
||||
for (const item of items || []) {
|
||||
formMap[item.filePath] = {
|
||||
chunkSize: item.strategyConfig?.chunkSize ?? 512,
|
||||
mdSplitterLevel: item.strategyConfig?.mdSplitterLevel ?? 2,
|
||||
overlapSize: item.strategyConfig?.overlapSize ?? 128,
|
||||
regex: item.strategyConfig?.regex ?? '',
|
||||
rowsPerChunk: item.strategyConfig?.rowsPerChunk ?? 1,
|
||||
strategyCode:
|
||||
item.strategyConfig?.strategyCode ||
|
||||
item.analysis?.recommendedStrategyCode ||
|
||||
'AUTO',
|
||||
};
|
||||
}
|
||||
},
|
||||
{
|
||||
label: $t('documentCollection.splitterDoc.simpleTokenizeSplitter'),
|
||||
value: 'SimpleTokenizeSplitter',
|
||||
},
|
||||
{
|
||||
label: $t('documentCollection.splitterDoc.regexDocumentSplitter'),
|
||||
value: 'RegexDocumentSplitter',
|
||||
},
|
||||
{
|
||||
label: $t('documentCollection.splitterDoc.markdownHeaderSplitter'),
|
||||
value: 'MarkdownHeaderSplitter',
|
||||
},
|
||||
];
|
||||
const mdSplitterLevel = [
|
||||
{
|
||||
label: '#',
|
||||
value: 1,
|
||||
},
|
||||
{
|
||||
label: '##',
|
||||
value: 2,
|
||||
},
|
||||
{
|
||||
label: '###',
|
||||
value: 3,
|
||||
},
|
||||
{
|
||||
label: '####',
|
||||
value: 4,
|
||||
},
|
||||
{
|
||||
label: '#####',
|
||||
value: 5,
|
||||
},
|
||||
{
|
||||
label: '######',
|
||||
value: 6,
|
||||
},
|
||||
];
|
||||
const rules = {
|
||||
name: [
|
||||
{ required: true, message: 'Please input Activity name', trigger: 'blur' },
|
||||
],
|
||||
region: [
|
||||
{
|
||||
required: true,
|
||||
message: 'Please select Activity zone',
|
||||
trigger: 'change',
|
||||
},
|
||||
],
|
||||
};
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
const items = computed(() => props.analysisItems ?? []);
|
||||
|
||||
defineExpose({
|
||||
getSplitterFormValues() {
|
||||
return form;
|
||||
getPreviewRequestItems() {
|
||||
return items.value.map((item) => ({
|
||||
fileName: item.fileName,
|
||||
filePath: item.filePath,
|
||||
strategyConfig: {
|
||||
...formMap[item.filePath],
|
||||
},
|
||||
}));
|
||||
},
|
||||
});
|
||||
|
||||
function showLengthSettings(strategyCode?: string) {
|
||||
return [
|
||||
'AUTO',
|
||||
'MARKDOWN_SECTION',
|
||||
'OUTLINE_SECTION',
|
||||
'PARAGRAPH_LENGTH',
|
||||
].includes(strategyCode || '');
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="splitter-doc-container">
|
||||
<ElForm
|
||||
ref="formRef"
|
||||
:model="form"
|
||||
:rules="rules"
|
||||
label-width="auto"
|
||||
class="custom-form"
|
||||
>
|
||||
<ElFormItem
|
||||
:label="$t('documentCollection.splitterDoc.fileType')"
|
||||
prop="fileType"
|
||||
<div class="strategy-container">
|
||||
<ElAlert
|
||||
:title="$t('documentCollection.importDoc.analysisTip')"
|
||||
type="info"
|
||||
:closable="false"
|
||||
class="strategy-tip"
|
||||
/>
|
||||
|
||||
<div class="strategy-list">
|
||||
<ElCard
|
||||
v-for="item in items"
|
||||
:key="item.filePath"
|
||||
class="strategy-card"
|
||||
shadow="never"
|
||||
>
|
||||
<ElSelect v-model="form.fileType">
|
||||
<ElOption
|
||||
v-for="item in fileTypes"
|
||||
:key="item.value"
|
||||
v-bind="item"
|
||||
:label="item.label"
|
||||
/>
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
<ElFormItem
|
||||
:label="$t('documentCollection.splitterDoc.splitterName')"
|
||||
prop="splitterName"
|
||||
>
|
||||
<ElSelect v-model="form.splitterName">
|
||||
<ElOption
|
||||
v-for="item in splitterNames"
|
||||
:key="item.value"
|
||||
v-bind="item"
|
||||
:label="item.label"
|
||||
/>
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
<ElFormItem
|
||||
:label="$t('documentCollection.splitterDoc.chunkSize')"
|
||||
v-if="
|
||||
form.splitterName === 'SimpleDocumentSplitter' ||
|
||||
form.splitterName === 'SimpleTokenizeSplitter'
|
||||
"
|
||||
prop="chunkSize"
|
||||
>
|
||||
<ElSlider v-model="form.chunkSize" show-input :max="2048" />
|
||||
</ElFormItem>
|
||||
<ElFormItem
|
||||
:label="$t('documentCollection.splitterDoc.overlapSize')"
|
||||
v-if="
|
||||
form.splitterName === 'SimpleDocumentSplitter' ||
|
||||
form.splitterName === 'SimpleTokenizeSplitter'
|
||||
"
|
||||
prop="overlapSize"
|
||||
>
|
||||
<ElSlider v-model="form.overlapSize" show-input :max="2048" />
|
||||
</ElFormItem>
|
||||
<ElFormItem
|
||||
:label="$t('documentCollection.splitterDoc.regex')"
|
||||
prop="regex"
|
||||
v-if="form.splitterName === 'RegexDocumentSplitter'"
|
||||
>
|
||||
<ElInput v-model="form.regex" />
|
||||
</ElFormItem>
|
||||
<ElFormItem
|
||||
v-if="form.splitterName === 'MarkdownHeaderSplitter'"
|
||||
:label="$t('documentCollection.splitterDoc.mdSplitterLevel')"
|
||||
prop="splitterName"
|
||||
>
|
||||
<ElSelect v-model="form.mdSplitterLevel">
|
||||
<ElOption
|
||||
v-for="item in mdSplitterLevel"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
<div class="strategy-card__header">
|
||||
<div>
|
||||
<div class="strategy-card__title">{{ item.fileName }}</div>
|
||||
<div class="strategy-card__meta">
|
||||
{{ item.analysis?.recommendedStructureType || '-' }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="strategy-card__badges">
|
||||
<ElTag type="success" effect="plain">
|
||||
{{
|
||||
item.analysis?.recommendedStrategyLabel ||
|
||||
$t('documentCollection.splitterDoc.autoStrategy')
|
||||
}}
|
||||
</ElTag>
|
||||
<ElTag effect="plain">
|
||||
{{ $t('documentCollection.importDoc.confidence') }}
|
||||
{{ item.analysis?.confidence ?? 0 }}
|
||||
</ElTag>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ElRow :gutter="16" class="strategy-card__content">
|
||||
<ElCol :span="12">
|
||||
<div class="strategy-block">
|
||||
<div class="strategy-block__label">
|
||||
{{ $t('documentCollection.importDoc.recommendReason') }}
|
||||
</div>
|
||||
<ul class="strategy-reason-list">
|
||||
<li
|
||||
v-for="reason in item.analysis?.reasons || []"
|
||||
:key="reason"
|
||||
class="strategy-reason-list__item"
|
||||
>
|
||||
{{ reason }}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="strategy-block">
|
||||
<div class="strategy-block__label">
|
||||
{{ $t('documentCollection.importDoc.candidateStrategies') }}
|
||||
</div>
|
||||
<div class="strategy-candidate-list">
|
||||
<ElTag
|
||||
v-for="candidate in item.analysis?.candidateStrategies || []"
|
||||
:key="candidate.strategyCode"
|
||||
effect="plain"
|
||||
>
|
||||
{{ candidate.strategyLabel }} / {{ candidate.score }}
|
||||
</ElTag>
|
||||
</div>
|
||||
</div>
|
||||
</ElCol>
|
||||
|
||||
<ElCol :span="12">
|
||||
<ElForm
|
||||
:model="formMap[item.filePath]"
|
||||
label-position="top"
|
||||
class="strategy-form"
|
||||
>
|
||||
<ElFormItem
|
||||
:label="$t('documentCollection.importDoc.strategySelection')"
|
||||
>
|
||||
<ElSelect
|
||||
v-model="formMap[item.filePath].strategyCode"
|
||||
class="w-full"
|
||||
>
|
||||
<ElOption
|
||||
v-for="option in strategyOptions"
|
||||
:key="option.value"
|
||||
:label="option.label"
|
||||
:value="option.value"
|
||||
/>
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem
|
||||
v-if="showLengthSettings(formMap[item.filePath].strategyCode)"
|
||||
:label="$t('documentCollection.splitterDoc.chunkSize')"
|
||||
>
|
||||
<ElSlider
|
||||
v-model="formMap[item.filePath].chunkSize"
|
||||
:max="2048"
|
||||
:min="128"
|
||||
show-input
|
||||
/>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem
|
||||
v-if="
|
||||
formMap[item.filePath].strategyCode === 'PARAGRAPH_LENGTH' ||
|
||||
formMap[item.filePath].strategyCode === 'AUTO'
|
||||
"
|
||||
:label="$t('documentCollection.splitterDoc.overlapSize')"
|
||||
>
|
||||
<ElSlider
|
||||
v-model="formMap[item.filePath].overlapSize"
|
||||
:max="512"
|
||||
:min="0"
|
||||
show-input
|
||||
/>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem
|
||||
v-if="
|
||||
formMap[item.filePath].strategyCode === 'MARKDOWN_SECTION'
|
||||
"
|
||||
:label="$t('documentCollection.splitterDoc.mdSplitterLevel')"
|
||||
>
|
||||
<ElSelect
|
||||
v-model="formMap[item.filePath].mdSplitterLevel"
|
||||
class="w-full"
|
||||
>
|
||||
<ElOption
|
||||
v-for="level in mdLevels"
|
||||
:key="level"
|
||||
:label="'#'.repeat(level)"
|
||||
:value="level"
|
||||
/>
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem
|
||||
v-if="formMap[item.filePath].strategyCode === 'CUSTOM_REGEX'"
|
||||
:label="$t('documentCollection.splitterDoc.regex')"
|
||||
>
|
||||
<ElInput v-model="formMap[item.filePath].regex" />
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
</ElCard>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.splitter-doc-container {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
align-items: center;
|
||||
.strategy-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
.custom-form {
|
||||
width: 500px;
|
||||
|
||||
.strategy-tip {
|
||||
border-radius: 12px;
|
||||
}
|
||||
.custom-form :deep(.el-input),
|
||||
.custom-form :deep(.ElSelect) {
|
||||
width: 100%;
|
||||
|
||||
.strategy-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.strategy-card {
|
||||
border: 1px solid var(--el-border-color-light);
|
||||
border-radius: 16px;
|
||||
}
|
||||
|
||||
.strategy-card__header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
padding-bottom: 16px;
|
||||
border-bottom: 1px solid var(--el-border-color-lighter);
|
||||
}
|
||||
|
||||
.strategy-card__title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
|
||||
.strategy-card__meta {
|
||||
margin-top: 4px;
|
||||
font-size: 13px;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
|
||||
.strategy-card__badges {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: flex-start;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.strategy-card__content {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.strategy-block {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.strategy-block + .strategy-block {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.strategy-block__label {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
|
||||
.strategy-reason-list {
|
||||
margin: 0;
|
||||
padding-left: 18px;
|
||||
color: var(--el-text-color-regular);
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.strategy-reason-list__item {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.strategy-candidate-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.strategy-form {
|
||||
padding: 16px;
|
||||
border-radius: 12px;
|
||||
background: var(--el-fill-color-light);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,168 +1,286 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref, watch } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { computed, ref, watch } from 'vue';
|
||||
|
||||
import { api } from '#/api/request';
|
||||
import CategoryPanel from '#/components/categoryPanel/CategoryPanel.vue';
|
||||
import PreviewSearchKnowledge from '#/views/ai/documentCollection/PreviewSearchKnowledge.vue';
|
||||
import { $t } from '@easyflow/locales';
|
||||
|
||||
export interface FileInfo {
|
||||
filePath: string;
|
||||
import {
|
||||
ElAlert,
|
||||
ElDescriptions,
|
||||
ElDescriptionsItem,
|
||||
ElEmpty,
|
||||
ElTabPane,
|
||||
ElTabs,
|
||||
ElTag,
|
||||
} from 'element-plus';
|
||||
|
||||
interface ChunkItem {
|
||||
answer?: string;
|
||||
charCount?: number;
|
||||
chunkId?: string;
|
||||
chunkType?: string;
|
||||
content?: string;
|
||||
headingPath?: string[];
|
||||
partNo?: number;
|
||||
partTotal?: number;
|
||||
question?: string;
|
||||
sourceLabel?: string;
|
||||
tokenEstimate?: number;
|
||||
warnings?: string[];
|
||||
}
|
||||
|
||||
interface PreviewItem {
|
||||
analysis?: {
|
||||
confidence?: number;
|
||||
recommendedStructureType?: string;
|
||||
};
|
||||
chunks?: ChunkItem[];
|
||||
fileName: string;
|
||||
previewSessionId: string;
|
||||
strategyLabel?: string;
|
||||
totalChunks?: number;
|
||||
totalWarnings?: number;
|
||||
}
|
||||
const props = defineProps({
|
||||
pageNumber: {
|
||||
default: 1,
|
||||
type: Number,
|
||||
},
|
||||
pageSize: {
|
||||
default: 10,
|
||||
type: Number,
|
||||
},
|
||||
knowledgeId: {
|
||||
default: '',
|
||||
type: String,
|
||||
},
|
||||
fliesList: {
|
||||
default: () => [],
|
||||
type: Array<FileInfo>,
|
||||
},
|
||||
splitterParams: {
|
||||
default: () => {},
|
||||
type: Object,
|
||||
},
|
||||
});
|
||||
const emit = defineEmits(['updateTotal']);
|
||||
const documentList = ref<any[]>([]);
|
||||
const route = useRoute();
|
||||
defineExpose({
|
||||
getFilesData() {
|
||||
return documentList.value.length;
|
||||
},
|
||||
});
|
||||
const knowledgeIdRef = ref<string>((route.query.id as string) || '');
|
||||
const selectedCategory = ref<any>();
|
||||
|
||||
watch(
|
||||
() => props.pageNumber,
|
||||
(newVal) => {
|
||||
if (selectedCategory.value) {
|
||||
splitterDocPreview(
|
||||
newVal,
|
||||
props.pageSize,
|
||||
selectedCategory.value.value,
|
||||
'textSplit',
|
||||
selectedCategory.value.label,
|
||||
);
|
||||
} else {
|
||||
splitterDocPreview(
|
||||
newVal,
|
||||
props.pageSize,
|
||||
props.fliesList[0]!.filePath,
|
||||
'textSplit',
|
||||
props.fliesList[0]!.fileName,
|
||||
);
|
||||
}
|
||||
},
|
||||
const props = defineProps<{
|
||||
previewItems?: PreviewItem[];
|
||||
}>();
|
||||
|
||||
const activeFile = ref('');
|
||||
|
||||
const previewItems = computed(() => props.previewItems ?? []);
|
||||
const currentPreview = computed(
|
||||
() =>
|
||||
previewItems.value.find(
|
||||
(item) => item.previewSessionId === activeFile.value,
|
||||
) || previewItems.value[0],
|
||||
);
|
||||
|
||||
watch(
|
||||
() => props.pageSize,
|
||||
(newVal) => {
|
||||
if (selectedCategory.value) {
|
||||
splitterDocPreview(
|
||||
props.pageNumber,
|
||||
newVal,
|
||||
selectedCategory.value.value,
|
||||
'textSplit',
|
||||
selectedCategory.value.label,
|
||||
);
|
||||
} else {
|
||||
splitterDocPreview(
|
||||
props.pageNumber,
|
||||
newVal,
|
||||
props.fliesList[0]!.filePath,
|
||||
'textSplit',
|
||||
props.fliesList[0]!.fileName,
|
||||
);
|
||||
previewItems,
|
||||
(items) => {
|
||||
if (items.length === 0) {
|
||||
activeFile.value = '';
|
||||
return;
|
||||
}
|
||||
if (!items.some((item) => item.previewSessionId === activeFile.value)) {
|
||||
activeFile.value = items[0]?.previewSessionId || '';
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
function splitterDocPreview(
|
||||
pageNumber: number,
|
||||
pageSize: number,
|
||||
filePath: string,
|
||||
operation: string,
|
||||
fileOriginName: string,
|
||||
) {
|
||||
api
|
||||
.post('/api/v1/document/textSplit', {
|
||||
pageNumber,
|
||||
pageSize,
|
||||
filePath,
|
||||
operation,
|
||||
knowledgeId: knowledgeIdRef.value,
|
||||
fileOriginName,
|
||||
...props.splitterParams,
|
||||
})
|
||||
.then((res) => {
|
||||
if (res.errorCode === 0) {
|
||||
documentList.value = res.data.previewData;
|
||||
emit('updateTotal', res.data.total);
|
||||
}
|
||||
});
|
||||
}
|
||||
onMounted(() => {
|
||||
if (props.fliesList.length === 0) {
|
||||
return;
|
||||
}
|
||||
splitterDocPreview(
|
||||
props.pageNumber,
|
||||
props.pageSize,
|
||||
props.fliesList[0]!.filePath,
|
||||
'textSplit',
|
||||
props.fliesList[0]!.fileName,
|
||||
);
|
||||
});
|
||||
const changeCategory = (category: any) => {
|
||||
selectedCategory.value = category;
|
||||
splitterDocPreview(
|
||||
props.pageNumber,
|
||||
props.pageSize,
|
||||
category.value,
|
||||
'textSplit',
|
||||
category.label,
|
||||
);
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="splitter-doc-container">
|
||||
<div>
|
||||
<CategoryPanel
|
||||
:categories="fliesList"
|
||||
title-key="fileName"
|
||||
:need-hide-collapse="true"
|
||||
:expand-width="200"
|
||||
value-key="filePath"
|
||||
:default-selected-category="fliesList[0]!.filePath"
|
||||
@click="changeCategory"
|
||||
/>
|
||||
</div>
|
||||
<div class="preview-shell">
|
||||
<ElAlert
|
||||
:title="$t('documentCollection.importDoc.previewTip')"
|
||||
type="info"
|
||||
:closable="false"
|
||||
class="preview-alert"
|
||||
/>
|
||||
|
||||
<div class="preview-container">
|
||||
<PreviewSearchKnowledge :data="documentList" :hide-score="true" />
|
||||
<ElEmpty
|
||||
v-if="previewItems.length === 0"
|
||||
:description="$t('documentCollection.importDoc.previewEmpty')"
|
||||
/>
|
||||
|
||||
<div v-else class="preview-panel">
|
||||
<ElTabs v-model="activeFile" class="preview-tabs">
|
||||
<ElTabPane
|
||||
v-for="item in previewItems"
|
||||
:key="item.previewSessionId"
|
||||
:label="item.fileName"
|
||||
:name="item.previewSessionId"
|
||||
/>
|
||||
</ElTabs>
|
||||
|
||||
<div v-if="currentPreview" class="preview-detail">
|
||||
<ElDescriptions :column="4" border class="preview-summary">
|
||||
<ElDescriptionsItem :label="$t('documentCollection.fileName')">
|
||||
{{ currentPreview.fileName }}
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem
|
||||
:label="$t('documentCollection.importDoc.strategySelection')"
|
||||
>
|
||||
{{ currentPreview.strategyLabel || '-' }}
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem :label="$t('documentCollection.total')">
|
||||
{{ currentPreview.totalChunks || 0 }}
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem
|
||||
:label="$t('documentCollection.importDoc.warningCount')"
|
||||
>
|
||||
{{ currentPreview.totalWarnings || 0 }}
|
||||
</ElDescriptionsItem>
|
||||
</ElDescriptions>
|
||||
|
||||
<div class="chunk-list">
|
||||
<div
|
||||
v-for="chunk in currentPreview.chunks || []"
|
||||
:key="chunk.chunkId"
|
||||
class="chunk-card"
|
||||
>
|
||||
<div class="chunk-card__header">
|
||||
<div>
|
||||
<div class="chunk-card__title">
|
||||
{{ chunk.sourceLabel || chunk.chunkId }}
|
||||
</div>
|
||||
<div
|
||||
v-if="chunk.headingPath && chunk.headingPath.length > 0"
|
||||
class="chunk-card__path"
|
||||
>
|
||||
{{ chunk.headingPath.join(' / ') }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="chunk-card__meta">
|
||||
<ElTag effect="plain">{{ chunk.chunkType || '-' }}</ElTag>
|
||||
<ElTag effect="plain">
|
||||
{{ chunk.charCount || 0 }} / {{ chunk.tokenEstimate || 0 }}
|
||||
</ElTag>
|
||||
<ElTag
|
||||
v-if="(chunk.partTotal || 1) > 1"
|
||||
type="warning"
|
||||
effect="plain"
|
||||
>
|
||||
{{ chunk.partNo }}/{{ chunk.partTotal }}
|
||||
</ElTag>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="chunk.chunkType === 'qa_pair'" class="qa-block">
|
||||
<div class="qa-block__item">
|
||||
<span class="qa-block__label">Q</span>
|
||||
<span>{{ chunk.question }}</span>
|
||||
</div>
|
||||
<div class="qa-block__item">
|
||||
<span class="qa-block__label">A</span>
|
||||
<span>{{ chunk.answer }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<pre class="chunk-card__content">{{ chunk.content }}</pre>
|
||||
|
||||
<div
|
||||
v-if="chunk.warnings && chunk.warnings.length > 0"
|
||||
class="chunk-card__warnings"
|
||||
>
|
||||
<ElTag
|
||||
v-for="warning in chunk.warnings"
|
||||
:key="warning"
|
||||
type="warning"
|
||||
effect="plain"
|
||||
>
|
||||
{{ warning }}
|
||||
</ElTag>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.splitter-doc-container {
|
||||
height: 100%;
|
||||
.preview-shell {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
.preview-container {
|
||||
flex: 1;
|
||||
overflow: scroll;
|
||||
|
||||
.preview-alert {
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.preview-panel {
|
||||
padding: 20px;
|
||||
border: 1px solid var(--el-border-color-light);
|
||||
border-radius: 16px;
|
||||
background: var(--el-bg-color);
|
||||
}
|
||||
|
||||
.preview-summary {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.chunk-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
max-height: 560px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.chunk-card {
|
||||
padding: 16px;
|
||||
border: 1px solid var(--el-border-color-lighter);
|
||||
border-radius: 14px;
|
||||
background: var(--el-fill-color-blank);
|
||||
}
|
||||
|
||||
.chunk-card__header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.chunk-card__title {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
|
||||
.chunk-card__path {
|
||||
margin-top: 6px;
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
|
||||
.chunk-card__meta {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.chunk-card__content {
|
||||
margin: 16px 0 0;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
font-family: inherit;
|
||||
line-height: 1.7;
|
||||
color: var(--el-text-color-regular);
|
||||
}
|
||||
|
||||
.chunk-card__warnings {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.qa-block {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
margin-top: 16px;
|
||||
padding: 12px;
|
||||
border-radius: 12px;
|
||||
background: var(--el-fill-color-light);
|
||||
}
|
||||
|
||||
.qa-block__item {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.qa-block__label {
|
||||
display: inline-flex;
|
||||
width: 22px;
|
||||
justify-content: center;
|
||||
border-radius: 999px;
|
||||
background: var(--el-color-primary-light-9);
|
||||
color: var(--el-color-primary);
|
||||
font-weight: 600;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user