VERTEX_AI_SEARCH_WIDGET_COMPONENT.md

Overview

The VertexAISearchWidget component is a sophisticated integration with Google’s Vertex AI Search service that provides users with the ability to search through knowledge bases and document collections. It handles complex authentication flows, token management, and third-party script integration to deliver a seamless search experience.

Purpose

  • AI-Powered Search: Integrate Google Vertex AI Search capabilities into the application
  • Authentication Management: Handle complex token-based authentication with auto-refresh
  • Third-Party Integration: Safely load and manage external Google search widgets
  • User Experience: Provide intuitive search interface with proper loading and error states

Key Features

Authentication Integration

  • Firebase Authentication: Integrates with Firebase auth for user verification
  • Google Token Management: Manages Google Cloud access tokens with automatic refresh
  • Token Persistence: Maintains tokens across component re-renders and config changes
  • Authentication Error Handling: Graceful handling of authentication failures

Third-Party Script Management

  • Dynamic Script Loading: Loads Google’s search widget script on-demand
  • Custom Element Registration: Handles custom web component integration
  • Script Error Handling: Manages script loading failures and retries

Search Widget Integration

  • Configuration Management: Supports dynamic config ID changes
  • Widget Lifecycle: Properly initializes and manages widget state
  • Token Application: Applies authentication tokens to the search widget
  • Trigger Integration: Connects custom trigger buttons to the search widget

State Management

  • Complex State Coordination: Manages multiple interdependent states
  • Reference Management: Uses refs for direct DOM manipulation and state persistence
  • Effect Coordination: Coordinates multiple useEffect hooks for proper initialization

Component Interface

interface VertexSearchWidgetProps {
  configId: string;           // Vertex AI Search configuration ID
  buttonText?: string | null; // Custom button text (default: "Search Knowledge Base")
  className?: string;         // Additional CSS classes
  iconOnly?: boolean;         // Show only icon without text
}

// Custom element declaration for TypeScript
declare global {
  namespace JSX {
    interface IntrinsicElements {
      'gen-search-widget': React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement> & {
        configId?: string;
        location?: string;
        triggerId?: string;
        authToken?: string;
        ref?: React.Ref<HTMLElement>;
      }, HTMLElement>;
    }
  }
}

// Extended widget element interface
interface VertexSearchWidgetElement extends HTMLElement {
  authToken?: string;
}

Core Functionality

1. Authentication Token Management

// Token fetching with Firebase authentication
const fetchSearchToken = async (user: User) => {
  // Prevent multiple concurrent requests
  if (tokenRequestInProgress.current) {
    debugLog('Token request already in progress, skipping');
    return;
  }
  
  tokenRequestInProgress.current = true;
  debugLog(`Fetching token for user: ${user.email}`);
  
  try {
    setIsLoading(true);
    setError(null);
    
    // Get Firebase ID token
    const idToken = await user.getIdToken();
    
    // Call API endpoint
    const response = await fetch('/api/aisearch', {
      method: 'GET',
      headers: {
        'Authorization': `Bearer ${idToken}`
      }
    });
    
    if (!response.ok) {
      const errorText = await response.text();
      console.error(`Vertex AI Search authentication error: ${response.status} ${errorText}`);
      setError('search-disabled');
      return;
    }
    
    const data = await response.json();
    
    // Store token regardless of widget status
    if (data.accessToken) {
      latestToken.current = data.accessToken;
      
      // Apply token if widget exists
      if (widgetRef.current) {
        widgetRef.current.authToken = data.accessToken;
      }
      
      // Set up refresh timer
      setupTokenRefresh(user, data.expiresIn);
    }
  } catch (error) {
    console.error(`Vertex AI Search token fetch error: ${error}`);
    setError('search-disabled');
  } finally {
    setIsLoading(false);
    tokenRequestInProgress.current = false;
  }
};

2. Automatic Token Refresh

