645 lines
15 KiB
Vue
645 lines
15 KiB
Vue
<script setup lang="ts">
|
|
import { computed, reactive, ref, watch } from 'vue';
|
|
|
|
import { $t } from '@easyflow/locales';
|
|
|
|
import {
|
|
ElButton,
|
|
ElForm,
|
|
ElFormItem,
|
|
ElInput,
|
|
ElInputNumber,
|
|
ElMessage,
|
|
ElOption,
|
|
ElSelect,
|
|
ElSlider,
|
|
ElSwitch,
|
|
} from 'element-plus';
|
|
|
|
import { api } from '#/api/request';
|
|
import { buildKnowledgePath } from '#/views/ai/documentCollection/share-path';
|
|
import SplitterDocPreview from '#/views/ai/documentCollection/SplitterDocPreview.vue';
|
|
|
|
interface SourceRange {
|
|
end: number;
|
|
start: number;
|
|
}
|
|
|
|
interface ChunkItem {
|
|
answer?: string;
|
|
charCount?: number;
|
|
chunkId?: string;
|
|
chunkType?: string;
|
|
content?: string;
|
|
headingPath?: string[];
|
|
options?: Record<string, any>;
|
|
partNo?: number;
|
|
partTotal?: number;
|
|
question?: string;
|
|
sourceLabel?: string;
|
|
sourceRanges?: SourceRange[];
|
|
tokenEstimate?: number;
|
|
warnings?: string[];
|
|
}
|
|
|
|
interface PreviewItem {
|
|
analysis?: {
|
|
normalizedContent?: string;
|
|
recommendedStrategyLabel?: string;
|
|
};
|
|
chunks?: ChunkItem[];
|
|
fileName: string;
|
|
normalizedContent?: string;
|
|
pageNo?: number;
|
|
pageSize?: number;
|
|
previewSessionId: string;
|
|
strategyCode?: string;
|
|
strategyLabel?: string;
|
|
totalChunks?: number;
|
|
totalWarnings?: number;
|
|
}
|
|
|
|
const props = defineProps({
|
|
documentId: {
|
|
type: String,
|
|
required: true,
|
|
},
|
|
documentTitle: {
|
|
type: String,
|
|
default: '',
|
|
},
|
|
endpointPrefix: {
|
|
type: String,
|
|
default: '',
|
|
},
|
|
knowledgeId: {
|
|
type: String,
|
|
required: true,
|
|
},
|
|
requestClient: {
|
|
type: Object as any,
|
|
default: () => api,
|
|
},
|
|
});
|
|
|
|
const emits = defineEmits(['cancel', 'started']);
|
|
|
|
const createDefaultFormState = () => ({
|
|
chunkSize: 512,
|
|
mdSplitterLevel: 2,
|
|
overlapSize: 128,
|
|
regex: '',
|
|
retainRegexMatch: false,
|
|
rowsPerChunk: 10,
|
|
strategyCode: 'AUTO',
|
|
});
|
|
|
|
const formState = reactive(createDefaultFormState());
|
|
const previewItems = ref<PreviewItem[]>([]);
|
|
const currentPreviewSessionId = ref('');
|
|
const activeDocumentId = ref(props.documentId || '');
|
|
const previewError = ref('');
|
|
const previewLoading = ref(false);
|
|
const startLoading = ref(false);
|
|
const previewPageSize = 20;
|
|
let previewDebounceTimer: null | ReturnType<typeof setTimeout> = null;
|
|
let previewSequence = 0;
|
|
|
|
const strategyOptions = [
|
|
{
|
|
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 fileExt = computed(
|
|
() =>
|
|
String(props.documentTitle || '')
|
|
.split('.')
|
|
.pop()
|
|
?.toLowerCase() || '',
|
|
);
|
|
|
|
const isPptx = computed(() => fileExt.value === 'pptx');
|
|
const isXlsx = computed(() => fileExt.value === 'xlsx');
|
|
const isCsv = computed(() => fileExt.value === 'csv');
|
|
const isTabular = computed(() => isXlsx.value || isCsv.value);
|
|
const showStrategySelector = computed(() => !isPptx.value && !isTabular.value);
|
|
|
|
const mdLevels = [1, 2, 3, 4, 5, 6];
|
|
|
|
const showLengthSettings = (strategyCode?: string) =>
|
|
['AUTO', 'MARKDOWN_SECTION', 'OUTLINE_SECTION', 'PARAGRAPH_LENGTH'].includes(
|
|
strategyCode || '',
|
|
);
|
|
|
|
const showOverlapSettings = (strategyCode?: string) =>
|
|
['AUTO', 'PARAGRAPH_LENGTH'].includes(strategyCode || '');
|
|
|
|
const maxOverlapSize = computed(() =>
|
|
Math.max(0, Number(formState.chunkSize || 0) - 1),
|
|
);
|
|
|
|
const clearPreviewTimer = () => {
|
|
if (!previewDebounceTimer) {
|
|
return;
|
|
}
|
|
clearTimeout(previewDebounceTimer);
|
|
previewDebounceTimer = null;
|
|
};
|
|
|
|
const resetPreviewState = () => {
|
|
previewItems.value = [];
|
|
currentPreviewSessionId.value = '';
|
|
previewError.value = '';
|
|
};
|
|
|
|
const buildStrategyConfig = () => {
|
|
if (isPptx.value) {
|
|
return {
|
|
strategyCode: 'OFFICE_PPTX_PAGE',
|
|
};
|
|
}
|
|
if (isTabular.value) {
|
|
return {
|
|
rowsPerChunk: formState.rowsPerChunk,
|
|
strategyCode: isCsv.value ? 'TABLE_ROW' : 'OFFICE_XLSX_ROW_WINDOW',
|
|
};
|
|
}
|
|
return {
|
|
...formState,
|
|
};
|
|
};
|
|
|
|
const normalizeSourceRanges = (ranges?: SourceRange[]) =>
|
|
Array.isArray(ranges)
|
|
? ranges.filter(
|
|
(item) =>
|
|
Number.isFinite(item?.start) &&
|
|
Number.isFinite(item?.end) &&
|
|
Number(item.end) > Number(item.start),
|
|
)
|
|
: [];
|
|
|
|
const normalizePreviewItems = (items: PreviewItem[]) =>
|
|
(items || []).map((item) => ({
|
|
...item,
|
|
normalizedContent:
|
|
item.normalizedContent || item.analysis?.normalizedContent || '',
|
|
strategyLabel:
|
|
item.strategyLabel || item.analysis?.recommendedStrategyLabel || '',
|
|
totalChunks:
|
|
Number(item.totalChunks || 0) > 0
|
|
? item.totalChunks
|
|
: (item.chunks || []).length,
|
|
chunks: (item.chunks || []).map((chunk) => ({
|
|
...chunk,
|
|
sourceRanges: normalizeSourceRanges(
|
|
chunk.sourceRanges ||
|
|
(Array.isArray(chunk.options?.sourceRanges)
|
|
? chunk.options?.sourceRanges
|
|
: []),
|
|
),
|
|
})),
|
|
}));
|
|
|
|
const generatePreview = async () => {
|
|
if (!activeDocumentId.value) {
|
|
return;
|
|
}
|
|
const requestSequence = ++previewSequence;
|
|
previewLoading.value = true;
|
|
previewError.value = '';
|
|
try {
|
|
const res = await props.requestClient.post(
|
|
buildKnowledgePath(
|
|
props.endpointPrefix,
|
|
'/api/v1/document/import/task/preview',
|
|
),
|
|
{
|
|
documentId: activeDocumentId.value,
|
|
files: [
|
|
{
|
|
strategyConfig: buildStrategyConfig(),
|
|
},
|
|
],
|
|
knowledgeId: props.knowledgeId,
|
|
pageNo: 1,
|
|
pageSize: previewPageSize,
|
|
},
|
|
);
|
|
if (requestSequence !== previewSequence) {
|
|
return;
|
|
}
|
|
const items = normalizePreviewItems(
|
|
(res.data?.items || []) as PreviewItem[],
|
|
);
|
|
previewItems.value = items;
|
|
currentPreviewSessionId.value = items[0]?.previewSessionId || '';
|
|
previewError.value = '';
|
|
} catch (error: any) {
|
|
if (requestSequence !== previewSequence) {
|
|
return;
|
|
}
|
|
const message =
|
|
error?.message || $t('documentCollection.importDoc.previewRequestFailed');
|
|
previewItems.value = [];
|
|
currentPreviewSessionId.value = '';
|
|
previewError.value = message;
|
|
ElMessage.error(message);
|
|
} finally {
|
|
if (requestSequence === previewSequence) {
|
|
previewLoading.value = false;
|
|
}
|
|
}
|
|
};
|
|
|
|
const loadPreviewPage = async (pageNo: number) => {
|
|
if (
|
|
!activeDocumentId.value ||
|
|
!currentPreviewSessionId.value ||
|
|
previewLoading.value
|
|
) {
|
|
return;
|
|
}
|
|
const requestSequence = ++previewSequence;
|
|
previewLoading.value = true;
|
|
previewError.value = '';
|
|
try {
|
|
const res = await props.requestClient.post(
|
|
buildKnowledgePath(
|
|
props.endpointPrefix,
|
|
'/api/v1/document/import/task/preview',
|
|
),
|
|
{
|
|
documentId: activeDocumentId.value,
|
|
knowledgeId: props.knowledgeId,
|
|
pageNo,
|
|
pageSize: previewPageSize,
|
|
previewSessionId: currentPreviewSessionId.value,
|
|
},
|
|
);
|
|
if (requestSequence !== previewSequence) {
|
|
return;
|
|
}
|
|
previewItems.value = normalizePreviewItems(
|
|
(res.data?.items || []) as PreviewItem[],
|
|
);
|
|
} catch (error: any) {
|
|
if (requestSequence !== previewSequence) {
|
|
return;
|
|
}
|
|
const message =
|
|
error?.message || $t('documentCollection.importDoc.previewRequestFailed');
|
|
previewError.value = message;
|
|
ElMessage.error(message);
|
|
} finally {
|
|
if (requestSequence === previewSequence) {
|
|
previewLoading.value = false;
|
|
}
|
|
}
|
|
};
|
|
|
|
const schedulePreviewGeneration = () => {
|
|
if (!activeDocumentId.value) {
|
|
return;
|
|
}
|
|
clearPreviewTimer();
|
|
resetPreviewState();
|
|
previewDebounceTimer = setTimeout(() => {
|
|
previewDebounceTimer = null;
|
|
void generatePreview();
|
|
}, 320);
|
|
};
|
|
|
|
const handlePreviewSessionChange = (previewSessionId: string) => {
|
|
currentPreviewSessionId.value = previewSessionId;
|
|
};
|
|
|
|
const handleCancel = () => {
|
|
previewSequence += 1;
|
|
clearPreviewTimer();
|
|
previewLoading.value = false;
|
|
emits('cancel');
|
|
};
|
|
|
|
const startIndex = async () => {
|
|
if (!currentPreviewSessionId.value) {
|
|
ElMessage.warning($t('documentCollection.importDoc.previewEmpty'));
|
|
return;
|
|
}
|
|
startLoading.value = true;
|
|
try {
|
|
const res = await props.requestClient.post(
|
|
buildKnowledgePath(
|
|
props.endpointPrefix,
|
|
'/api/v1/document/import/task/startIndex',
|
|
),
|
|
{
|
|
documentId: activeDocumentId.value,
|
|
knowledgeId: props.knowledgeId,
|
|
previewSessionId: currentPreviewSessionId.value,
|
|
},
|
|
);
|
|
if (res.errorCode === 0) {
|
|
ElMessage.success($t('documentCollection.importDoc.indexQueued'));
|
|
emits('started');
|
|
}
|
|
} finally {
|
|
startLoading.value = false;
|
|
}
|
|
};
|
|
|
|
watch(
|
|
() => props.documentId,
|
|
(value) => {
|
|
activeDocumentId.value = value || '';
|
|
previewSequence += 1;
|
|
clearPreviewTimer();
|
|
Object.assign(formState, createDefaultFormState());
|
|
if (isPptx.value) {
|
|
formState.strategyCode = 'OFFICE_PPTX_PAGE';
|
|
}
|
|
if (isTabular.value) {
|
|
formState.strategyCode = isCsv.value
|
|
? 'TABLE_ROW'
|
|
: 'OFFICE_XLSX_ROW_WINDOW';
|
|
formState.rowsPerChunk = 10;
|
|
}
|
|
resetPreviewState();
|
|
if (activeDocumentId.value) {
|
|
schedulePreviewGeneration();
|
|
}
|
|
},
|
|
{ immediate: true },
|
|
);
|
|
|
|
watch(
|
|
() => formState.chunkSize,
|
|
() => {
|
|
if (formState.overlapSize > maxOverlapSize.value) {
|
|
formState.overlapSize = maxOverlapSize.value;
|
|
}
|
|
},
|
|
);
|
|
|
|
watch(
|
|
formState,
|
|
() => {
|
|
if (!activeDocumentId.value) {
|
|
return;
|
|
}
|
|
schedulePreviewGeneration();
|
|
},
|
|
{ deep: true },
|
|
);
|
|
</script>
|
|
|
|
<template>
|
|
<div class="workbench">
|
|
<ElForm :model="formState" label-position="top" class="workbench__form">
|
|
<div class="workbench__form-grid">
|
|
<ElFormItem
|
|
v-if="showStrategySelector"
|
|
:label="$t('documentCollection.importDoc.strategySelection')"
|
|
class="workbench__form-full"
|
|
>
|
|
<ElSelect v-model="formState.strategyCode" class="w-full">
|
|
<ElOption
|
|
v-for="option in strategyOptions"
|
|
:key="option.value"
|
|
:label="option.label"
|
|
:value="option.value"
|
|
/>
|
|
</ElSelect>
|
|
</ElFormItem>
|
|
|
|
<ElFormItem
|
|
v-if="isTabular"
|
|
label="每多少行分一块"
|
|
class="workbench__form-full"
|
|
>
|
|
<ElInputNumber
|
|
v-model="formState.rowsPerChunk"
|
|
:min="1"
|
|
:max="200"
|
|
:step="1"
|
|
class="workbench__rows-input"
|
|
/>
|
|
</ElFormItem>
|
|
|
|
<ElFormItem
|
|
v-if="showLengthSettings(formState.strategyCode)"
|
|
:label="$t('documentCollection.splitterDoc.chunkSize')"
|
|
:class="
|
|
showOverlapSettings(formState.strategyCode)
|
|
? ''
|
|
: 'workbench__form-full'
|
|
"
|
|
>
|
|
<ElSlider
|
|
v-model="formState.chunkSize"
|
|
:max="2048"
|
|
:min="128"
|
|
show-input
|
|
/>
|
|
</ElFormItem>
|
|
|
|
<ElFormItem
|
|
v-if="showOverlapSettings(formState.strategyCode)"
|
|
:label="$t('documentCollection.splitterDoc.overlapSize')"
|
|
>
|
|
<ElSlider
|
|
v-model="formState.overlapSize"
|
|
:max="Math.min(512, maxOverlapSize)"
|
|
:min="0"
|
|
show-input
|
|
/>
|
|
</ElFormItem>
|
|
|
|
<ElFormItem
|
|
v-if="formState.strategyCode === 'MARKDOWN_SECTION'"
|
|
:label="$t('documentCollection.splitterDoc.mdSplitterLevel')"
|
|
class="workbench__form-full"
|
|
>
|
|
<ElSelect v-model="formState.mdSplitterLevel" class="w-full">
|
|
<ElOption
|
|
v-for="level in mdLevels"
|
|
:key="level"
|
|
:label="'#'.repeat(level)"
|
|
:value="level"
|
|
/>
|
|
</ElSelect>
|
|
</ElFormItem>
|
|
|
|
<ElFormItem
|
|
v-if="formState.strategyCode === 'CUSTOM_REGEX'"
|
|
:label="$t('documentCollection.splitterDoc.regex')"
|
|
class="workbench__form-full"
|
|
>
|
|
<ElInput v-model="formState.regex" />
|
|
</ElFormItem>
|
|
|
|
<ElFormItem
|
|
v-if="formState.strategyCode === 'CUSTOM_REGEX'"
|
|
:label="$t('documentCollection.splitterDoc.regexMatchHandling')"
|
|
class="workbench__form-full"
|
|
>
|
|
<ElSwitch
|
|
v-model="formState.retainRegexMatch"
|
|
:active-text="$t('documentCollection.splitterDoc.retainRegexMatch')"
|
|
:inactive-text="
|
|
$t('documentCollection.splitterDoc.discardRegexMatch')
|
|
"
|
|
/>
|
|
</ElFormItem>
|
|
</div>
|
|
</ElForm>
|
|
|
|
<div class="workbench__actions">
|
|
<ElButton :disabled="startLoading" @click="handleCancel">
|
|
{{ $t('button.cancel') }}
|
|
</ElButton>
|
|
<ElButton
|
|
type="primary"
|
|
:disabled="previewLoading || !currentPreviewSessionId"
|
|
:loading="startLoading"
|
|
@click="startIndex"
|
|
>
|
|
{{ $t('button.startIndex') }}
|
|
</ElButton>
|
|
</div>
|
|
|
|
<section class="workbench__content">
|
|
<SplitterDocPreview
|
|
:loading="previewLoading"
|
|
:preview-items="previewItems"
|
|
@page-change="loadPreviewPage"
|
|
@preview-session-change="handlePreviewSessionChange"
|
|
/>
|
|
</section>
|
|
|
|
<div v-if="previewError" class="workbench__error">
|
|
{{ previewError }}
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.workbench {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 20px;
|
|
min-height: 100%;
|
|
padding: 4px 4px 0;
|
|
}
|
|
|
|
.workbench__form {
|
|
padding: 0;
|
|
background: transparent;
|
|
}
|
|
|
|
.workbench__form-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
gap: 6px 18px;
|
|
}
|
|
|
|
.workbench__form-full {
|
|
grid-column: 1 / -1;
|
|
}
|
|
|
|
.workbench__actions {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 8px;
|
|
justify-content: flex-end;
|
|
padding-bottom: 16px;
|
|
border-bottom: 1px solid var(--el-border-color-lighter);
|
|
}
|
|
|
|
.workbench__content {
|
|
min-height: 620px;
|
|
}
|
|
|
|
.workbench__error {
|
|
padding: 14px 16px;
|
|
font-size: 13px;
|
|
line-height: 1.7;
|
|
color: var(--el-color-danger-dark-2);
|
|
background: var(--el-color-danger-light-9);
|
|
background: color-mix(in srgb, var(--el-color-danger-light-9) 88%, white);
|
|
border: 1px solid var(--el-color-danger-light-8);
|
|
border: 1px solid color-mix(in srgb, var(--el-color-danger) 14%, white);
|
|
border-radius: 16px;
|
|
}
|
|
|
|
:deep(.el-form-item__label) {
|
|
padding-bottom: 6px;
|
|
font-size: 13px;
|
|
font-weight: 500;
|
|
color: var(--el-text-color-secondary);
|
|
}
|
|
|
|
:deep(.workbench__form .el-input__wrapper),
|
|
:deep(.workbench__form .el-select__wrapper) {
|
|
box-shadow: 0 0 0 1px rgb(15 23 42 / 7%) inset;
|
|
}
|
|
|
|
.workbench__rows-input {
|
|
width: 100%;
|
|
}
|
|
|
|
:deep(.workbench__form .el-slider__runway) {
|
|
background: rgb(15 23 42 / 8%);
|
|
}
|
|
|
|
@media (max-width: 1180px) {
|
|
.workbench__content {
|
|
min-height: 520px;
|
|
}
|
|
}
|
|
|
|
@media (max-width: 768px) {
|
|
.workbench {
|
|
padding: 0;
|
|
}
|
|
|
|
.workbench__header,
|
|
.workbench__form-grid {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
|
|
.workbench__form,
|
|
.workbench__content {
|
|
min-height: auto;
|
|
}
|
|
}
|
|
|
|
@supports not (color: color-mix(in srgb, red, blue)) {
|
|
.workbench__error {
|
|
border-color: var(--el-color-danger-light-8);
|
|
}
|
|
}
|
|
</style>
|