import React, { useCallback, useEffect, useState } from 'react'; import { Card, CardContent } from '@/components/ui/card'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@/components/ui/select'; import { Button } from '@/components/ui/button'; import { httpClient } from '@/app/infra/http/HttpClient'; import { toast } from 'sonner'; import { useTranslation } from 'react-i18next'; import { ParserInfo } from '@/app/infra/entities/api'; import { CustomApiError, I18nObject } from '@/app/infra/entities/common'; import { extractI18nObject } from '@/i18n/I18nProvider'; interface FileUploadZoneProps { kbId: string; ragEngineName?: I18nObject; ragEngineCapabilities?: string[]; onUploadSuccess: () => void; onUploadError: (error: string) => void; } export default function FileUploadZone({ kbId, ragEngineName, ragEngineCapabilities, onUploadSuccess, onUploadError, }: FileUploadZoneProps) { const { t } = useTranslation(); const [isDragOver, setIsDragOver] = useState(false); const [isUploading, setIsUploading] = useState(false); // Parser selection state const [pendingFile, setPendingFile] = useState(null); const [availableParsers, setAvailableParsers] = useState([]); const [selectedParser, setSelectedParser] = useState('builtin'); const [loadingParsers, setLoadingParsers] = useState(false); // Whether the Knowledge Engine natively supports document parsing. // This is a coarse-grained capability check rather than per-MIME-type filtering. // Fine-grained MIME type declaration (e.g. supported_parse_mime_types on the engine) // would require changes across the SDK, backend, and frontend prop chain; // using an engine-level capability flag keeps the change minimal. const ragEngineCanParse = ragEngineCapabilities?.includes('doc_parsing') ?? false; // When a file is selected, check for available parsers useEffect(() => { if (!pendingFile) return; const mimeType = pendingFile.type || undefined; setLoadingParsers(true); httpClient .listParsers(mimeType) .then((resp) => { const parsers = resp.parsers || []; setAvailableParsers(parsers); if (ragEngineCanParse) { setSelectedParser('builtin'); } else if (parsers.length > 0) { setSelectedParser(parsers[0].plugin_id); } else { setSelectedParser(''); } }) .catch(() => { setAvailableParsers([]); }) .finally(() => { setLoadingParsers(false); }); }, [pendingFile, ragEngineCanParse]); const doUpload = useCallback( async (file: File, parserPluginId?: string) => { setIsUploading(true); const toastId = toast.loading(t('knowledge.documentsTab.uploadingFile')); try { // Step 1: Upload file to server const uploadResult = await httpClient.uploadDocumentFile(file); // Step 2: Associate file with knowledge base (with optional parser) await httpClient.uploadKnowledgeBaseFile( kbId, uploadResult.file_id, parserPluginId, ); toast.success(t('knowledge.documentsTab.uploadSuccess'), { id: toastId, }); onUploadSuccess(); } catch (error) { console.error('File upload failed:', error); const errorMessage = t('knowledge.documentsTab.uploadError') + (error as CustomApiError).msg; toast.error(errorMessage, { id: toastId }); onUploadError(errorMessage); } finally { setIsUploading(false); setPendingFile(null); setAvailableParsers([]); setSelectedParser('builtin'); } }, [kbId, onUploadSuccess, onUploadError, t], ); const handleFileSelected = useCallback( async (file: File) => { if (isUploading) return; // Check file size (10MB limit) const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB if (file.size > MAX_FILE_SIZE) { toast.error(t('knowledge.documentsTab.fileSizeExceeded')); return; } // Set loadingParsers=true BEFORE pendingFile so both state updates // batch together in the same render. This prevents the auto-upload // effect from firing before parser fetch completes. setLoadingParsers(true); setPendingFile(file); }, [isUploading, t], ); // Auto-upload if Knowledge Engine can parse and no external parsers available useEffect(() => { if ( pendingFile && !loadingParsers && ragEngineCanParse && availableParsers.length === 0 ) { doUpload(pendingFile); } }, [ pendingFile, loadingParsers, ragEngineCanParse, availableParsers, doUpload, ]); const handleConfirmUpload = useCallback(() => { if (!pendingFile) return; const parserPluginId = selectedParser === 'builtin' ? undefined : selectedParser; doUpload(pendingFile, parserPluginId); }, [pendingFile, selectedParser, doUpload]); const handleCancelUpload = useCallback(() => { setPendingFile(null); setAvailableParsers([]); setSelectedParser('builtin'); }, []); const handleDragOver = useCallback((e: React.DragEvent) => { e.preventDefault(); setIsDragOver(true); }, []); const handleDragLeave = useCallback((e: React.DragEvent) => { e.preventDefault(); setIsDragOver(false); }, []); const handleDrop = useCallback( (e: React.DragEvent) => { e.preventDefault(); setIsDragOver(false); const files = Array.from(e.dataTransfer.files); if (files.length > 0) { handleFileSelected(files[0]); } }, [handleFileSelected], ); const handleFileSelect = useCallback( (e: React.ChangeEvent) => { const files = e.target.files; if (files && files.length > 0) { handleFileSelected(files[0]); } // Reset the input so the same file can be selected again e.target.value = ''; }, [handleFileSelected], ); // Show parser selection UI when there are choices to make, or when no parser is available const showParserSelector = pendingFile && !loadingParsers && (availableParsers.length > 0 || !ragEngineCanParse); const noParserAvailable = !ragEngineCanParse && availableParsers.length === 0; return ( {showParserSelector ? (

{pendingFile.name}

{noParserAvailable ? (

{t('knowledge.documentsTab.noParserAvailable')}

) : (
)}
{!noParserAvailable && ( )}
) : (
)}
); }