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 } 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) // Auto-refresh state const [isAutoRefreshEnabled, setIsAutoRefreshEnabled] = useState(true) const [autoRefreshInterval] = useState(5000) 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 { await uploadService.uploadFileWithConfig(rawFile.file, config, 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 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()); const matchGroup = filterGroup === 'all' || file.groups?.some(g => g.id === filterGroup); 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, filterGroup, 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 ; }; if (isLoadingSettings) { return (
) } return (
{ 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 */}

Knowledge Base

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.` }

{/* Meta & Actions */}
{new Date().toLocaleDateString('ja-JP')} {formatBytes(file.size)}
{file.status !== 'ready' && file.status !== 'vectorized' ? ( ) : null}
{isFormatSupportedForPreview(file.name) && ( )} {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} /> {pdfPreview && ( setPdfPreview(null)} /> )} {chunkDrawer && ( setChunkDrawer(null)} fileId={chunkDrawer.fileId} fileName={chunkDrawer.fileName} authToken={authToken} /> )}
) }