FILE_BROWSER_DIALOG_COMPONENT.md
Overview
The FileBrowserDialog component is a sophisticated file management interface that provides users with the ability to browse, navigate, and select files and folders from Google Cloud Storage (GCS). It features pagination, multi-selection, and intelligent file filtering based on supported file types.
Purpose
- Cloud Storage Integration: Browse and access files from Google Cloud Storage buckets
- File Selection: Multi-select files and folders for assistant document processing
- Navigation: Hierarchical folder navigation with breadcrumb support
- Pagination: Efficient handling of large directories with token-based pagination
- File Type Filtering: Automatic filtering to show only supported file types
Key Features
Cloud Storage Integration
- GCS API Integration: Direct connection to Google Cloud Storage via custom API endpoint
- Bucket Configuration: Flexible bucket URL configuration through tool context
- Authentication: Secure API communication with proper error handling
Navigation System
- Folder Hierarchy: Navigate through folder structures with up/down navigation
- Path History: Maintains navigation history for back button functionality
- Breadcrumb Display: Shows current path location within the storage structure
File Management
- Multi-Selection: Select multiple files and folders simultaneously
- File Type Filtering: Only displays supported file extensions
- File Metadata: Displays file sizes, creation dates, and content types
- Visual Indicators: Different icons for files and folders
Pagination Support
- Token-Based Pagination: Efficient pagination using GCS page tokens
- Page Navigation: Forward and backward navigation through large directories
- Page Size Control: Configurable number of items per page (default: 50)
Component Interface
interface FileBrowserDialogProps {
open: boolean; // Dialog open/closed state
onOpenChange: (open: boolean) => void; // Dialog state change handler
onSelect: (items: FileSystemItem[]) => void; // File selection handler
selectedItems?: FileSystemItem[]; // Currently selected items
}
interface FileSystemItem {
path: string; // Full path to the file/folder
type: 'file' | 'folder'; // Item type
name: string; // Display name
}
// GCS object structure from API
interface GCSObject {
name: string; // Full object path
size?: string | number; // File size in bytes
contentType?: string; // MIME type
updated?: string; // Last modified timestamp
created?: string; // Creation timestamp
timeCreated?: string; // Alternative creation timestamp
crc32c?: string; // Checksum
md5Hash?: string; // MD5 hash
}
// Pagination state management
interface PaginationState {
pageSize: number; // Items per page
pageToken?: string; // Current page token
hasMore: boolean; // More pages available
currentPage: number; // Current page number
}
Core Functionality
1. Tool Configuration Integration
// Integration with ToolContext for configuration
const { toolConfigs } = useToolContext();
// Try document_search_agent first, then fall back to file-browser
const documentAgentConfig = toolConfigs['document_search_agent'] || {};
const fileConfig = toolConfigs['file-browser'] || {};
// Both tools now use 'bucketUrl' consistently
const bucketUrl = documentAgentConfig.bucketUrl || fileConfig.bucketUrl;
const rootPath = fileConfig.rootPath || '/';
// Determine which tool is providing the configuration
const activeToolName = documentAgentConfig.bucketUrl ? 'Document Search Agent' : 'File Browser';
2. Cloud Storage API Integration
// Load folder contents from GCS
const loadFolderContents = async (path: string, pageToken?: string) => {
setIsLoading(true);
setError(null);
try {
let normalizedPath = path;
if (normalizedPath.startsWith('/')) {
normalizedPath = normalizedPath.slice(1);
}
const response = await fetch('/api/gcs', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
bucket: bucketUrl,
prefix: normalizedPath === '/' ? '' : normalizedPath,
delimiter: '/',
maxResults: pagination.pageSize,
pageToken: pageToken
})
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.error || 'Failed to load storage contents');
}
const data = await response.json();
setFileSystem(data.contents);
// Update pagination state
setPagination(prev => ({
...prev,
pageToken: data.nextPageToken,
hasMore: !!data.nextPageToken
}));
} catch (err) {
console.error('Error loading folder contents:', err);
setError(err instanceof Error ? err.message : 'Failed to load storage contents');
} finally {
setIsLoading(false);
}
};
3. Navigation System
// Navigation state management
const [currentPath, setCurrentPath] = useState(rootPath);
const [pathHistory, setPathHistory] = useState<string[]>([rootPath]);
// Navigate into a folder
const navigateToFolder = (folderPath: string) => {
setPathHistory(prev => [...prev, currentPath]);
setCurrentPath(folderPath);
};
// Navigate back to parent folder
const navigateUp = () => {
const previousPath = pathHistory[pathHistory.length - 1];
if (previousPath) {
setPathHistory(prev => prev.slice(0, -1));
setCurrentPath(previousPath);
}
};
// Path navigation UI
{currentPath !== rootPath && (
<Button
variant="ghost"
size="sm"
onClick={navigateUp}
className="h-8 px-2"
>
<ArrowLeft className="h-4 w-4" />
</Button>
)}
<div className="flex-1 truncate">
Current path: {currentPath}
</div>
4. Pagination Implementation
// Pagination state and token management
const [pagination, setPagination] = useState<PaginationState>({
pageSize: 50,
hasMore: false,
currentPage: 1
});
// Token history to support backward navigation
const [pageTokenHistory, setPageTokenHistory] = useState<Map<number, string>>(new Map());
// Navigate to next page
const goToNextPage = () => {
if (pagination.hasMore) {
const nextPage = pagination.currentPage + 1;
setPagination(prev => ({ ...prev, currentPage: nextPage }));
// Use the stored token for the next page
const nextPageToken = pageTokenHistory.get(nextPage);
loadFolderContents(currentPath, nextPageToken);
}
};
// Navigate to previous page
const goToPreviousPage = () => {
if (pagination.currentPage > 1) {
const prevPage = pagination.currentPage - 1;
setPagination(prev => ({ ...prev, currentPage: prevPage }));
// For previous page, use the token for page - 1 or undefined for first page
const prevPageToken = prevPage > 1 ? pageTokenHistory.get(prevPage - 1) : undefined;
loadFolderContents(currentPath, prevPageToken);
}
};
5. File Selection System
// Selection state management
const isItemSelected = (path: string, type: 'file' | 'folder'): boolean => {
return selectedItems.some(item => item.path === path && item.type === type);
};
// Toggle item selection
const toggleSelection = (path: string, name: string, type: 'file' | 'folder', e?: React.MouseEvent) => {
if (e) {
e.stopPropagation(); // Prevent folder navigation when clicking checkbox
}
const isSelected = isItemSelected(path, type);
if (isSelected) {
onSelect(selectedItems.filter(item => !(item.path === path && item.type === type)));
} else {
onSelect([...selectedItems, { path, type, name }]);
}
};
// Selection display with badges
{selectedItems.length > 0 && (
<div className="flex flex-wrap gap-1 p-2 bg-gray-50 rounded flex-shrink-0">
{selectedItems.map(item => (
<Badge key={`${item.type}-${item.path}`} variant="secondary" className="flex items-center gap-1">
{item.type === 'folder' ? <Folder className="h-3 w-3" /> : <File className="h-3 w-3" />}
{item.name}
<span
className="ml-1 cursor-pointer hover:text-gray-700"
onClick={() => toggleSelection(item.path, item.name, item.type)}
>
×
</span>
</Badge>
))}
</div>
)}
6. File and Folder Rendering
// Group and display files and folders
const groupedContents = React.useMemo(() => {
const folders: GCSObject[] = [];
const files: GCSObject[] = [];
fileSystem.forEach(item => {
if (item.name.endsWith('/')) {
folders.push(item);
} else {
files.push(item);
}
});
return { folders, files };
}, [fileSystem]);
// Folder rendering
{groupedContents.folders.map((folder) => {
const name = folder.name.split('/').slice(-2)[0];
const isSelected = isItemSelected(folder.name, 'folder');
return (
<div
key={folder.name}
className="flex items-center p-2 rounded hover:bg-gray-100"
onClick={() => navigateToFolder(folder.name)}
>
<div className="flex items-center mr-2" onClick={(e) => e.stopPropagation()}>
<input
type="checkbox"
checked={isSelected}
onChange={(e) => toggleSelection(folder.name, name, 'folder')}
className="w-4 h-4 rounded border-gray-300"
/>
</div>
<Folder className="h-4 w-4 mr-2 text-blue-500" />
<span className="truncate flex-1">{name}</span>
</div>
);
})}
// File rendering
{groupedContents.files.map((file) => {
const name = file.name.split('/').pop() || '';
const isSelected = isItemSelected(file.name, 'file');
return (
<div
key={file.name}
className={`flex items-center p-2 rounded cursor-pointer hover:bg-gray-100 ${
isSelected ? 'bg-blue-50' : ''
}`}
onClick={() => toggleSelection(file.name, name, 'file')}
>
<File className="h-4 w-4 mr-2 text-gray-500" />
<span className="truncate flex-1">{name}</span>
<span className="text-xs text-gray-500">
{file.size && formatFileSize(Number(file.size))}
</span>
</div>
);
})}
7. File Size Formatting
// Utility function for human-readable file sizes
function formatFileSize(bytes: number): string {
if (bytes === 0) return '0 B';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(1))} ${sizes[i]}`;
}
// Date formatting for timestamps
function formatDate(dateString?: string): string {
if (!dateString) return '';
try {
return format(new Date(dateString), 'MMM d, yyyy HH:mm:ss');
} catch {
return dateString;
}
}
Configuration Requirements
Tool Configuration
// Required configuration in tool settings
{
"file-browser": {
"bucketUrl": "gs://your-bucket-name",
"rootPath": "/optional/root/path"
}
}
// Or via Document Search Agent
{
"document_search_agent": {
"bucketUrl": "gs://your-bucket-name"
}
}
Missing Configuration Handling
// Configuration validation and error display
if (!bucketUrl) {
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent>
<DialogHeader>
<DialogTitle>File Browser Configuration Required</DialogTitle>
</DialogHeader>
<Alert>
<span>Please configure the file browser or document search agent in the admin settings first.</span>
</Alert>
<div className="text-sm text-muted-foreground">
Configuration required for File Browser:
<ul className="list-disc ml-4 mt-2">
<li>Storage Bucket URL</li>
<li>Root Path (optional)</li>
</ul>
Or configure Document Search Agent with preset configurations.
</div>
</DialogContent>
</Dialog>
);
}
State Management
Component State Variables
// Core navigation state
const [currentPath, setCurrentPath] = useState(rootPath);
const [pathHistory, setPathHistory] = useState<string[]>([rootPath]);
// File system data
const [fileSystem, setFileSystem] = useState<GCSObject[]>([]);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
// Pagination state
const [pagination, setPagination] = useState<PaginationState>({
pageSize: 50,
hasMore: false,
currentPage: 1
});
// Token history for backward navigation
const [pageTokenHistory, setPageTokenHistory] = useState<Map<number, string>>(new Map());
Effect Hooks
// Load contents when dialog opens or path changes
useEffect(() => {
if (open && bucketUrl) {
// Reset pagination whenever the dialog opens or path changes
setPagination({
pageSize: 50,
hasMore: false,
currentPage: 1
});
setPageTokenHistory(new Map());
loadFolderContents(currentPath);
}
}, [open, bucketUrl, currentPath]);
Integration Examples
Basic File Selection
function DocumentUploadComponent() {
const [isDialogOpen, setIsDialogOpen] = useState(false);
const [selectedFiles, setSelectedFiles] = useState<FileSystemItem[]>([]);
return (
<div>
<Button onClick={() => setIsDialogOpen(true)}>
Browse Cloud Storage
</Button>
<FileBrowserDialog
open={isDialogOpen}
onOpenChange={setIsDialogOpen}
onSelect={setSelectedFiles}
selectedItems={selectedFiles}
/>
{selectedFiles.length > 0 && (
<div>
Selected {selectedFiles.length} items:
{selectedFiles.map(item => (
<div key={`${item.type}-${item.path}`}>
{item.type}: {item.name}
</div>
))}
</div>
)}
</div>
);
}
Integration with Assistant Configuration
function AssistantDocumentManager({ assistantId }: { assistantId: string }) {
const [documents, setDocuments] = useState<FileSystemItem[]>([]);
const [showBrowser, setShowBrowser] = useState(false);
const handleDocumentSelection = (items: FileSystemItem[]) => {
setDocuments(items);
setShowBrowser(false);
// Optionally save to assistant configuration
updateAssistantDocuments(assistantId, items);
};
return (
<div className="space-y-4">
<Button onClick={() => setShowBrowser(true)}>
Add Documents from Cloud Storage
</Button>
<FileBrowserDialog
open={showBrowser}
onOpenChange={setShowBrowser}
onSelect={handleDocumentSelection}
selectedItems={documents}
/>
<DocumentList documents={documents} />
</div>
);
}
Tool Configuration Context
// Integration with ToolContext for dynamic configuration
function FileManagementPage() {
const { toolConfigs, updateToolConfig } = useToolContext();
const [showBrowser, setShowBrowser] = useState(false);
// Check if file browser is configured
const isConfigured = toolConfigs['file-browser']?.bucketUrl ||
toolConfigs['document_search_agent']?.bucketUrl;
if (!isConfigured) {
return (
<div>
<p>File browser not configured. Please set up your cloud storage connection.</p>
<Button onClick={() => /* Open configuration */}>
Configure Cloud Storage
</Button>
</div>
);
}
return (
<div>
<Button onClick={() => setShowBrowser(true)}>
Browse Files
</Button>
<FileBrowserDialog
open={showBrowser}
onOpenChange={setShowBrowser}
onSelect={(items) => {
console.log('Selected items:', items);
setShowBrowser(false);
}}
/>
</div>
);
}
Error Handling
API Error Management
// Comprehensive error handling for API calls
try {
const response = await fetch('/api/gcs', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(requestData)
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.error || 'Failed to load storage contents');
}
const data = await response.json();
// Process successful response...
} catch (err) {
console.error('Error loading folder contents:', err);
setError(err instanceof Error ? err.message : 'Failed to load storage contents');
}
Error Display
// Error state rendering
{error ? (
<div className="text-red-500 p-4 text-center">
{error}
</div>
) : (
// Normal content rendering...
)}
Performance Optimizations
- Memoized Content Grouping: Uses
React.useMemofor expensive folder/file grouping - Pagination: Limits items per page to prevent UI performance issues
- Token-Based Navigation: Efficient pagination without re-loading previous pages
- Selective Re-renders: Proper state management to minimize unnecessary re-renders
- API Caching: Page tokens stored for efficient backward navigation
Accessibility Features
- Keyboard Navigation: All interactive elements are keyboard accessible
- Screen Reader Support: Proper ARIA labels and semantic markup
- Focus Management: Logical tab order and focus indicators
- Visual Indicators: Clear visual feedback for selection states
- Error Announcements: Error messages are announced to screen readers
Security Considerations
- API Authentication: Secure communication with GCS API endpoints
- Path Validation: Server-side validation of requested paths
- File Type Filtering: Client-side filtering of supported file types
- Error Sanitization: Safe display of error messages without exposing sensitive data
- Access Control: Bucket access controlled through proper authentication