diff --git a/frontend/app/[locale]/knowledges/KnowledgeBaseConfiguration.tsx b/frontend/app/[locale]/knowledges/KnowledgeBaseConfiguration.tsx index 6618bc38ed..7217de7cc9 100644 --- a/frontend/app/[locale]/knowledges/KnowledgeBaseConfiguration.tsx +++ b/frontend/app/[locale]/knowledges/KnowledgeBaseConfiguration.tsx @@ -1,6 +1,7 @@ "use client"; import type React from "react"; +import type { TFunction } from "i18next"; import { useState, useEffect, @@ -11,11 +12,11 @@ import { } from "react"; import { useTranslation } from "react-i18next"; -import { App, Modal, Row, Col, theme, Button, Input } from "antd"; +import { App, Modal, theme, Button, Input } from "antd"; import { + ArrowLeftOutlined, ExclamationCircleFilled, WarningFilled, - InfoCircleFilled, } from "@ant-design/icons"; import { DOCUMENT_ACTION_TYPES, @@ -32,11 +33,11 @@ import { isKnowledgeBaseFileSizeValid } from "@/services/uploadService"; import { KnowledgeBase } from "@/types/knowledgeBase"; import { useConfig } from "@/hooks/useConfig"; import { useModelList } from "@/hooks/model/useModelList"; -import { - SETUP_PAGE_CONTAINER, - TWO_COLUMN_LAYOUT, - STANDARD_CARD, -} from "@/const/layoutConstants"; +import { useAuthorizationContext } from "@/components/providers/AuthorizationProvider"; +import { SETUP_PAGE_CONTAINER, STANDARD_CARD } from "@/const/layoutConstants"; +import { QUOTA_USAGE_CHANGED_EVENT } from "@/lib/quotaEvents"; +import quotaService from "@/services/quotaService"; +import type { KBQuotaStatus, QuotaUsageResponse } from "@/types/quota"; import KnowledgeBaseList from "./components/knowledge/KnowledgeBaseList"; import DocumentList from "./components/document/DocumentList"; @@ -60,6 +61,39 @@ const isApiErrorCode = (error: unknown, code: string | number): boolean => "code" in error && String((error as { code?: unknown }).code) === String(code); +const getKnowledgeBaseCreateErrorMessage = ( + error: unknown, + name: string, + t: TFunction +) => { + if (isApiErrorCode(error, 409)) { + return t("knowledgeBase.message.nameExists", { name }); + } + if (isApiErrorCode(error, ErrorCode.TENANT_PERSONAL_KB_QUOTA_EXCEEDED)) { + return t("quota.personalKbUploadBlocked"); + } + if (isApiErrorCode(error, ErrorCode.TENANT_PERSONAL_KB_QUOTA_UNAVAILABLE)) { + return t(`errorCode.${ErrorCode.TENANT_PERSONAL_KB_QUOTA_UNAVAILABLE}`); + } + if (isApiErrorCode(error, 413)) { + return t("quota.uploadBlocked"); + } + return t("knowledgeBase.message.createUploadError"); +}; + +const getKnowledgeBaseUploadErrorMessage = (error: unknown, t: TFunction) => { + if (isApiErrorCode(error, ErrorCode.TENANT_PERSONAL_KB_QUOTA_EXCEEDED)) { + return t("quota.personalKbUploadBlocked"); + } + if (isApiErrorCode(error, ErrorCode.TENANT_PERSONAL_KB_QUOTA_UNAVAILABLE)) { + return t(`errorCode.${ErrorCode.TENANT_PERSONAL_KB_QUOTA_UNAVAILABLE}`); + } + if (isApiErrorCode(error, 413)) { + return t("quota.uploadBlocked"); + } + return t("document.message.uploadError"); +}; + const toEmbeddingModelOptionValue = (displayName: string, type: string) => `${displayName}${EMBEDDING_MODEL_OPTION_DELIMITER}${type}`; @@ -87,43 +121,6 @@ const parseEmbeddingModelOptionValue = (value: string) => { }; }; -// EmptyState component defined directly in this file -interface EmptyStateProps { - icon?: React.ReactNode | string; - title: string; - description?: string; - action?: React.ReactNode; - containerHeight?: string; -} - -const EmptyState: React.FC = ({ - icon = "馃搵", - title, - description, - action, - containerHeight = "100%", -}) => { - return ( -
-
- {typeof icon === "string" ? ( -
{icon}
- ) : ( -
{icon}
- )} -

{title}

- {description && ( -

{description}

- )} - {action &&
{action}
} -
-
- ); -}; - // Combined AppProvider implementation interface AppProviderProps { children: React.ReactNode; @@ -176,6 +173,43 @@ function DataConfig({ isActive }: DataConfigProps) { saveConfig, } = useConfig(); const { token } = theme.useToken(); + const { user } = useAuthorizationContext(); + + const [quotaUsage, setQuotaUsage] = useState(null); + + const loadQuotaUsage = useCallback( + async (forceRefresh = false) => { + if (!user?.tenantId) { + setQuotaUsage(null); + return; + } + + try { + const usage = await quotaService.getQuotaUsage( + user.tenantId, + forceRefresh, + true + ); + setQuotaUsage(usage); + } catch (error) { + log.warn("Failed to load knowledge base quota usage:", error); + } + }, + [user?.tenantId] + ); + + useEffect(() => { + void loadQuotaUsage(); + const handleQuotaUsageChanged = () => { + void loadQuotaUsage(true); + }; + window.addEventListener(QUOTA_USAGE_CHANGED_EVENT, handleQuotaUsageChanged); + return () => + window.removeEventListener( + QUOTA_USAGE_CHANGED_EVENT, + handleQuotaUsageChanged + ); + }, [loadQuotaUsage]); // Get available embedding models for knowledge base creation const { models } = useModelList({ enabled: true }); @@ -216,7 +250,6 @@ function DataConfig({ isActive }: DataConfigProps) { deleteKnowledgeBase, setActiveKnowledgeBase, updateKnowledgeBase, - hasKnowledgeBaseModelMismatch, refreshKnowledgeBaseData, refreshKnowledgeBaseDataWithDataMate, dispatch: kbDispatch, @@ -238,6 +271,7 @@ function DataConfig({ isActive }: DataConfigProps) { // Create mode state const [isCreatingMode, setIsCreatingMode] = useState(false); const [newKbName, setNewKbName] = useState(""); + const [newKbDescription, setNewKbDescription] = useState(""); const [newKbIngroupPermission, setNewKbIngroupPermission] = useState("READ_ONLY"); const [newKbGroupIds, setNewKbGroupIds] = useState([]); @@ -354,26 +388,6 @@ function DataConfig({ isActive }: DataConfigProps) { }); }, [models]); - const hasKnowledgeBaseDetailModelMismatch = useCallback( - (knowledgeBase: KnowledgeBase) => { - if (hasKnowledgeBaseModelMismatch(knowledgeBase)) { - return true; - } - if ( - knowledgeBase.embeddingModel === "unknown" || - knowledgeBase.source === "datamate" - ) { - return false; - } - - const knowledgeBaseModel = knowledgeBase.embeddingModel.trim(); - return !availableEmbeddingModels.some( - (model) => model.displayName.trim() === knowledgeBaseModel - ); - }, - [availableEmbeddingModels, hasKnowledgeBaseModelMismatch] - ); - const resolveEmbeddingModelId = useCallback( ({ displayName, @@ -441,66 +455,13 @@ function DataConfig({ isActive }: DataConfigProps) { ]); // User configuration loading and saving logic based on isActive state - const prevIsActiveRef = useRef(null); // Initialize as null to distinguish first render - const hasLoadedRef = useRef(false); // Track whether configuration has been loaded - const hasCleanedRef = useRef(false); // Ensure auto-deselect runs only once per entry - // Listen for isActive state changes useLayoutEffect(() => { // Clear cache that might affect state localStorage.removeItem("preloaded_kb_data"); localStorage.removeItem("kb_cache"); - - const prevIsActive = prevIsActiveRef.current; - - // Mark ready to load when entering second page - if ((prevIsActive === null || !prevIsActive) && isActive) { - hasLoadedRef.current = false; // Reset loading state - hasCleanedRef.current = false; // Reset auto-clean flag on entering - } - - // Update ref - prevIsActiveRef.current = isActive; }, [isActive]); - // Separately listen for knowledge base loading state, load user configuration when knowledge base loading is complete and in active state - useEffect(() => { - // Only execute when second page is active, knowledge base is loaded, and user configuration hasn't been loaded yet - if ( - isActive && - kbState.knowledgeBases.length > 0 && - !kbState.isLoading && - !hasLoadedRef.current - ) { - hasLoadedRef.current = true; - } - }, [isActive, kbState.knowledgeBases.length, kbState.isLoading]); - - // Auto-deselect incompatible knowledge bases once after selections are loaded and page is active - useEffect(() => { - if (!isActive) return; - if (!hasLoadedRef.current) return; // ensure user selections loaded - if (kbState.isLoading) return; // avoid running during list loading - if (hasCleanedRef.current) return; // run once per entry - - const embeddingName = modelConfig?.embedding?.displayName?.trim() || ""; - const multiEmbeddingName = - modelConfig?.multiEmbedding?.displayName?.trim() || ""; - - const allowedModels = new Set(); - if (embeddingName) allowedModels.add(embeddingName); - if (multiEmbeddingName) allowedModels.add(multiEmbeddingName); - - hasCleanedRef.current = true; - }, [ - isActive, - kbState.isLoading, - kbState.knowledgeBases, - modelConfig?.embedding?.displayName, - modelConfig?.multiEmbedding?.displayName, - kbDispatch, - ]); - // Generate the default name from knowledge bases visible to the current user. const generateVisibleKbName = (existingKbs: KnowledgeBase[]): string => { const baseNamePrefix = t("knowledgeBase.name.new"); @@ -620,7 +581,9 @@ function DataConfig({ isActive }: DataConfigProps) { } if (validFiles.length > 0) { setUploadFiles(validFiles); - handleFileUpload(); + if (!isCreatingMode) { + void handleFileUpload(validFiles); + } } } else { message.warning(t("knowledgeBase.message.selectFirst")); @@ -831,6 +794,7 @@ function DataConfig({ isActive }: DataConfigProps) { // Generate the default name without probing hidden tenant knowledge bases. const defaultName = generateVisibleKbName(kbState.knowledgeBases); setNewKbName(defaultName); + setNewKbDescription(""); setNewKbIngroupPermission("READ_ONLY"); setNewKbGroupIds([]); setNewKbPreserveSourceFile(true); @@ -895,7 +859,7 @@ function DataConfig({ isActive }: DataConfigProps) { }); }; - // Handle file upload - in creation mode create knowledge base first then upload, in normal mode upload directly + // Create the knowledge base from the modal, then upload the selected files if any. const handleFileUpload = async (selectedFiles: File[] = uploadFiles) => { if ( !isCreatingMode && @@ -904,7 +868,7 @@ function DataConfig({ isActive }: DataConfigProps) { message.error(t("errorCode.000202", "Access forbidden.")); return; } - if (!selectedFiles.length) { + if (!isCreatingMode && !selectedFiles.length) { message.warning(t("document.message.noFiles")); return; } @@ -915,6 +879,14 @@ function DataConfig({ isActive }: DataConfigProps) { message.warning(t("knowledgeBase.message.nameRequired")); return; } + if (!newKbEmbeddingModel) { + message.warning(t("knowledgeBase.message.embeddingModelRequired")); + return; + } + if (!selectedFiles.length) { + message.warning(t("knowledgeBase.message.noFiles")); + return; + } setHasClickedUpload(true); @@ -944,7 +916,7 @@ function DataConfig({ isActive }: DataConfigProps) { const newKB = await createKnowledgeBase( newKbName.trim(), - t("knowledgeBase.description.default"), + newKbDescription.trim() || t("knowledgeBase.description.default"), "elasticsearch", newKbIngroupPermission, newKbGroupIds, @@ -957,7 +929,13 @@ function DataConfig({ isActive }: DataConfigProps) { setActiveKnowledgeBase(newKB); knowledgeBasePollingService.setActiveKnowledgeBase(newKB.id); setHasClickedUpload(false); - setNewlyCreatedKbId(newKB.id); // Mark this KB as newly created + setNewlyCreatedKbId(filesToUpload.length > 0 ? newKB.id : null); // Mark this KB as newly created when files need processing + + if (!filesToUpload.length) { + setUploadFiles([]); + knowledgeBasePollingService.triggerKnowledgeBaseListUpdate(true); + return; + } await uploadDocuments(newKB.id, filesToUpload); setUploadFiles([]); @@ -982,23 +960,7 @@ function DataConfig({ isActive }: DataConfigProps) { }); } catch (error) { message.error( - isApiErrorCode(error, 409) - ? t("knowledgeBase.message.nameExists", { - name: newKbName.trim(), - }) - : isApiErrorCode( - error, - ErrorCode.TENANT_PERSONAL_KB_QUOTA_EXCEEDED - ) - ? t("quota.personalKbUploadBlocked") - : isApiErrorCode( - error, - ErrorCode.TENANT_PERSONAL_KB_QUOTA_UNAVAILABLE - ) - ? t(`errorCode.${ErrorCode.TENANT_PERSONAL_KB_QUOTA_UNAVAILABLE}`) - : isApiErrorCode(error, 413) - ? t("quota.uploadBlocked") - : t("knowledgeBase.message.createUploadError") + getKnowledgeBaseCreateErrorMessage(error, newKbName.trim(), t) ); setHasClickedUpload(false); // Clear the waiting flag so a failed upload cannot leave the page @@ -1040,18 +1002,7 @@ function DataConfig({ isActive }: DataConfigProps) { } ); } catch (error) { - message.error( - isApiErrorCode(error, ErrorCode.TENANT_PERSONAL_KB_QUOTA_EXCEEDED) - ? t("quota.personalKbUploadBlocked") - : isApiErrorCode( - error, - ErrorCode.TENANT_PERSONAL_KB_QUOTA_UNAVAILABLE - ) - ? t(`errorCode.${ErrorCode.TENANT_PERSONAL_KB_QUOTA_UNAVAILABLE}`) - : isApiErrorCode(error, 413) - ? t("quota.uploadBlocked") - : t("document.message.uploadError") - ); + message.error(getKnowledgeBaseUploadErrorMessage(error, t)); throw error; } }; @@ -1080,6 +1031,16 @@ function DataConfig({ isActive }: DataConfigProps) { const viewingKbName = kbState.activeKnowledgeBase?.name || (isCreatingMode ? newKbName : ""); + const activeKnowledgeBaseQuota: KBQuotaStatus | undefined = useMemo(() => { + const activeKnowledgeBase = kbState.activeKnowledgeBase; + if (!activeKnowledgeBase) return undefined; + + const indexName = activeKnowledgeBase.index_name || activeKnowledgeBase.id; + return quotaUsage?.breakdown?.find( + (quota) => quota.index_name === indexName + ); + }, [kbState.activeKnowledgeBase, quotaUsage]); + // Check if current knowledge base is newly created and waiting for documents const isNewlyCreatedAndWaiting = newlyCreatedKbId !== null && @@ -1124,6 +1085,12 @@ function DataConfig({ isActive }: DataConfigProps) { setNewKbName(name); }; + const handleCloseCreateModal = () => { + setIsCreatingMode(false); + setHasClickedUpload(false); + setUploadFiles([]); + }; + // If Embedding model is not configured, show warning container instead of content if (showEmbeddingWarning) { return ( @@ -1172,15 +1139,8 @@ function DataConfig({ isActive }: DataConfigProps) { onDrop={handleDrop} >
- - + {!kbState.activeKnowledgeBase ? ( + <> - - - - {isCreatingMode ? ( - {}} - knowledgeBaseSource={""} - isCreatingMode={true} - knowledgeBaseId={""} - knowledgeBaseName={newKbName} - onNameChange={handleNameChange} - containerHeight={SETUP_PAGE_CONTAINER.MAIN_CONTENT_HEIGHT} - hasDocuments={hasClickedUpload || docState.isUploading} - // Group permission and user groups for create mode - ingroupPermission={newKbIngroupPermission} - onIngroupPermissionChange={setNewKbIngroupPermission} - selectedGroupIds={newKbGroupIds} - onSelectedGroupIdsChange={setNewKbGroupIds} - preserveSourceFile={newKbPreserveSourceFile} - onPreserveSourceFileChange={setNewKbPreserveSourceFile} - quotaLimitBytes={newKbQuotaBytes} - onQuotaLimitBytesChange={setNewKbQuotaBytes} - // Embedding model for create mode - availableEmbeddingModels={availableEmbeddingModels} - selectedEmbeddingModel={newKbEmbeddingModel} - onEmbeddingModelChange={setNewKbEmbeddingModel} - // Upload related props - isDragging={uiState.isDragging} - onDragOver={handleDragOver} - onDragLeave={handleDragLeave} - onDrop={handleDrop} - onFileSelect={handleFileSelect} - onUpload={handleFileUpload} - isUploading={docState.isUploading} - /> - ) : kbState.activeKnowledgeBase ? ( + + + +
+ } + width={640} + centered + maskClosable={false} + destroyOnHidden + onCancel={handleCloseCreateModal} + styles={{ + container: { padding: 0 }, + body: { padding: 0 }, + footer: { + margin: 0, + padding: "12px 20px 16px", + borderTop: "1px solid #f0f0f0", + }, + }} + getContainer={() => contentRef.current || document.body} + > +
+ {}} + knowledgeBaseSource="" + isCreatingMode={true} + knowledgeBaseId="" + knowledgeBaseName={newKbName} + onNameChange={handleNameChange} + knowledgeBaseDescription={newKbDescription} + onDescriptionChange={setNewKbDescription} + selectedFiles={uploadFiles} + containerHeight={SETUP_PAGE_CONTAINER.MAIN_CONTENT_HEIGHT} + hasDocuments={hasClickedUpload || docState.isUploading} + // Group permission and user groups for create mode + ingroupPermission={newKbIngroupPermission} + onIngroupPermissionChange={setNewKbIngroupPermission} + selectedGroupIds={newKbGroupIds} + onSelectedGroupIdsChange={setNewKbGroupIds} + preserveSourceFile={newKbPreserveSourceFile} + onPreserveSourceFileChange={setNewKbPreserveSourceFile} + quotaLimitBytes={newKbQuotaBytes} + onQuotaLimitBytesChange={setNewKbQuotaBytes} + // Embedding model for create mode + availableEmbeddingModels={availableEmbeddingModels} + selectedEmbeddingModel={newKbEmbeddingModel} + onEmbeddingModelChange={setNewKbEmbeddingModel} + // Upload related props + isDragging={uiState.isDragging} + onDragOver={handleDragOver} + onDragLeave={handleDragLeave} + onDrop={handleDrop} + onFileSelect={handleFileSelect} + onUpload={handleFileUpload} + isUploading={docState.isUploading} + /> +
+ + + ) : ( +
+
+ +
+
0} isNewlyCreatedAndWaiting={isNewlyCreatedAndWaiting} @@ -1311,6 +1314,15 @@ function DataConfig({ isActive }: DataConfigProps) { true ); }} + onRefresh={() => { + if (kbState.activeKnowledgeBase) { + void fetchDocuments( + kbState.activeKnowledgeBase.id, + true, + kbState.activeKnowledgeBase.source + ); + } + }} permission={kbState.activeKnowledgeBase?.permission} summaryFrequency={ kbState.activeKnowledgeBase?.summaryFrequency @@ -1347,27 +1359,9 @@ function DataConfig({ isActive }: DataConfigProps) { onUpload={handleFileUpload} isUploading={docState.isUploading} /> - ) : ( -
- - } - containerHeight="100%" - /> -
- )} - - +
+
+ )} diff --git a/frontend/app/[locale]/knowledges/components/document/DocumentChunk.tsx b/frontend/app/[locale]/knowledges/components/document/DocumentChunk.tsx index e31f775222..0e8bd6db62 100644 --- a/frontend/app/[locale]/knowledges/components/document/DocumentChunk.tsx +++ b/frontend/app/[locale]/knowledges/components/document/DocumentChunk.tsx @@ -62,8 +62,6 @@ interface DocumentChunkProps { knowledgeBaseId: string; // Internal knowledge base ID / Elasticsearch index name documents: Document[]; getFileIcon: (type: string) => string; - currentEmbeddingModel?: string | null; - knowledgeBaseEmbeddingModel?: string; onChunkCountChange?: () => void; // Callback when chunk count changes (for updating KnowledgeBaseList) permission?: string; // User's permission for this knowledge base (READ_ONLY, EDIT, etc.) } @@ -84,8 +82,6 @@ const DocumentChunk: React.FC = ({ knowledgeBaseId, documents, getFileIcon, - currentEmbeddingModel, - knowledgeBaseEmbeddingModel, onChunkCountChange, permission, }) => { @@ -140,33 +136,10 @@ const DocumentChunk: React.FC = ({ const effectiveIndexName = knowledgeBaseId || knowledgeBaseName; - const hasKnowledgeBaseModel = - Boolean(knowledgeBaseEmbeddingModel) && - knowledgeBaseEmbeddingModel !== "unknown"; - const hasCurrentModel = Boolean(currentEmbeddingModel); - - // Determine if embedding models mismatch (specific condition for tooltip) - const isEmbeddingModelMismatch = React.useMemo(() => { - if (!hasKnowledgeBaseModel) { - return false; - } - return ( - !hasCurrentModel || currentEmbeddingModel !== knowledgeBaseEmbeddingModel - ); - }, [ - currentEmbeddingModel, - hasCurrentModel, - hasKnowledgeBaseModel, - knowledgeBaseEmbeddingModel, - ]); - - // Determine if in read-only mode (embedding model mismatch OR user has READ_ONLY permission) - // Note: isReadOnlyMode is broader, includes model mismatch and other conditions - const isReadOnlyMode = React.useMemo(() => { - return permission === "READ_ONLY" || isEmbeddingModelMismatch; - }, [permission, isEmbeddingModelMismatch]); - - const isSearchDisabled = isEmbeddingModelMismatch; + // The knowledge base owns its embedding model, so the global model must not + // disable chunk search or editing in this knowledge base. + const isReadOnlyMode = permission === "READ_ONLY"; + const isSearchDisabled = false; useEffect(() => { if (isSearchDisabled) { @@ -644,11 +617,12 @@ const DocumentChunk: React.FC = ({ }, {}); }, [chunkSearchResult]); - const visibleDocuments = isChunkSearchActive && !chunkSearchLoading - ? documents.filter( - (doc) => (chunkSearchResultMap?.[doc.id]?.length ?? 0) > 0 - ) - : documents; + const visibleDocuments = + isChunkSearchActive && !chunkSearchLoading + ? documents.filter( + (doc) => (chunkSearchResultMap?.[doc.id]?.length ?? 0) > 0 + ) + : documents; const tabItems = visibleDocuments.map((doc) => { const chunkCount = isChunkSearchActive @@ -762,7 +736,9 @@ const DocumentChunk: React.FC = ({ className="self-center" /> - {t("document.chunk.tooltip.edit")} + + {t("document.chunk.tooltip.edit")} + )} @@ -775,7 +751,9 @@ const DocumentChunk: React.FC = ({ className="self-center" /> - {t("document.chunk.tooltip.download")} + + {t("document.chunk.tooltip.download")} + {!isReadOnlyMode && ( @@ -789,7 +767,9 @@ const DocumentChunk: React.FC = ({ className="self-center" /> - {t("document.chunk.tooltip.delete")} + + {t("document.chunk.tooltip.delete")} + )} @@ -913,7 +893,9 @@ const DocumentChunk: React.FC = ({ onClick={openCreateChunkModal} /> - {t("document.chunk.tooltip.create")} + + {t("document.chunk.tooltip.create")} + )} diff --git a/frontend/app/[locale]/knowledges/components/document/DocumentList.tsx b/frontend/app/[locale]/knowledges/components/document/DocumentList.tsx index fc253afe2a..e51a64a929 100644 --- a/frontend/app/[locale]/knowledges/components/document/DocumentList.tsx +++ b/frontend/app/[locale]/knowledges/components/document/DocumentList.tsx @@ -15,13 +15,17 @@ import { App, Select, Popover, + Progress, Segmented, - Space, Tooltip, } from "antd"; import { useStorageQuotaBlocked } from "@/hooks/useStorageQuotaBlocked"; const { TextArea } = Input; -import { FilterOutlined, InfoCircleFilled } from "@ant-design/icons"; +import { + FileTextOutlined, + FilterOutlined, + ReloadOutlined, +} from "@ant-design/icons"; import { BookText, Pilcrow, @@ -31,6 +35,9 @@ import { CircleOff, AlertCircle, Tag, + SlidersHorizontal, + ChevronDown, + ChevronUp, } from "lucide-react"; import { NAME_CHECK_STATUS } from "@/const/agentConfig"; import { MarkdownRenderer } from "@/components/common/markdownRenderer"; @@ -49,12 +56,14 @@ import { modelService } from "@/services/modelService"; import { getTenantDefaultGroupId } from "@/services/groupService"; import { extractObjectNameFromUrl } from "@/services/storageService"; import { Document } from "@/types/knowledgeBase"; +import type { KBQuotaStatus } from "@/types/quota"; import type { TagDocumentPredicate, TagDocumentBatchStatusEntry, } from "@/types/tagManagement"; import { ModelOption } from "@/types/modelConfig"; import { formatFileSize } from "@/lib/utils"; +import { getKnowledgeBaseQuotaDisplay } from "@/lib/knowledgeBaseQuota"; import log from "@/lib/logger"; import { useConfig } from "@/hooks/useConfig"; import { useGroupDetails, useGroupList } from "@/hooks/group/useGroupList"; @@ -63,6 +72,7 @@ import DocumentStatus from "./DocumentStatus"; import DocumentChunk from "./DocumentChunk"; import UploadArea from "../upload/UploadArea"; import ResourceTagAssignmentModal from "@/components/tag/ResourceTagAssignmentModal"; +import ResourceTagChips from "@/components/tag/ResourceTagChips"; import TagDefinitionManagementModal from "@/components/tag/TagDefinitionManagementModal"; import TagFilterControls from "@/components/tag/TagFilterControls"; import { useTagDefinitions, useTagLibraries } from "@/hooks/useTagManagement"; @@ -89,6 +99,8 @@ interface DocumentListProps { knowledgeBaseSource?: string; // User-facing knowledge base name (display name) knowledgeBaseName?: string; + knowledgeBaseDescription?: string; + onDescriptionChange?: (description: string) => void; // Internal knowledge base ID / Elasticsearch index name knowledgeBaseId?: string; modelMismatch?: boolean; @@ -101,6 +113,7 @@ interface DocumentListProps { hasDocuments?: boolean; isNewlyCreatedAndWaiting?: boolean; // New prop to track newly created KB waiting for documents onChunkCountChange?: () => void; // Callback when chunk count changes + onRefresh?: () => void; // Group permission and user groups for create mode ingroupPermission?: string; @@ -114,6 +127,7 @@ interface DocumentListProps { isMultimodal?: boolean; quotaLimitBytes?: number | null; onQuotaLimitBytesChange?: (value: number | null) => void; + quotaStatus?: KBQuotaStatus; onMultimodalChange?: (value: boolean) => void; permission?: string; // User's permission for this knowledge base (READ_ONLY, EDIT, etc.) preserveSourceFile?: boolean; @@ -129,6 +143,7 @@ interface DocumentListProps { onDragLeave?: (e: React.DragEvent) => void; onDrop?: (e: React.DragEvent) => void; onFileSelect: (files: File[]) => void; + selectedFiles?: File[]; onUpload?: (files: File[]) => Promise; isUploading?: boolean; } @@ -145,6 +160,8 @@ const DocumentListContainer = forwardRef( knowledgeBaseSource = "", knowledgeBaseId = "", knowledgeBaseName = "", + knowledgeBaseDescription = "", + onDescriptionChange, modelMismatch = false, currentModel = "", knowledgeBaseModel = "", @@ -155,6 +172,7 @@ const DocumentListContainer = forwardRef( hasDocuments = false, isNewlyCreatedAndWaiting = false, // New prop onChunkCountChange, + onRefresh, // Group permission and user groups for create mode ingroupPermission, onIngroupPermissionChange, @@ -171,6 +189,7 @@ const DocumentListContainer = forwardRef( onPreserveSourceFileChange, quotaLimitBytes = null, onQuotaLimitBytesChange, + quotaStatus, // Auto-summary frequency summaryFrequency, onSummaryFrequencyChange, @@ -181,6 +200,7 @@ const DocumentListContainer = forwardRef( onDragLeave, onDrop, onFileSelect, + selectedFiles = [], onUpload, isUploading = false, }, @@ -223,18 +243,18 @@ const DocumentListContainer = forwardRef( // Use fixed height instead of percentage const titleBarHeight = UI_CONFIG.TITLE_BAR_HEIGHT; const uploadHeight = UI_CONFIG.UPLOAD_COMPONENT_HEIGHT; + const [isAdvancedSettingsOpen, setIsAdvancedSettingsOpen] = useState(false); const [assignTarget, setAssignTarget] = useState<{ docId: string; canEdit: boolean; } | null>(null); + const [documentTagRefreshKey, setDocumentTagRefreshKey] = useState(0); const { data: tagLibraries } = useTagLibraries(); const documentLibrary = tagLibraries?.find((lib) => lib.bucket_key === "knowledge_content") ?? null; - const { - data: assignDefinitions, - refresh: refreshAssignDefinitions, - } = useTagDefinitions(documentLibrary?.bucket_id ?? null); + const { data: assignDefinitions, refresh: refreshAssignDefinitions } = + useTagDefinitions(documentLibrary?.bucket_id ?? null); const [tagManagementOpen, setTagManagementOpen] = useState(false); const [documentPredicates, setDocumentPredicates] = useState< @@ -378,6 +398,18 @@ const DocumentListContainer = forwardRef( ); const { t } = useTranslation(); const isDataMate = (knowledgeBaseSource || "").toLowerCase() === "datamate"; + const { + hasQuota, + availableCapacity: quotaAvailable, + totalCapacity: quotaTotal, + usagePercent: quotaUsagePercent, + } = getKnowledgeBaseQuotaDisplay(quotaStatus, t); + let documentListClassName = "flex-grow min-h-0 overflow-auto px-6 py-5"; + if (isCreatingMode) { + documentListClassName = "w-full bg-white p-2"; + } else if (showChunk) { + documentListClassName = "flex-grow min-h-0 overflow-hidden px-6 pt-5"; + } // Determine if user has read-only permission const isReadOnlyMode = permission === "READ_ONLY"; @@ -671,221 +703,101 @@ const DocumentListContainer = forwardRef( return (
{/* Title bar */}
-
+
{isCreatingMode ? ( -
-
- - onNameChange && onNameChange(e.target.value) - } - placeholder={t("document.input.knowledgeBaseName")} - className={`${LAYOUT.KB_TITLE_MARGIN} max-w-[240px] font-medium`} - size="large" - prefix={📚} - status={ - isCreatingMode && - (nameStatus === NAME_CHECK_STATUS.EXISTS_IN_TENANT || - nameStatus === - NAME_CHECK_STATUS.EXISTS_IN_OTHER_TENANT) - ? "error" - : undefined - } - autoFocus - disabled={ - hasDocuments || - isUploading || - docState.isLoadingDocuments - } - /> - {isCreatingMode && - (nameStatus === NAME_CHECK_STATUS.EXISTS_IN_TENANT || - nameStatus === - NAME_CHECK_STATUS.EXISTS_IN_OTHER_TENANT) && ( -
- - - {t("tenantResources.knowledgeBase.nameExists")} +
+

+ {t("document.title.createNew")} +

+

+ {t("knowledgeBase.create.subtitle")} +

+
+ ) : ( + <> +
+
+ +
+
+

+ {knowledgeBaseName} +

+
+ + {t("knowledgeBase.tag.documents", { + count: documents.length, + })} + + {modelMismatch && ( + + {getMismatchInfo()} -
- )} -
- {/* Right dropdowns for create mode */} -
- {/* Embedding model selection - first position in create mode */} - {isCreatingMode && onEmbeddingModelChange && ( - - - {/* Group permission dropdown */} - - - )} - {onQuotaLimitBytesChange && ( - - { - if (v == null) { - onQuotaLimitBytesChange(null); - } else if (quotaUnit === "GB") { - onQuotaLimitBytesChange(v * 1024 * 1024 * 1024); - } else { - onQuotaLimitBytesChange(v * 1024 * 1024); - } - }} - addonAfter={quotaUnit} - placeholder={t("quota.unlimited", "无限制")} - min={0} - precision={0} - style={{ width: 130 }} - /> - + {hasQuota && ( + setQuotaUnit(val as "GB" | "MB")} + status={ + quotaStatus.kb_warning_level === "exceeded" + ? "exception" + : "normal" + } /> - - )} -
-
- ) : ( -

- {knowledgeBaseName} -

- )} - {modelMismatch && !isCreatingMode && ( -
- {getMismatchInfo()} -
+ )} +
+ )} + )}
{/* Right: tag filter, Tag Management, overview and detail buttons */} {!isCreatingMode && !isDataMate && ( -
+
{documentLibrary && assignDefinitions && ( ( )} + {onRefresh && ( + +
)}
@@ -965,9 +890,7 @@ const DocumentListContainer = forwardRef( {/* Document list */}
{ if (!isCreatingMode && knowledgeBaseName) { return; @@ -989,15 +912,13 @@ const DocumentListContainer = forwardRef( }} > {showChunk ? ( -
+
@@ -1014,7 +935,7 @@ const DocumentListContainer = forwardRef(
) : showDetail ? ( -
+
{t("document.summary.title")} @@ -1154,72 +1075,359 @@ const DocumentListContainer = forwardRef(
) : isCreatingMode ? ( - hasDocuments || isUploading || docState.isLoadingDocuments ? ( -
-
-
-

- {t("document.status.waitingForTask")} -

+
+ {hasDocuments || isUploading || docState.isLoadingDocuments ? ( +
+
+
+

+ {t("document.status.waitingForTask")} +

+
-
- ) : ( -
-
-
- + ) : ( +
+
+ + + onNameChange && onNameChange(e.target.value) + } + placeholder={t( + "knowledgeBase.create.namePlaceholder", + "例如:产品知识中心" + )} + className="!rounded-xl" + size="large" + status={ + nameStatus === NAME_CHECK_STATUS.EXISTS_IN_TENANT || + nameStatus === NAME_CHECK_STATUS.EXISTS_IN_OTHER_TENANT + ? "error" + : undefined + } + autoFocus + disabled={ + hasDocuments || + isUploading || + docState.isLoadingDocuments + } + /> + {(nameStatus === NAME_CHECK_STATUS.EXISTS_IN_TENANT || + nameStatus === + NAME_CHECK_STATUS.EXISTS_IN_OTHER_TENANT) && ( +
+ + + {t("tenantResources.knowledgeBase.nameExists")} + +
+ )}
-

- {t("document.title.createNew")} -

-

- {t("document.hint.uploadToCreate")} -

+ +
+ +