AVATAR_SELECTOR_COMPONENT.md

Overview

The AvatarSelector component provides a comprehensive interface for selecting and managing avatar images with support for both predefined avatar galleries and custom URL inputs. It features API-driven avatar loading, image validation, and fallback handling.

Purpose

  • Avatar Management: Select from gallery or custom URLs for user/assistant avatars
  • Image Validation: Handle image loading errors with fallback mechanisms
  • Gallery Integration: API-driven avatar gallery with grid-based selection
  • Custom Images: Support for external image URLs with validation

Key Features

  • API Integration: Fetches available avatars from /api/avatars endpoint
  • Grid Layout: Responsive 4-column grid for avatar selection
  • Visual Selection: Clear selection indicators with check marks
  • Image Validation: Error handling for broken/missing images

Custom URL Support

  • URL Input: Text input for custom avatar URLs
  • Real-time Validation: Immediate validation and preview
  • Fallback Handling: Automatic fallback to default avatar on errors
  • URL State Management: Tracks custom vs gallery avatar usage

Component Interface

interface AvatarSelectorProps {
  currentAvatar: string;                    // Current avatar URL
  onAvatarChange: (avatar: string) => void; // Avatar change callback
  className?: string;                       // Additional CSS classes
}

Core Functionality

// API-driven avatar loading
useEffect(() => {
  const fetchAvatars = async () => {
    try {
      setLoading(true);
      setError(null);
      const response = await fetch('/api/avatars');
      if (!response.ok) {
        throw new Error('Failed to fetch avatars');
      }
      const data = await response.json();
      setAvailableAvatars(data.avatars);
    } catch (err) {
      console.error('Error fetching avatars:', err);
      setError('Failed to load avatars');
    } finally {
      setLoading(false);
    }
  };
  fetchAvatars();
}, []);

// Gallery selection handler
const handleAvatarSelect = (avatarFilename: string) => {
  const fullPath = `/images/avatars/${avatarFilename}`;
  onAvatarChange(fullPath);
  setCustomUrl(fullPath);
  setUseCustomUrl(false);
  setOpen(false);
};

2. Avatar Grid Rendering

// Gallery grid with selection indicators
<ScrollArea className="h-[300px] rounded-md border p-4">
  <div className="grid grid-cols-4 gap-4">
    {availableAvatars.map((avatarFile) => {
      const avatarPath = `/images/avatars/${avatarFile}`;
      const isSelected = currentAvatar === avatarPath;
      
      return (
        <button
          key={avatarFile}
          onClick={() => handleAvatarSelect(avatarFile)}
          className={cn(
            "relative aspect-square rounded-lg overflow-hidden border-2 transition-all",
            isSelected 
              ? "border-primary ring-2 ring-primary ring-offset-2" 
              : "border-transparent hover:border-muted-foreground/50"
          )}
        >
          <img
            src={avatarPath}
            alt={avatarFile}
            className="w-full h-full object-cover"
            onError={(e) => {
              const img = e.target as HTMLImageElement;
              img.style.display = 'none';
            }}
          />
          {isSelected && (
            <div className="absolute inset-0 bg-primary/20 flex items-center justify-center">
              <Check className="h-6 w-6 text-primary" />
            </div>
          )}
          <div className="absolute bottom-0 left-0 right-0 bg-black/70 text-white text-xs p-1 truncate">
            {avatarFile.replace(/\.(jpg|jpeg|png|svg)$/i, '')}
          </div>
        </button>
      );
    })}
  </div>
</ScrollArea>

3. Custom URL Handling

// Custom URL input and validation
const handleCustomUrlSubmit = () => {
  if (customUrl && customUrl.trim()) {
    onAvatarChange(customUrl.trim());
    setUseCustomUrl(true);
    setOpen(false);
  }
};