// Set up automatic token refresh before expiration
const setupTokenRefresh = (user: User, expiresIn: number) => {
  if (tokenRefreshTimer) {
    clearTimeout(tokenRefreshTimer);
  }
  
  // Refresh 5 minutes before expiration
  const refreshTime = (expiresIn || 3600) * 1000 - 5 * 60 * 1000;
  const refreshTimer = setTimeout(() => {
    debugLog('Token refresh timer triggered');
    tokenRequestInProgress.current = false; // Reset flag before refreshing
    fetchSearchToken(user);
  }, refreshTime);
  
  setTokenRefreshTimer(refreshTimer);
};

3. Dynamic Script Loading

// Load Google's search widget script
useEffect(() => {
  if (!scriptLoaded.current) {
    debugLog('Loading widget script...');
    const script = document.createElement('script');
    script.src = 'https://cloud.google.com/ai/gen-app-builder/client?hl=en_US';
    script.async = true;
    script.onload = () => {
      debugLog('Widget script loaded successfully');
      scriptLoaded.current = true;
    };
    script.onerror = (error) => {
      debugLog(`Error loading widget script: ${error}`);
    };
    document.body.appendChild(script);
    
    return () => {
      if (document.body.contains(script)) {
        document.body.removeChild(script);
      }
    };
  }
}, []);

4. Configuration Change Handling

// Handle dynamic config ID changes
useEffect(() => {
  // Check if configId has changed
  if (configId !== previousConfigId.current) {
    debugLog(`ConfigId changed from ${previousConfigId.current} to ${configId}`);
    previousConfigId.current = configId;
    
    // Reset widget by recreating it
    if (widgetRef.current) {
      debugLog('Resetting widget due to configId change');
      
      // Remove configId and save a reference to the element
      const widgetElement = widgetRef.current;
      widgetElement.removeAttribute('configId');
      
      // Apply token if available
      if (latestToken.current) {
        debugLog('Reapplying token to reset widget');
        widgetElement.authToken = latestToken.current;
      }
      
      // Short delay before adding the new configId
      setTimeout(() => {
        if (widgetElement) {
          debugLog(`Setting new configId: ${configId}`);
          widgetElement.setAttribute('configId', configId);
        }
      }, 100);
    }
  }
}, [configId]);

5. Firebase Authentication Integration

// Monitor Firebase authentication state
useEffect(() => {
  const unsubscribe = FirebaseService.onAuthStateChange((user) => {
    debugLog(`Auth state changed, user: ${user ? `${user.email} (logged in)` : 'not logged in'}`);
    setIsAuthenticated(!!user);
    setCurrentUser(user);
    
    // Reset setup flag on auth change
    hasSetupWidget.current = false;
    
    // Try to immediately set up widget if user is available
    if (user) {
      debugLog('User authenticated, scheduling immediate token fetch');
      // Force token fetch on auth change
      setTimeout(() => {
        if (!tokenRequestInProgress.current) {
          debugLog('Delayed token fetch after auth change');
          fetchSearchToken(user);
        }
      }, 100); // Short delay to ensure component updates complete
    }
  });
  
  return () => {
    debugLog('Cleaning up auth state monitor');
    unsubscribe();
  };
}, []);

6. Widget Reference Management

// Complex widget reference management with token application
<gen-search-widget
  ref={(el: VertexSearchWidgetElement | null) => {
    debugLog(`Widget ref callback: element ${el ? 'exists' : 'is null'}`);
    if (el !== widgetRef.current) {
      widgetRef.current = el;
      debugLog('Updated widget ref');
      
      // Apply token if we already have one
      if (el && latestToken.current) {
        debugLog('Applying token to newly referenced widget');
        
        // Remove the configId first, then set the token, then re-add the configId
        // This forces the widget to start fresh with the token already set
        const currentConfigId = el.getAttribute('configId');
        el.removeAttribute('configId');
        
        // Apply token
        el.authToken = latestToken.current;
        
        // Wait a moment for the token to be processed, then re-add configId
        setTimeout(() => {
          if (el && currentConfigId) {
            debugLog('Re-adding configId after token was set');
            el.setAttribute('configId', currentConfigId);
          }
        }, 100);
      }
    }
  }}
  configId={configId}
  location="eu"
  triggerId="searchWidgetTrigger">
</gen-search-widget>

State Management

Component State Variables

// Authentication state
const [isAuthenticated, setIsAuthenticated] = useState(false);
const [currentUser, setCurrentUser] = useState<User | null>(null);

