import React, { useCallback, useEffect, useState, useMemo } from 'react' import IndexingModalWithMode from '../../components/IndexingModalWithMode' import { PDFPreview } from '../../components/PDFPreview' import { DragDropUpload } from '../../components/DragDropUpload' import { GlobalDragDropOverlay } from '../../components/GlobalDragDropOverlay' import { AppSettings, DEFAULT_MODELS, DEFAULT_SETTINGS, IndexingConfig, KnowledgeFile, ModelConfig, ModelType, RawFile, KnowledgeGroup, } from '../../types' import { readFile, formatBytes } from '../../utils/fileUtils' import { useLanguage } from '../../contexts/LanguageContext' import { useToast } from '../../contexts/ToastContext' import { userSettingService } from '../../services/userSettingService' import { uploadService } from '../../services/uploadService' import { knowledgeBaseService } from '../../services/knowledgeBaseService' import { knowledgeGroupService } from '../../services/knowledgeGroupService' import { useConfirm } from '../../contexts/ConfirmContext' import { ChunkInfoDrawer } from '../../components/ChunkInfoDrawer' import { Search, Filter, Plus, FileText, Image as ImageIcon, FileType, CheckCircle2, CircleDashed, RefreshCw, Eye, Trash2, RotateCcw, Settings, MoreVertical, Folder, Hash, ChevronRight, Tag, Layers, MoreHorizontal } from 'lucide-react' import { motion, AnimatePresence } from 'framer-motion' import { KB_ALLOWED_EXTENSIONS, IMAGE_MIME_TYPES, isExtensionAllowed, isFormatSupportedForPreview } from '../../constants/fileSupport' interface KnowledgeBaseViewProps { authToken: string; onLogout: () => void; modelConfigs?: ModelConfig[]; onNavigate: (view: any) => void; isAdmin?: boolean; } export const KnowledgeBaseView: React.FC = (props) => { const { authToken, modelConfigs = DEFAULT_MODELS, isAdmin = false } = props; const { showError, showWarning, showSuccess } = useToast() const { confirm } = useConfirm() const { t } = useLanguage() // Data State const [files, setFiles] = useState([]) const [groups, setGroups] = useState([]) const [settings, setSettings] = useState(DEFAULT_SETTINGS) const [isLoadingSettings, setIsLoadingSettings] = useState(true) const [isLoadingFiles, setIsLoadingFiles] = useState(true) // UI State const [pdfPreview, setPdfPreview] = useState<{ fileId: string; fileName: string } | null>(null) const [isIndexingModalOpen, setIsIndexingModalOpen] = useState(false) const [pendingFiles, setPendingFiles] = useState([]) const [fileInputRef] = useState>(React.createRef()) const [shouldOpenModal, setShouldOpenModal] = useState(false) // Filter & Pagination State const [filterName, setFilterName] = useState('') const [filterGroup, setFilterGroup] = useState('all') const [filterStatus, setFilterStatus] = useState<'all' | 'ready' | 'indexing' | 'failed'>('all') const [currentPage, setCurrentPage] = useState(1) const pageSize = 12 // Adjusted for card grid const [chunkDrawer, setChunkDrawer] = useState<{ isOpen: boolean; fileId: string; fileName: string } | null>(null) const [isAutoRefreshEnabled, setIsAutoRefreshEnabled] = useState(true) const [autoRefreshInterval] = useState(5000) // Sidebar State const [selectedSidebarFilter, setSelectedSidebarFilter] = useState<{ type: 'all' | 'uncategorized' | 'group'; groupId?: string }>({ type: 'all' }) const [isGroupModalOpen, setIsGroupModalOpen] = useState(false) const [editingGroup, setEditingGroup] = useState(null) const [newGroupName, setNewGroupName] = useState('') const fetchAndSetSettings = useCallback(async () => { if (!authToken) return try { const settingsData = isAdmin ? await userSettingService.getGlobal(authToken) : await userSettingService.get(authToken); setSettings({ ...DEFAULT_SETTINGS, ...settingsData }) } catch (error) { console.error('Failed to fetch settings:', error) } finally { setIsLoadingSettings(false) } }, [authToken, isAdmin]) const fetchAndSetFiles = useCallback(async () => { if (!authToken) return try { const remoteFiles = await knowledgeBaseService.getAll(authToken) setFiles(remoteFiles) } catch (error) { console.error('Failed to fetch files:', error) } finally { setIsLoadingFiles(false) } }, [authToken]) const fetchAndSetGroups = useCallback(async () => { if (!authToken) return try { const remoteGroups = await knowledgeGroupService.getGroups() setGroups(remoteGroups) } catch (error) { console.error('Failed to fetch groups:', error) } }, [authToken]) useEffect(() => { if (authToken) { fetchAndSetSettings() fetchAndSetFiles() fetchAndSetGroups() } }, [authToken, fetchAndSetSettings, fetchAndSetFiles, fetchAndSetGroups]) useEffect(() => { if (shouldOpenModal && pendingFiles.length > 0) { setIsIndexingModalOpen(true); setShouldOpenModal(false); } }, [shouldOpenModal, pendingFiles.length]); const handleFileUpload = async (fileList: FileList) => { if (!authToken) { showWarning(t('loginRequired')) return } const MAX_FILE_SIZE = 104857600; const rawFiles: RawFile[] = [] const errors: string[] = [] const filesArray = Array.from(fileList); for (const file of filesArray) { const extension = file.name.split('.').pop() || '' if (!isExtensionAllowed(extension, 'kb')) { errors.push(t('unsupportedFileType', file.name, extension)) continue } if (file.size > MAX_FILE_SIZE) { errors.push(t('fileSizeLimitExceeded', file.name, formatBytes(file.size), 100)) continue; } try { const rawFile = await readFile(file) rawFiles.push(rawFile) } catch (error) { errors.push(`${file.name} - ${t('readingFailed')}`); } } if (errors.length > 0) showError(`${t('uploadErrors')}:\n${errors.join('\n')}`); if (rawFiles.length === 0) return; setPendingFiles(rawFiles); setShouldOpenModal(true); } const handleConfirmIndexing = async (config: IndexingConfig) => { if (!authToken) return let hasSuccess = false for (const rawFile of pendingFiles) { try { const indexingConfig = { ...config, groupIds: selectedSidebarFilter.type === 'group' ? [selectedSidebarFilter.groupId!] : [] }; await uploadService.uploadFileWithConfig(rawFile.file, indexingConfig, authToken) hasSuccess = true } catch (error: any) { showError(`${t('uploadFailed')}: ${rawFile.name} - ${error.message}`) } } if (hasSuccess) await fetchAndSetFiles() setPendingFiles([]) setIsIndexingModalOpen(false) } const handleRemoveFile = async (id: string) => { if (!(await confirm(t('confirmDeleteFile')))) return if (!authToken) return try { await knowledgeBaseService.deleteFile(id, authToken) setFiles(prev => prev.filter(f => f.id !== id)) showSuccess(t('fileDeleted')) } catch (error: any) { showError(`${t('deleteFailed')}: ` + error.message) } } const handleToggleFileCategory = async (file: KnowledgeFile, groupId: string) => { try { const currentGroupIds = file.groups?.map(g => g.id) || []; const isAssigned = currentGroupIds.includes(groupId); let newGroupIds: string[]; if (isAssigned) { newGroupIds = currentGroupIds.filter(id => id !== groupId); } else { newGroupIds = [...currentGroupIds, groupId]; } await knowledgeGroupService.addFileToGroups(file.id, newGroupIds); await fetchAndSetFiles(); } catch (error: any) { console.error('Failed to toggle category:', error); showError(t('actionFailed') + ': ' + error.message); } } const handleClearAll = async () => { if (!(await confirm(t('confirmClearKB')))) return if (!authToken) return try { await knowledgeBaseService.clearAll(authToken) setFiles([]) showSuccess(t('kbCleared')) } catch (error: any) { showError(`${t('clearFailed')}: ` + error.message) } } const filteredFiles = useMemo(() => { return files.filter(file => { const matchName = file.name.toLowerCase().includes(filterName.toLowerCase()); // Updated filtering logic for sidebar let matchGroup = true; if (selectedSidebarFilter.type === 'uncategorized') { matchGroup = !file.groups || file.groups.length === 0; } else if (selectedSidebarFilter.type === 'group') { matchGroup = file.groups?.some(g => g.id === selectedSidebarFilter.groupId) || false; } const matchStatus = filterStatus === 'all' || (filterStatus === 'ready' && (file.status === 'ready' || file.status === 'vectorized')) || (filterStatus === 'indexing' && (file.status === 'indexing' || file.status === 'pending' || file.status === 'extracted')) || (filterStatus === 'failed' && (file.status === 'failed' || file.status === 'error')); return matchName && matchGroup && matchStatus; }); }, [files, filterName, selectedSidebarFilter, filterStatus]); const totalPages = Math.ceil(filteredFiles.length / pageSize); const paginatedFiles = useMemo(() => { const start = (currentPage - 1) * pageSize; return filteredFiles.slice(start, start + pageSize); }, [filteredFiles, currentPage, pageSize]); useEffect(() => { setCurrentPage(1); }, [filterName, filterGroup, filterStatus]); useEffect(() => { let intervalId: NodeJS.Timeout | null = null; const hasIndexingFiles = files.some(file => ['pending', 'indexing', 'extracted', 'vectorized'].includes(file.status)); if (isAutoRefreshEnabled && hasIndexingFiles) { intervalId = setInterval(() => { fetchAndSetFiles(); }, autoRefreshInterval); } return () => { if (intervalId) clearInterval(intervalId); }; }, [isAutoRefreshEnabled, files, autoRefreshInterval, fetchAndSetFiles]); const getFileIcon = (file: KnowledgeFile) => { if (file.type.startsWith('image/')) return ; if (file.type === 'application/pdf') return ; return ; }; const handleCreateOrUpdateGroup = async () => { if (!newGroupName.trim()) return try { if (editingGroup) { await knowledgeGroupService.updateGroup(editingGroup.id, { name: newGroupName }) showSuccess(t('groupUpdated')) } else { await knowledgeGroupService.createGroup({ name: newGroupName }) showSuccess(t('groupCreated')) } fetchAndSetGroups() setIsGroupModalOpen(false) setEditingGroup(null) setNewGroupName('') } catch (error: any) { showError(t('actionFailed') + ': ' + error.message) } } const handleDeleteGroup = async (group: KnowledgeGroup) => { if (!(await confirm(t('confirmDeleteGroup')))) return try { await knowledgeGroupService.deleteGroup(group.id) showSuccess(t('groupDeleted')) if (selectedSidebarFilter.groupId === group.id) { setSelectedSidebarFilter({ type: 'all' }) } fetchAndSetGroups() } catch (error: any) { showError(t('deleteFailed') + ': ' + error.message) } } if (isLoadingSettings) { return (
) } return (
{/* Sidebar */}

Catalog

Categories

{isAdmin && ( )}
{groups.map(group => (
{isAdmin && (
)}
))}
{/* Main Content Area */}
{ if (e.target.files && e.target.files.length > 0) handleFileUpload(e.target.files) }} multiple className="hidden" accept={KB_ALLOWED_EXTENSIONS.map(ext => `.${ext}`).join(',') + ',' + IMAGE_MIME_TYPES.join(',')} /> {/* Header Section */}

