feat: 增强知识库分块策略流程

- 增加导入分析预览提交与预览态缓存键

- 支持知识库分块策略配置与分块预览

- 重构知识库导入与确认导入前端流程
This commit is contained in:
2026-03-29 17:27:12 +08:00
parent 22ceabff96
commit b6213d0933
11 changed files with 2078 additions and 600 deletions

View File

@@ -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>