// Loading and error states
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);

// Timer management
const [tokenRefreshTimer, setTokenRefreshTimer] = useState<NodeJS.Timeout | null>(null);

// Refs for direct DOM manipulation and state persistence
const searchButtonRef = useRef<HTMLButtonElement | null>(null);
const widgetRef = useRef<VertexSearchWidgetElement | null>(null);
const scriptLoaded = useRef(false);
const tokenRequestInProgress = useRef(false);
const latestToken = useRef<string | null>(null);
const hasSetupWidget = useRef(false);
const previousConfigId = useRef<string>(configId);

Complex Effect Coordination

// Effect for initialization and setup
useEffect(() => {
  debugLog(`Setup effect running: authenticated=${isAuthenticated}, scriptLoaded=${scriptLoaded.current}, hasSetupWidget=${hasSetupWidget.current}`);
  
  // Check if we can initialize
  if (isAuthenticated && currentUser && scriptLoaded.current && !hasSetupWidget.current) {
    debugLog('All conditions met for widget setup');
    hasSetupWidget.current = true;
    
    // Force fetch token
    debugLog('Triggering initial token fetch');
    fetchSearchToken(currentUser);
  }
}, [isAuthenticated, currentUser, scriptLoaded.current]);

// Effect to apply token when widget becomes available
useEffect(() => {
  debugLog(`Widget ref effect: widgetRef=${!!widgetRef.current}, latestToken=${!!latestToken.current}`);
  if (widgetRef.current && latestToken.current) {
    debugLog('Applying stored token to widget from ref effect');
    widgetRef.current.authToken = latestToken.current;
  }
}, [widgetRef.current]);

UI States and Rendering

Authentication Required State

// If user is not authenticated, show login prompt
if (!isAuthenticated) {
  return (
    <div className="flex flex-col items-center justify-center p-2 border border-gray-200 rounded-lg">
      <p className="mb-2 text-center text-sm">Sign in to access search</p>
      <Button 
        variant="outline"
        size="sm"
        onClick={() => FirebaseService.loginWithGoogle()}
        className="w-full"
      >
        Sign in with Google
      </Button>
    </div>
  );
}

Loading State

// Show loading state
if (isLoading && !latestToken.current) {
  return (
    <div className="flex items-center justify-center p-2">
      <div className="animate-spin rounded-full h-4 w-4 border-b-2 border-gray-900"></div>
      <span className="ml-2 text-sm">Loading search...</span>
    </div>
  );
}

Error State

// Show disabled state on error
if (error === 'search-disabled') {
  return (
    <Button
      variant="ghost"
      size="icon"
      disabled={true}
      className={`h-10 w-10 flex items-center justify-center border rounded-lg opacity-50 ${className}`}
      title="Search unavailable - authentication error"
    >
      <Search className="h-4 w-4" />
    </Button>
  );
}

Active Search Widget

// Main widget interface
return (
  <div className="search-widget-container">
    {/* Search button that triggers the widget */}
    <Button
      ref={searchButtonRef}
      id="searchWidgetTrigger"
      variant="ghost"
      size="icon"
      className={`h-10 w-10 flex items-center justify-center border rounded-lg ${className}`}
    >
      <Search className="h-4 w-4" />
      {!iconOnly && buttonText && <span className="ml-2">{buttonText}</span>}
    </Button>
    
    {/* The actual widget element */}
    {isAuthenticated && (
      <gen-search-widget
        ref={widgetRefCallback}
        configId={configId}
        location="eu"
        triggerId="searchWidgetTrigger">
      </gen-search-widget>
    )}
  </div>
);

Integration Examples

Basic Usage

// Simple search widget integration
function DocumentSearchPanel() {
  return (
    <div className="search-panel">
      <h3>Search Knowledge Base</h3>
      <VertexSearchWidget
        configId="your-vertex-ai-config-id"
        buttonText="Search Documents"
      />
    </div>
  );
}

Icon-Only Integration

// Compact icon-only version for toolbars
function ToolbarSearch() {
  return (
    <div className="toolbar">
      <VertexSearchWidget
        configId="your-config-id"
        iconOnly={true}
        className="toolbar-search"
      />
    </div>
  );
}

