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') }}

+
    +
  • + + + +
    + + {{ doc.fileName || t('uploadConfirm.reparseSource') }} + + {{ doc.fileType }} +
    + +
  • +
  • @@ -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;