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
  • 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

  1. Memoized Content Grouping: Uses React.useMemo for expensive folder/file grouping
  2. Pagination: Limits items per page to prevent UI performance issues
  3. Token-Based Navigation: Efficient pagination without re-loading previous pages
  4. Selective Re-renders: Proper state management to minimize unnecessary re-renders
  5. API Caching: Page tokens stored for efficient backward navigation

Accessibility Features

  1. Keyboard Navigation: All interactive elements are keyboard accessible
  2. Screen Reader Support: Proper ARIA labels and semantic markup
  3. Focus Management: Logical tab order and focus indicators
  4. Visual Indicators: Clear visual feedback for selection states
  5. Error Announcements: Error messages are announced to screen readers

Security Considerations

  1. API Authentication: Secure communication with GCS API endpoints
  2. Path Validation: Server-side validation of requested paths
  3. File Type Filtering: Client-side filtering of supported file types
  4. Error Sanitization: Safe display of error messages without exposing sensitive data
  5. Access Control: Bucket access controlled through proper authentication