{selectedSidebarFilter.type === 'all' ? 'Knowledge Base' : selectedSidebarFilter.type === 'uncategorized' ? 'Uncategorized Files' : groups.find(g => g.id === selectedSidebarFilter.groupId)?.name || 'Category'}

{selectedSidebarFilter.type === 'group' ? groups.find(g => g.id === selectedSidebarFilter.groupId)?.description || 'Documents in this category.' : 'Manage your documents and data sources.'}

{isAdmin && ( )}
{/* Filter Bar */}
setFilterName(e.target.value)} className="w-full h-9 pl-9 pr-4 bg-white border border-slate-200 rounded-lg text-sm transition-all focus:ring-1 focus:ring-blue-500 focus:border-blue-500 outline-none" />
{/* Card Grid */}
{isLoadingFiles ? (
) : paginatedFiles.length > 0 ? (
{paginatedFiles.map((file) => ( {/* Icon */}
{getFileIcon(file)}
{/* Name & Desc */}

setChunkDrawer({ isOpen: true, fileId: file.id, fileName: file.name })} className="font-bold text-slate-900 text-[15px] cursor-pointer hover:text-blue-600 transition-colors truncate" > {file.name}

{file.name.split('.').pop()?.toUpperCase() || 'FILE'}

{file.status === 'ready' || file.status === 'vectorized' ? `Document processed and ready.` : `Processing... Currently in ${file.status} state.` }

{file.groups && file.groups.length > 0 && (
{file.groups.map(g => ( {g.name} ))}
)}
{/* Meta & Actions */}
{new Date(file.createdAt || Date.now()).toLocaleDateString('ja-JP')} {formatBytes(file.size)}
{file.status !== 'ready' && file.status !== 'vectorized' ? ( ) : null}
{isFormatSupportedForPreview(file.name) && ( )}
Select Category
{groups.map(g => ( ))}
{isAdmin && ( )}
))}
) : (
)}
{/* Pagination */} {totalPages > 1 && (
{currentPage} of {totalPages}
)}
{ setPendingFiles([]); setIsIndexingModalOpen(false); }} files={pendingFiles} embeddingModels={modelConfigs.filter(m => m.type === ModelType.EMBEDDING)} defaultEmbeddingId={settings.selectedEmbeddingId} onConfirm={handleConfirmIndexing} authToken={authToken} /> {/* Group Modal */} {isGroupModalOpen && (

{editingGroup ? 'Edit Category' : 'Create New Category'}

Organize your documents with descriptive categories.

setNewGroupName(e.target.value)} placeholder="e.g. Finance, Projects..." className="w-full h-11 px-4 bg-slate-50 border border-slate-200 rounded-xl outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition-all" />
)}
{pdfPreview && ( setPdfPreview(null)} /> )} {chunkDrawer && ( setChunkDrawer(null)} fileId={chunkDrawer.fileId} fileName={chunkDrawer.fileName} authToken={authToken} /> )}
) }