From a22f020135e263ce1e9773b3085bca85d03bc4e3 Mon Sep 17 00:00:00 2001
From: asmh1989
Date: Fri, 21 Aug 2026 09:28:02 +0800
Subject: [PATCH] =?UTF-8?q?feat(ui):=20=E6=89=B9=E9=87=8F=E9=87=8D?=
=?UTF-8?q?=E5=BB=BA=E6=96=87=E6=A1=A3=E5=85=88=E7=BB=8F=E8=A7=A3=E6=9E=90?=
=?UTF-8?q?=E8=AE=BE=E7=BD=AE=E5=BC=B9=E7=AA=97=EF=BC=8C=E6=94=AF=E6=8C=81?=
=?UTF-8?q?=E9=80=90=E6=96=87=E6=A1=A3=E7=8B=AC=E7=AB=8B=E9=85=8D=E7=BD=AE?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
frontend/src/stores/uploadConfirm.ts | 11 ++
.../src/views/knowledge/KnowledgeBase.vue | 73 ++++++--
.../components/UploadConfirmDialog.vue | 162 ++++++++++++++++--
3 files changed, 222 insertions(+), 24 deletions(-)
diff --git a/frontend/src/stores/uploadConfirm.ts b/frontend/src/stores/uploadConfirm.ts
index 3dfc2abd50..8cb89dbfee 100644
--- a/frontend/src/stores/uploadConfirm.ts
+++ b/frontend/src/stores/uploadConfirm.ts
@@ -11,11 +11,22 @@ export interface UploadConfirmManualSource {
tagIds?: string[]
}
+export interface UploadConfirmReparseDoc {
+ knowledgeId: string
+ fileName?: string
+ fileType?: string
+ processOverrides?: KnowledgeProcessOverrides | null
+}
+
export interface UploadConfirmReparseSource {
knowledgeId: string
fileName?: string
fileType?: string
processOverrides?: KnowledgeProcessOverrides | null
+ /** Batch reparse: per-document entries edited independently in the dialog. */
+ docs?: UploadConfirmReparseDoc[]
+ /** Batch reparse confirm output: per-document configs keyed by knowledge ID. */
+ processConfigs?: Record
}
export interface UploadConfirmResult {
diff --git a/frontend/src/views/knowledge/KnowledgeBase.vue b/frontend/src/views/knowledge/KnowledgeBase.vue
index dfa0e41198..87e080632f 100644
--- a/frontend/src/views/knowledge/KnowledgeBase.vue
+++ b/frontend/src/views/knowledge/KnowledgeBase.vue
@@ -32,7 +32,6 @@ import {
reparseKnowledge,
cancelKnowledgeParse,
batchDeleteKnowledge,
- batchReparseKnowledge,
getKnowledgeSpans,
getKnowledgeDetails,
listKnowledgeFolders,
@@ -491,6 +490,11 @@ const awaitBatchReparseReflection = async (ids: string[]) => {
applyOptimisticBatchReparse(Array.from(pendingReparseAck.value));
await new Promise((r) => setTimeout(r, delayMs));
}
+ // Docs that finished between polls were never seen in-flight; refresh once
+ // more so the server's terminal status replaces the optimistic one.
+ if (pendingReparseAck.value.size > 0) {
+ await loadKnowledgeFiles(kbId.value);
+ }
pendingReparseAck.value.clear();
};
@@ -509,18 +513,64 @@ const confirmBatchReparse = async () => {
if (skipped > 0) {
MessagePlugin.warning(t('knowledgeBase.batchReparseSkippedInFlight', { count: skipped }));
}
+
+ if (ids.length === 1) {
+ const idx = cardList.value.findIndex((c) => c.id === ids[0]);
+ const item = cardList.value[idx];
+ if (item && await confirmRebuildKnowledge(idx, item)) {
+ clearSelection();
+ batchMode.value = false;
+ }
+ return;
+ }
+
+ let processConfigs: Record | undefined;
+ if (kbInfo.value) {
+ const docs = ids.map((id) => {
+ const item = cardList.value.find((c) => c.id === id);
+ return {
+ knowledgeId: id,
+ fileName: item?.file_name || item?.title || '',
+ fileType: item?.file_type || '',
+ processOverrides: (item?.metadata?.process_overrides ?? null) as KnowledgeProcessOverrides | null,
+ };
+ });
+ try {
+ const result = await uploadConfirmStore.open({
+ mode: 'reparse',
+ kbInfo: kbInfo.value,
+ reparse: { knowledgeId: '', docs },
+ });
+ processConfigs = result.reparse?.processConfigs;
+ } catch {
+ return;
+ }
+ }
+
batchReparsing.value = true;
try {
- const res: any = await batchReparseKnowledge(kbId.value, ids);
- if (res?.success) {
- MessagePlugin.success(t('knowledgeBase.batchReparseSuccess', { count: ids.length }));
- applyOptimisticBatchReparse(ids);
+ const rebuildIds = processConfigs ? Object.keys(processConfigs) : ids;
+ if (rebuildIds.length === 0) return;
+ const CHUNK_SIZE = 10;
+ const okIds: string[] = [];
+ let failedCount = 0;
+ for (let i = 0; i < rebuildIds.length; i += CHUNK_SIZE) {
+ const chunk = rebuildIds.slice(i, i + CHUNK_SIZE);
+ const results = await Promise.allSettled(chunk.map((id) =>
+ reparseKnowledge(id, processConfigs ? { process_config: processConfigs[id] } : undefined),
+ ));
+ results.forEach((r, j) => (r.status === 'fulfilled' ? okIds.push(chunk[j]) : failedCount++));
+ }
+ if (okIds.length > 0) {
+ MessagePlugin.success(t('knowledgeBase.batchReparseSuccess', { count: okIds.length }));
+ applyOptimisticBatchReparse(okIds);
clearSelection();
batchMode.value = false;
scheduleWikiStatusProbes();
- void awaitBatchReparseReflection(ids);
- } else {
- MessagePlugin.error(res?.message || t('knowledgeBase.batchReparseFailed'));
+ void awaitBatchReparseReflection(okIds);
+ }
+ if (failedCount > 0) {
+ MessagePlugin.error(t('knowledgeBase.batchReparseFailed'));
}
} catch (e: any) {
MessagePlugin.error(e?.message || t('knowledgeBase.batchReparseFailed'));
@@ -1911,8 +1961,7 @@ const confirmRebuildKnowledge = async (index: number, item: KnowledgeCard) => {
// No KB context to seed the dialog defaults — fall back to a direct reparse
// that reuses the overrides stored at upload time.
if (!kbInfo.value) {
- await submitReparse(item.id);
- return;
+ return submitReparse(item.id);
}
// Prefill the confirm dialog with the overrides this doc was last parsed with.
@@ -1937,7 +1986,7 @@ const confirmRebuildKnowledge = async (index: number, item: KnowledgeCard) => {
reparse: { knowledgeId: item.id, fileName, fileType, processOverrides },
});
if (result.mode === 'reparse' && result.reparse) {
- await submitReparse(result.reparse.knowledgeId, result.processConfig);
+ return submitReparse(result.reparse.knowledgeId, result.processConfig);
}
} catch {
// cancelled
@@ -1953,8 +2002,10 @@ const submitReparse = async (id: string, processConfig?: KnowledgeProcessOverrid
resetPage();
loadKnowledgeFiles(kbId.value);
scheduleWikiStatusProbes();
+ return true;
} catch (error: any) {
MessagePlugin.error(error?.message || t('knowledgeBase.rebuildFailed'));
+ return false;
}
};
diff --git a/frontend/src/views/knowledge/components/UploadConfirmDialog.vue b/frontend/src/views/knowledge/components/UploadConfirmDialog.vue
index 266734f13b..2c0f9a933c 100644
--- a/frontend/src/views/knowledge/components/UploadConfirmDialog.vue
+++ b/frontend/src/views/knowledge/components/UploadConfirmDialog.vue
@@ -70,12 +70,41 @@
{{ t('uploadConfirm.manualCharCount', { count: manualCharCount }) }}
-
+
{{ reparsePreview.fileName || t('uploadConfirm.reparseSource') }}
{{ t('uploadConfirm.reparseHint') }}
+
-
@@ -779,6 +808,32 @@ function inferMediaExtsFromMarkdown(content: string): string[] {
const manualCharCount = computed(() => props.manualPreview?.content?.length ?? 0)
const batchItemCount = computed(() => localFiles.value.length + localUrls.value.length)
+const isBatchReparse = computed(() => props.mode === 'reparse' && (props.reparsePreview?.docs?.length || 0) > 0)
+
+const reparseDocs = ref>([])
+const docStates = new Map()
+const activeDocId = ref('')
+
+const activeDoc = computed(() => reparseDocs.value.find((doc) => doc.knowledgeId === activeDocId.value))
+
+function selectReparseDoc(id: string) {
+ const state = docStates.get(id)
+ if (!state) return
+ activeDocId.value = id
+ uiState.value = state
+}
+
+function removeReparseDoc(id: string) {
+ reparseDocs.value = reparseDocs.value.filter((doc) => doc.knowledgeId !== id)
+ docStates.delete(id)
+ if (activeDocId.value === id) {
+ activeDocId.value = reparseDocs.value[0]?.knowledgeId || ''
+ if (activeDocId.value) {
+ uiState.value = docStates.get(activeDocId.value)!
+ }
+ }
+}
+
const dialogTitle = computed(() => {
if (props.mode === 'manual') return t('uploadConfirm.titleManual')
if (props.mode === 'reparse') return t('uploadConfirm.titleReparse')
@@ -799,7 +854,8 @@ const batchFileExts = computed(() => {
}
}
if (props.mode === 'reparse') {
- const ext = (props.reparsePreview?.fileType || '').toLowerCase()
+ const source = isBatchReparse.value ? activeDoc.value : props.reparsePreview
+ const ext = (source?.fileType || '').toLowerCase()
if (ext) set.add(ext)
}
for (const url of localUrls.value) {
@@ -1085,13 +1141,12 @@ function createDefaultUIState(): UploadUIState {
}
}
-function initFromKbInfo(kb: any) {
+function buildKbState(kb: any): UploadUIState {
if (!kb) {
- uiState.value = createDefaultUIState()
- return
+ return createDefaultUIState()
}
- uiState.value = {
+ return {
chunkingConfig: {
chunkSize: kb.chunking_config?.chunk_size || 512,
chunkOverlap: kb.chunking_config?.chunk_overlap || 80,
@@ -1137,8 +1192,7 @@ function initFromKbInfo(kb: any) {
}
}
-function buildProcessOverrides(): KnowledgeProcessOverrides {
- const state = uiState.value
+function buildProcessOverrides(state: UploadUIState = uiState.value): KnowledgeProcessOverrides {
const chunking = state.chunkingConfig
const overrides: KnowledgeProcessOverrides = {
@@ -1192,9 +1246,9 @@ function buildProcessOverrides(): KnowledgeProcessOverrides {
return overrides
}
-function applyOverridesToState(o?: KnowledgeProcessOverrides | null) {
+function applyOverridesToState(o?: KnowledgeProcessOverrides | null, state: UploadUIState = uiState.value) {
if (!o) return
- const s = uiState.value
+ const s = state
const cc = o.chunking_config
if (cc) {
if (cc.chunk_size != null) s.chunkingConfig.chunkSize = cc.chunk_size
@@ -1294,9 +1348,27 @@ watch(
localTargetFolder.value = props.mode === 'file' ? (props.targetFolder || '') : ''
pendingFolderPaths.value = []
destinationPickerVisible.value = false
- initFromKbInfo(props.kbInfo)
- if (props.mode === 'reparse') {
- applyOverridesToState(props.reparsePreview?.processOverrides)
+ reparseDocs.value = []
+ docStates.clear()
+ activeDocId.value = ''
+ if (isBatchReparse.value) {
+ for (const doc of props.reparsePreview?.docs || []) {
+ const state = buildKbState(props.kbInfo)
+ applyOverridesToState(doc.processOverrides, state)
+ docStates.set(doc.knowledgeId, state)
+ reparseDocs.value.push({
+ knowledgeId: doc.knowledgeId,
+ fileName: doc.fileName || '',
+ fileType: doc.fileType || '',
+ })
+ }
+ activeDocId.value = reparseDocs.value[0]?.knowledgeId || ''
+ uiState.value = (activeDocId.value && docStates.get(activeDocId.value)) || buildKbState(props.kbInfo)
+ } else {
+ uiState.value = buildKbState(props.kbInfo)
+ if (props.mode === 'reparse') {
+ applyOverridesToState(props.reparsePreview?.processOverrides)
+ }
}
activeSection.value = getDefaultSection()
chunkingMoreOpen.value = false
@@ -1378,6 +1450,42 @@ const handleNodeExtractUpdate = (config: UploadUIState['nodeExtractConfig']) =>
}
const validateBeforeConfirm = (): boolean => {
+ if (isBatchReparse.value) {
+ for (const doc of reparseDocs.value) {
+ const state = docStates.get(doc.knowledgeId)
+ if (!state) continue
+ const ext = (doc.fileType || '').toLowerCase()
+ const isImage = IMAGE_EXTENSIONS.includes(ext)
+ const isAudio = AUDIO_EXTENSIONS.includes(ext)
+ if (isImage && (!state.multimodalConfig.enabled || !state.multimodalConfig.vllmModelId)) {
+ selectReparseDoc(doc.knowledgeId)
+ MessagePlugin.warning(t('uploadConfirm.vlmModelRequired'))
+ uiState.value.multimodalConfig.enabled = true
+ goToSection('multimodal')
+ return false
+ }
+ if (!isImage && state.multimodalConfig.enabled && !state.multimodalConfig.vllmModelId) {
+ selectReparseDoc(doc.knowledgeId)
+ MessagePlugin.warning(t('uploadConfirm.vlmModelSelectRequired'))
+ goToSection('multimodal')
+ return false
+ }
+ if (isAudio && (!state.asrConfig.enabled || !state.asrConfig.modelId)) {
+ selectReparseDoc(doc.knowledgeId)
+ MessagePlugin.warning(t('uploadConfirm.asrModelRequired'))
+ uiState.value.asrConfig.enabled = true
+ goToSection('asr')
+ return false
+ }
+ if (!isAudio && state.asrConfig.enabled && !state.asrConfig.modelId) {
+ selectReparseDoc(doc.knowledgeId)
+ MessagePlugin.warning(t('uploadConfirm.asrModelSelectRequired'))
+ goToSection('asr')
+ return false
+ }
+ }
+ return true
+ }
if (hasImages.value) {
if (!uiState.value.multimodalConfig.enabled || !uiState.value.multimodalConfig.vllmModelId) {
MessagePlugin.warning(t('uploadConfirm.vlmModelRequired'))
@@ -1426,6 +1534,21 @@ const handleConfirm = () => {
tagIds: [...selectedTagIds.value],
manual: { ...props.manualPreview, tagIds: [...selectedTagIds.value] },
})
+ } else if (isBatchReparse.value) {
+ if (reparseDocs.value.length === 0) {
+ MessagePlugin.warning(t('uploadConfirm.noItems'))
+ return
+ }
+ const processConfigs: Record = {}
+ for (const doc of reparseDocs.value) {
+ const state = docStates.get(doc.knowledgeId)
+ if (state) processConfigs[doc.knowledgeId] = buildProcessOverrides(state)
+ }
+ emit('confirm', {
+ processConfig,
+ mode: 'reparse',
+ reparse: { ...props.reparsePreview, knowledgeId: '', processConfigs },
+ })
} else if (props.mode === 'reparse' && props.reparsePreview) {
emit('confirm', { processConfig, mode: 'reparse', reparse: { ...props.reparsePreview } })
} else {
@@ -1672,6 +1795,19 @@ const handleConfirm = () => {
color: var(--td-brand-color);
}
+.file-item--doc {
+ cursor: pointer;
+}
+
+.file-item--doc.file-item--active,
+.file-item--doc.file-item--active:hover {
+ background: var(--td-brand-color-light);
+
+ .file-name {
+ color: var(--td-brand-color);
+ }
+}
+
.file-meta {
flex: 1;
min-width: 0;