Dynamic Configuration

// Dynamic config based on user context
function ContextualSearch({ userRole, department }: { userRole: string, department: string }) {
  const getConfigId = (role: string, dept: string) => {
    // Return different config IDs based on user context
    if (role === 'admin') return 'admin-search-config';
    if (dept === 'legal') return 'legal-docs-config';
    return 'general-search-config';
  };

  return (
    <VertexSearchWidget
      configId={getConfigId(userRole, department)}
      buttonText={`Search ${department} Resources`}
    />
  );
}

Integration with Tool Context

// Integration with tool configuration system
function ConfigurableSearch() {
  const { toolConfigs } = useToolContext();
  const searchConfig = toolConfigs['vertex_search'] || {};
  
  if (!searchConfig.configId) {
    return (
      <div className="p-4 text-center text-gray-500">
        Search not configured. Please set up Vertex AI Search in admin settings.
      </div>
    );
  }

  return (
    <VertexSearchWidget
      configId={searchConfig.configId}
      buttonText={searchConfig.buttonText || "Search Knowledge Base"}
    />
  );
}

API Integration

Backend Authentication Endpoint

// Expected API endpoint structure
// GET /api/aisearch
// Headers: Authorization: Bearer <firebase-id-token>
// Response:
{
  "accessToken": "google-cloud-access-token",
  "expiresIn": 3600, // seconds
  "tokenType": "Bearer"
}

Error Handling

// API error responses
// 401 Unauthorized - Firebase token invalid
// 403 Forbidden - User not authorized for Vertex AI
// 500 Internal Server Error - Google Cloud authentication failed

// Local development setup error
console.error(`To fix locally, run: gcloud auth application-default login`);

Debugging and Logging

Debug Logging System

// Comprehensive debug logging
const debugLog = (message: string) => {
  console.log(`[VertexSearch] ${message}`);
};

// Usage throughout component
debugLog('ConfigId changed from ${previousConfigId.current} to ${configId}');
debugLog('All conditions met for widget setup');
debugLog('Applying stored token to widget from ref effect');

Common Debug Scenarios

  1. Script Loading Issues: Check browser console for script load errors
  2. Authentication Failures: Verify Firebase authentication and Google Cloud credentials
  3. Token Refresh Problems: Monitor timer setup and refresh cycles
  4. Widget Initialization: Track widget ref callbacks and token application
  5. Config Changes: Verify proper widget reset on configuration updates

Performance Considerations

  1. Script Caching: Google’s widget script is loaded once and cached
  2. Token Persistence: Tokens are stored in refs to survive re-renders
  3. Debounced Initialization: Prevents multiple simultaneous token requests
  4. Efficient Re-renders: Uses refs for values that don’t need to trigger re-renders
  5. Timer Management: Proper cleanup of timeout timers on unmount

Security Considerations

  1. Token Management: Access tokens are handled securely and auto-refreshed
  2. Authentication Validation: Proper Firebase ID token validation
  3. HTTPS Required: Google Cloud integration requires HTTPS in production
  4. Cross-Origin Security: Widget script loaded from trusted Google domain
  5. Token Expiration: Automatic refresh prevents expired token usage

Error Handling

Authentication Errors

  • Firebase Auth Failures: Graceful handling with login prompts
  • Google Cloud Auth Issues: Disable search with helpful error messages
  • Token Refresh Failures: Retry logic with exponential backoff

Script Loading Errors

  • Network Issues: Retry mechanism for script loading
  • CORS Problems: Proper error messages for development environments
  • Script Failures: Fallback UI when widget script fails to load

Widget Integration Errors

  • Element Creation Failures: Proper error boundaries around widget elements
  • Token Application Issues: Retry logic for token application
  • Configuration Errors: Validation of config IDs and parameters

Browser Compatibility

Supported Browsers

  • Chrome: Full support (recommended)
  • Firefox: Full support with modern versions
  • Safari: Requires recent versions for custom elements
  • Edge: Chromium-based versions supported

Polyfills Required

  • Custom Elements: For older browser support
  • Promise: For token management in legacy browsers
  • Fetch: For API communication in older browsers