// Custom URL interface
<div className="space-y-2">
  <h3 className="text-sm font-medium">Or use a custom URL:</h3>
  <div className="flex gap-2">
    <input
      type="text"
      value={customUrl}
      onChange={(e) => setCustomUrl(e.target.value)}
      placeholder="https://example.com/avatar.png"
      className="flex-1 px-3 py-2 border rounded-md text-sm"
    />
    <Button 
      onClick={handleCustomUrlSubmit}
      disabled={!customUrl || !customUrl.trim()}
      size="sm"
    >
      Use URL
    </Button>
  </div>
</div>

4. Image Error Handling

// Avatar display with fallback
<img
  src={currentAvatar || '/images/avatars/aitana_help.jpeg'}
  alt="Current avatar"
  className="w-full h-full object-cover"
  onError={(e) => {
    const img = e.target as HTMLImageElement;
    img.src = '/images/avatars/aitana_help.jpeg';
  }}
/>

// Gallery image error handling
<img
  src={avatarPath}
  alt={avatarFile}
  className="w-full h-full object-cover"
  onError={(e) => {
    const img = e.target as HTMLImageElement;
    img.style.display = 'none';
  }}
/>

State Management

// Component state variables
const [open, setOpen] = useState(false);                    // Dialog open state
const [customUrl, setCustomUrl] = useState(currentAvatar);  // Custom URL input
const [useCustomUrl, setUseCustomUrl] = useState(!currentAvatar.includes('/images/avatars/')); // URL vs gallery
const [availableAvatars, setAvailableAvatars] = useState<string[]>([]); // Gallery avatars
const [loading, setLoading] = useState(true);               // Loading state
const [error, setError] = useState<string | null>(null);    // Error state

Integration Examples

Basic Usage

function UserProfile() {
  const [avatar, setAvatar] = useState('/images/avatars/default.jpg');
  
  return (
    <div className="profile-form">
      <AvatarSelector
        currentAvatar={avatar}
        onAvatarChange={setAvatar}
      />
    </div>
  );
}

Assistant Creation

function AssistantForm() {
  const [assistantData, setAssistantData] = useState({
    name: '',
    avatar: '/images/avatars/aitana_help.jpeg',
    description: ''
  });

  return (
    <form>
      <AvatarSelector
        currentAvatar={assistantData.avatar}
        onAvatarChange={(avatar) => 
          setAssistantData(prev => ({ ...prev, avatar }))
        }
        className="mb-4"
      />
    </form>
  );
}

API Integration

Expected Endpoint Response

// GET /api/avatars
{
  "avatars": [
    "aitana_help.jpeg",
    "assistant1.png", 
    "user_default.jpg",
    // ... more avatar filenames
  ]
}

Error Handling

// Loading state
{loading && (
  <div className="flex items-center justify-center h-[300px] border rounded-md">
    <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary" />
    <p className="text-sm text-muted-foreground">Loading avatars...</p>
  </div>
)}

// Error state
{error && (
  <div className="flex items-center justify-center h-[300px] border rounded-md">
    <div className="text-center">
      <p className="text-sm text-destructive mb-2">{error}</p>
      <p className="text-xs text-muted-foreground">Please try again later</p>
    </div>
  </div>
)}

Features

Visual Design

  • Current Avatar Display: Circular preview with border
  • Selection Indicators: Primary color rings and check marks
  • Hover Effects: Subtle border changes on gallery items
  • Grid Layout: Responsive 4-column layout for avatars
  • Loading States: Spinner animation during API calls

Accessibility

  • Keyboard Navigation: Full keyboard support for selection
  • Screen Reader Support: Proper alt attributes and labels
  • Focus Management: Logical tab order through interface
  • Error Announcements: Screen reader accessible error messages

Performance

  • Lazy Loading: Images loaded as needed in gallery
  • Error Boundaries: Graceful handling of image failures
  • API Optimization: Single fetch for avatar gallery
  • State Optimization: Minimal re-renders through proper state management