import React, { useState, useRef, useEffect } from 'react'; import { useTranslation } from 'react-i18next'; import { useSelector } from 'react-redux'; import DocumentChunks from './DocumentChunks'; import ContextMenu, { MenuOption } from './ContextMenu'; import userService from '../api/services/userService'; import FileIcon from '../assets/file.svg'; import FolderIcon from '../assets/folder.svg'; import ArrowLeft from '../assets/arrow-left.svg'; import ThreeDots from '../assets/three-dots.svg'; import EyeView from '../assets/eye-view.svg'; import OutlineSource from '../assets/outline-source.svg'; import Trash from '../assets/red-trash.svg'; interface FileNode { type?: string; token_count?: number; size_bytes?: number; [key: string]: any; } interface DirectoryStructure { [key: string]: FileNode; } interface FileTreeComponentProps { docId: string; sourceName: string; onBackToDocuments?: () => void; } const FileTreeComponent: React.FC = ({ docId, sourceName, onBackToDocuments, }) => { const { t } = useTranslation(); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [directoryStructure, setDirectoryStructure] = useState(null); const [currentPath, setCurrentPath] = useState([]); const token = useSelector((state: any) => state.auth?.token); const [activeMenuId, setActiveMenuId] = useState(null); const menuRefs = useRef<{ [key: string]: React.RefObject; }>({}); const [selectedFile, setSelectedFile] = useState<{ id: string; name: string; } | null>(null); const handleFileClick = (fileName: string) => { const fullPath = [...currentPath, fileName].join('/'); setSelectedFile({ id: fullPath, name: fileName, }); }; useEffect(() => { const fetchDirectoryStructure = async () => { try { setLoading(true); const response = await userService.getDirectoryStructure(docId, token); const data = await response.json(); if (data && data.directory_structure) { setDirectoryStructure(data.directory_structure); } else { setError('Invalid response format'); } } catch (err) { setError('Failed to load directory structure'); console.error(err); } finally { setLoading(false); } }; if (docId) { fetchDirectoryStructure(); } }, [docId, token]); const formatBytes = (bytes: number): string => { if (bytes === 0) return '0 Bytes'; const k = 1024; const sizes = ['Bytes', 'KB', 'MB', 'GB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; }; const navigateToDirectory = (dirName: string) => { setCurrentPath((prev) => [...prev, dirName]); }; const navigateUp = () => { setCurrentPath((prev) => prev.slice(0, -1)); }; const getCurrentDirectory = (): DirectoryStructure => { if (!directoryStructure) return {}; let current: any = directoryStructure; for (const dir of currentPath) { if (current[dir] && !current[dir].type) { current = current[dir]; } else { return {}; } } return current; }; const handleBackNavigation = () => { if (currentPath.length === 0) { if (onBackToDocuments) { onBackToDocuments(); } } else { navigateUp(); } }; const getMenuRef = (itemId: string) => { if (!menuRefs.current[itemId]) { menuRefs.current[itemId] = React.createRef(); } return menuRefs.current[itemId]; }; const handleMenuClick = (e: React.MouseEvent, itemId: string) => { e.preventDefault(); e.stopPropagation(); if (activeMenuId === itemId) { setActiveMenuId(null); return; } setActiveMenuId(itemId); }; const getActionOptions = ( name: string, isFile: boolean, itemId: string, ): MenuOption[] => { const options: MenuOption[] = []; if (isFile) { options.push({ icon: EyeView, label: t('settings.documents.view'), onClick: (event: React.SyntheticEvent) => { event.stopPropagation(); handleFileClick(name); }, iconWidth: 18, iconHeight: 18, variant: 'primary', }); } options.push({ icon: Trash, label: t('convTile.delete'), onClick: (event: React.SyntheticEvent) => { event.stopPropagation(); console.log('Delete item:', name); // Delete action will be implemented later }, iconWidth: 18, iconHeight: 18, variant: 'danger', }); return options; }; const renderPathNavigation = () => { return (
source {sourceName} {currentPath.length > 0 && ( <> / {currentPath.map((dir, index) => ( {dir} {index < currentPath.length - 1 && ( / )} ))} )}
); }; const calculateDirectoryStats = ( structure: DirectoryStructure, ): { totalSize: number; totalTokens: number } => { let totalSize = 0; let totalTokens = 0; Object.entries(structure).forEach(([_, node]) => { if (node.type) { // It's a file totalSize += node.size_bytes || 0; totalTokens += node.token_count || 0; } else { // It's a directory, recurse const stats = calculateDirectoryStats(node); totalSize += stats.totalSize; totalTokens += stats.totalTokens; } }); return { totalSize, totalTokens }; }; const renderFileTree = (structure: DirectoryStructure): React.ReactNode[] => { // Separate directories and files const entries = Object.entries(structure); const directories = entries.filter(([_, node]) => !node.type); const files = entries.filter(([_, node]) => node.type); // Create parent directory row const parentRow = currentPath.length > 0 ? [
Parent folder ..
- - , ] : []; // Render directories first, then files return [ ...parentRow, ...directories.map(([name, node]) => { const itemId = `dir-${name}`; const menuRef = getMenuRef(itemId); const dirStats = calculateDirectoryStats(node as DirectoryStructure); return ( navigateToDirectory(name)} >
Folder {name}
{dirStats.totalTokens > 0 ? dirStats.totalTokens.toLocaleString() : '-'} {dirStats.totalSize > 0 ? formatBytes(dirStats.totalSize) : '-'}
setActiveMenuId(isOpen ? itemId : null) } options={getActionOptions(name, false, itemId)} anchorRef={menuRef} position="bottom-left" offset={{ x: 0, y: 8 }} />
); }), ...files.map(([name, node]) => { const itemId = `file-${name}`; const menuRef = getMenuRef(itemId); return ( handleFileClick(name)} >
File {name}
{node.token_count?.toLocaleString() || '-'} {node.size_bytes ? formatBytes(node.size_bytes) : '-'}
setActiveMenuId(isOpen ? itemId : null) } options={getActionOptions(name, true, itemId)} anchorRef={menuRef} position="bottom-left" offset={{ x: 0, y: 8 }} />
); }), ]; }; const currentDirectory = getCurrentDirectory(); return ( <> {selectedFile ? ( setSelectedFile(null)} path={selectedFile.id} /> ) : (
{renderPathNavigation()}
{renderFileTree(currentDirectory)}
Name Tokens Size Actions
)} ); }; export default FileTreeComponent;