Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 53 additions & 6 deletions frontend/src/views/knowledge/KnowledgeBase.vue
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import {
batchReparseKnowledge,
getKnowledgeSpans,
getKnowledgeDetails,
listKnowledgeFiles,
listKnowledgeFolders,
moveKnowledgeToFolder,
renameKnowledgeFolder,
Expand Down Expand Up @@ -1999,15 +2000,57 @@ const onCardGridCheckboxChange = (id: string, checked: boolean, ctx?: { e?: Even
toggleSelectRow(id, checked, !!me?.shiftKey);
};

const toggleSelectAll = (checked: boolean) => {
if (checked) {
for (const item of cardList.value || []) selectedIds.value.add(item.id);
} else {
for (const item of cardList.value || []) selectedIds.value.delete(item.id);
// Select all across pages: the list is loaded incrementally via infinite
// scroll, so cardList only holds the loaded window. When the header checkbox
// is checked, fetch every document id matching the current filters from the
// backend page by page; otherwise only the loaded slice would be selected.
// The backend caps page_size at 1000.
const SELECT_ALL_PAGE_SIZE = 1000;
const selectAllAcrossPages = ref(false);
const selectAllLoading = ref(false);
let selectAllGeneration = 0;

const toggleSelectAll = async (checked: boolean) => {
if (!checked) {
clearSelection();
return;
}
for (const item of cardList.value || []) selectedIds.value.add(item.id);
selectAllAcrossPages.value = true;
if (cardList.value.length >= total.value) return;
const currentKbId = kbId.value;
if (!currentKbId) return;
const generation = ++selectAllGeneration;
selectAllLoading.value = true;
try {
let pageNum = 1;
while (true) {
const res: any = await listKnowledgeFiles(currentKbId, {
page: pageNum,
page_size: SELECT_ALL_PAGE_SIZE,
...filterParams.value,
});
// The user cancelled select-all or switched filters/kb while fetching:
// discard this result.
if (generation !== selectAllGeneration || !isCurrentKb(currentKbId)) return;
const items = res?.data || [];
const totalAll = res?.total || 0;
for (const it of items) {
if (it?.id) selectedIds.value.add(it.id);
}
if (items.length === 0 || pageNum * SELECT_ALL_PAGE_SIZE >= totalAll) break;
pageNum++;
}
} catch {
// Keep the partially selected ids if the fetch fails midway.
} finally {
if (generation === selectAllGeneration) selectAllLoading.value = false;
}
};

const clearSelection = () => {
selectAllGeneration++;
selectAllAcrossPages.value = false;
selectedIds.value.clear();
lastSelectedIndex = -1;
};
Expand Down Expand Up @@ -2197,6 +2240,9 @@ watch(cardList, () => {
moreIndex.value = -1;
}
if (selectedIds.value.size === 0) return;
// With select-all across pages, selectedIds intentionally contains ids
// outside the loaded window; don't prune them against the visible list.
if (selectAllAcrossPages.value) return;
const visible = new Set(items.map((i: KnowledgeCard) => i.id));
for (const id of selectedIds.value) {
if (!visible.has(id)) selectedIds.value.delete(id);
Expand Down Expand Up @@ -2629,7 +2675,8 @@ async function createNewSession(value: string): Promise<void> {
</div>
<div class="doc-batch-bar-anchor" v-show="batchMode || selectedIds.size > 0">
<DocumentBatchBar :count="selectedIds.size" :delete-loading="batchDeleting"
:reparse-loading="batchReparsing" :tag-loading="batchTagging" :visible="batchMode || selectedIds.size > 0"
:reparse-loading="batchReparsing" :tag-loading="batchTagging" :select-all-loading="selectAllLoading"
:visible="batchMode || selectedIds.size > 0"
:show-move-to-folder="canEdit" :folder-options="folderOptions"
@cancel="handleBatchCancel" @delete="confirmBatchDelete" @reparse="confirmBatchReparse"
@batch-tag="handleBatchTag"
Expand Down
18 changes: 14 additions & 4 deletions frontend/src/views/knowledge/components/DocumentBatchBar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ defineProps<{
deleteLoading?: boolean;
reparseLoading?: boolean;
tagLoading?: boolean;
// Select-all across pages is still fetching every matched id: disable the
// action buttons so operations don't run on an incomplete selection.
selectAllLoading?: boolean;
// When true the bar stays visible even with 0 selections, so users can exit
// batch mode from here without selecting anything first.
visible?: boolean;
Expand Down Expand Up @@ -35,6 +38,7 @@ const folderPickerVisible = ref(false);
:aria-label="t('knowledgeBase.selectedCount', { count })">
<div class="batch-bar-inner">
<div class="batch-bar-left">
<t-loading v-if="selectAllLoading" size="12px" class="batch-bar-loading" />
<span class="batch-bar-count">{{ t('knowledgeBase.selectedCount', { count }) }}</span>
<t-button variant="text" theme="default" size="small" class="batch-bar-clear" @click="emit('cancel')">
{{ t('knowledgeBase.clearSelection') }}
Expand All @@ -45,14 +49,14 @@ const folderPickerVisible = ref(false);
:confirm-btn="{ content: t('knowledgeBase.confirmBatchReparse'), theme: 'warning' }"
:cancel-btn="{ content: t('common.cancel') }" placement="top" @confirm="emit('reparse')">
<t-button theme="default" variant="outline" size="small"
:disabled="count === 0 || deleteLoading || reparseLoading || tagLoading" :loading="reparseLoading" @click.stop>
:disabled="count === 0 || deleteLoading || reparseLoading || tagLoading || selectAllLoading" :loading="reparseLoading" @click.stop>
<template #icon><t-icon name="refresh" size="14px" /></template>
{{ t('knowledgeBase.rebuildDocument') }}
</t-button>
</t-popconfirm>

<t-button theme="default" variant="outline" size="small"
:disabled="count === 0 || deleteLoading || reparseLoading || tagLoading" :loading="tagLoading"
:disabled="count === 0 || deleteLoading || reparseLoading || tagLoading || selectAllLoading" :loading="tagLoading"
@click="emit('batchTag')">
<template #icon><t-icon name="discount" size="14px" /></template>
{{ t('knowledgeBase.batchTag') }}
Expand All @@ -61,7 +65,7 @@ const folderPickerVisible = ref(false);
<t-popup v-if="showMoveToFolder" v-model:visible="folderPickerVisible" trigger="click"
placement="top" overlay-class-name="card-more" destroy-on-close>
<t-button theme="default" variant="outline" size="small"
:disabled="count === 0 || deleteLoading || reparseLoading || tagLoading">
:disabled="count === 0 || deleteLoading || reparseLoading || tagLoading || selectAllLoading">
<template #icon><t-icon name="folder" size="14px" /></template>
{{ t('knowledgeBase.moveToFolder.action') }}
</t-button>
Expand All @@ -77,7 +81,7 @@ const folderPickerVisible = ref(false);
:confirm-btn="{ content: t('knowledgeBase.confirmDelete'), theme: 'danger' }"
:cancel-btn="{ content: t('common.cancel') }" placement="top" @confirm="emit('delete')">
<t-button theme="danger" variant="outline" size="small"
:disabled="count === 0 || deleteLoading || reparseLoading || tagLoading" :loading="deleteLoading" @click.stop>
:disabled="count === 0 || deleteLoading || reparseLoading || tagLoading || selectAllLoading" :loading="deleteLoading" @click.stop>
<template #icon><t-icon name="delete" size="14px" /></template>
{{ t('knowledgeBase.batchDelete') }}
</t-button>
Expand Down Expand Up @@ -126,6 +130,12 @@ const folderPickerVisible = ref(false);
white-space: nowrap;
}

.batch-bar-loading {
flex-shrink: 0;
display: inline-flex;
align-items: center;
}

.batch-bar-clear {
flex-shrink: 0;
padding: 0 6px !important;
Expand Down