import React, { useState, useEffect } from 'react'; import { X, FolderInput, ArrowRight, Info } from 'lucide-react'; import { GROUP_ALLOWED_EXTENSIONS, isExtensionAllowed, getSupportedFormatsLabel } from '../constants/fileSupport'; import { useLanguage } from '../contexts/LanguageContext'; import { ModelConfig, ModelType, IndexingConfig } from '../types'; import { modelConfigService } from '../services/modelConfigService'; import { knowledgeGroupService } from '../services/knowledgeGroupService'; import { useToast } from '../contexts/ToastContext'; import IndexingModalWithMode from './IndexingModalWithMode'; interface ImportFolderDrawerProps { isOpen: boolean; onClose: () => void; authToken: string; initialGroupId?: string; // If provided, locks target to this group initialGroupName?: string; onImportSuccess?: () => void; } export const ImportFolderDrawer: React.FC = ({ isOpen, onClose, authToken, initialGroupId, initialGroupName, onImportSuccess }) => { const { t } = useLanguage(); const { showError, showSuccess } = useToast(); // Form State const [localFiles, setLocalFiles] = useState([]); const [folderName, setFolderName] = useState(''); const [targetName, setTargetName] = useState(''); const fileInputRef = React.useRef(null); // Indexing Config State const [isIndexingConfigOpen, setIsIndexingConfigOpen] = useState(false); // Data State const [models, setModels] = useState([]); const [isLoading, setIsLoading] = useState(false); useEffect(() => { if (isOpen) { // Reset form setLocalFiles([]); setFolderName(''); setTargetName(initialGroupName || ''); setIsIndexingConfigOpen(false); // Fetch models modelConfigService.getAll(authToken).then(res => { setModels(res.filter(m => m.type === ModelType.EMBEDDING)); }); } }, [isOpen, authToken, initialGroupName]); const handleLocalFolderChange = (e: React.ChangeEvent) => { if (e.target.files && e.target.files.length > 0) { const allFiles = Array.from(e.target.files); // Filter files by allowed extensions const files = allFiles.filter(file => { const ext = file.name.split('.').pop() || ''; return isExtensionAllowed(ext, 'group'); }); if (files.length === 0 && allFiles.length > 0) { showError(t('noFilesFound')); return; } setLocalFiles(files); // Get root folder name from webkitRelativePath const firstPath = allFiles[0].webkitRelativePath; // Use allFiles to get path even if filtered if (firstPath) { const parts = firstPath.split('/'); if (parts.length > 0) { const name = parts[0]; setFolderName(name); if (!initialGroupId && !targetName) { setTargetName(name); } } } } }; const handleNext = async () => { if (localFiles.length === 0) { showError(t('clickToSelectFolder')); return; } if (!initialGroupId && !targetName) { showError(t('fillTargetName')); return; } // Open indexing config modal setIsIndexingConfigOpen(true); }; const handleConfirmConfig = async (config: IndexingConfig) => { setIsLoading(true); try { // 1. Ensure target group exists or create it let groupId = initialGroupId; if (!groupId) { const newGroup = await knowledgeGroupService.createGroup({ name: targetName, description: t('importedFromLocalFolder').replace('$1', folderName) }); groupId = newGroup.id; } // 2. Upload files in batches const { uploadService } = await import('../services/uploadService'); const { readFile } = await import('../utils/fileUtils'); // Limit concurrent uploads for large folders const BATCH_SIZE = 3; for (let i = 0; i < localFiles.length; i += BATCH_SIZE) { const batch = localFiles.slice(i, i + BATCH_SIZE); await Promise.all(batch.map(async (file) => { try { await readFile(file); // Optional: verify readability const uploadedKb = await uploadService.uploadFileWithConfig(file, config, authToken); if (groupId) { await knowledgeGroupService.addFileToGroups(uploadedKb.id, [groupId]); } } catch (err) { console.error(`Failed to upload ${file.name}:`, err); } })); } showSuccess(t('importComplete')); onImportSuccess?.(); onClose(); } catch (error: any) { showError(t('submitFailed', error.message)); } finally { setIsLoading(false); setIsIndexingConfigOpen(false); } }; if (!isOpen) return null; return ( <>
{/* Header */}

{t('importFolderTitle')}

{/* Body */}

{t('supportedFormatsInfo')}

{t('importFolderTip')}

{/* Local Folder Selection */}
fileInputRef.current?.click()} className="border-2 border-dashed border-slate-200 rounded-xl p-8 flex flex-col items-center justify-center gap-3 cursor-pointer hover:border-blue-400 hover:bg-blue-50 transition-all group" >

{localFiles.length > 0 ? t('selectedFilesCount').replace('$1', localFiles.length.toString()) : t('clickToSelectFolder')}

{localFiles.length > 0 ? folderName : t('selectFolderTip')}

{/* Target Group */}
setTargetName(e.target.value)} disabled={!!initialGroupId} // Readonly if locking to group placeholder={t('placeholderNewGroup')} className={`w-full px-3 py-2 border border-slate-200 rounded-lg text-sm focus:ring-2 focus:ring-blue-500 outline-none ${initialGroupId ? 'bg-slate-50 text-slate-500' : ''}`} /> {initialGroupId &&

{t('importToCurrentGroup')}

}
{/* Footer */}
{/* Indexing Config Modal */} setIsIndexingConfigOpen(false)} files={[]} // Empty array for folder import mode embeddingModels={models} defaultEmbeddingId={models.length > 0 ? models[0].id : ''} onConfirm={handleConfirmConfig} isReconfiguring={false} /> ); };