import React, { useState, useEffect } from 'react'; import { Box, Stack, TextField, InputAdornment, RadioGroup, FormControlLabel, Radio, Typography, CircularProgress, Alert, Grid, Card, CardMedia, Button, IconButton } from '@mui/material'; import { Search as SearchIcon, Collections as CollectionsIcon, CheckCircle as CheckCircleIcon, ExpandMore as ExpandMoreIcon, Favorite as FavoriteIcon, FavoriteBorder as FavoriteBorderIcon } from '@mui/icons-material'; import { useContentAssets } from '../../hooks/useContentAssets'; import { fetchMediaBlobUrl } from '../../utils/fetchMediaBlobUrl'; interface AvatarAssetBrowserProps { onSelect: (url: string) => void; selectedUrl: string | null; } export const AvatarAssetBrowser: React.FC = ({ onSelect, selectedUrl }) => { const [filter, setFilter] = useState<'all' | 'favorites'>('all'); const [search, setSearch] = useState(''); const [imageBlobUrls, setImageBlobUrls] = useState>(new Map()); const [loadingImages, setLoadingImages] = useState>(new Set()); const [limit, setLimit] = useState(24); const { assets, loading, error, total, toggleFavorite, refetch } = useContentAssets({ asset_type: 'image', search: search || undefined, favorites_only: filter === 'favorites', limit: limit, }); // No-op useEffect to satisfy the linter if needed, but the actual fetch is handled by useContentAssets hook's internal useEffect // which runs when stableFilters change. // The user reported that images don't load on initial tab mount unless toggled. // useContentAssets's useEffect(fetchAssets, [filterKey, fetchAssets]) should handle it, // but if it's failing initially due to auth timing, this manual refetch helps. useEffect(() => { // Only refetch on mount to ensure initial load const timer = setTimeout(() => { refetch(); }, 200); // Slightly longer delay to ensure auth is fully ready return () => clearTimeout(timer); }, [refetch]); // Only run on mount or if refetch function changes // Check if a URL requires authentication (internal API endpoints) const isAuthenticatedUrl = React.useCallback((url: string): boolean => { if (!url) return false; return url.includes('/api/podcast/') || url.includes('/api/youtube/') || url.includes('/api/story/') || (url.startsWith('/') && !url.startsWith('//')); }, []); // Load blob URLs for authenticated images useEffect(() => { if (assets.length === 0) { setImageBlobUrls(new Map()); return; } const loadBlobUrls = async () => { const newBlobUrls = new Map(); const newLoadingImages = new Set(); for (const asset of assets) { if (!asset.file_url) continue; if (isAuthenticatedUrl(asset.file_url)) { newLoadingImages.add(asset.id); try { const blobUrl = await fetchMediaBlobUrl(asset.file_url); if (blobUrl) { newBlobUrls.set(asset.id, blobUrl); } } catch (err) { console.error(`Failed to load image for asset ${asset.id}:`, err); } finally { newLoadingImages.delete(asset.id); } } else { newBlobUrls.set(asset.id, asset.file_url); } } setImageBlobUrls(prev => { // Revoke old blobs that are no longer needed prev.forEach((url, id) => { if (url.startsWith('blob:') && !newBlobUrls.has(id)) URL.revokeObjectURL(url); }); return newBlobUrls; }); setLoadingImages(newLoadingImages); }; loadBlobUrls(); // Cleanup on unmount/change is handled by the effect below or next run }, [assets, isAuthenticatedUrl]); // Cleanup all blobs on unmount useEffect(() => { return () => { imageBlobUrls.forEach(url => { if (url.startsWith('blob:')) URL.revokeObjectURL(url); }); }; }, [imageBlobUrls]); const handleLoadMore = () => { setLimit(prev => prev + 24); }; return ( setSearch(e.target.value)} InputProps={{ startAdornment: ( ), }} /> setFilter(e.target.value as 'all' | 'favorites')} sx={{ flexShrink: 0, ml: 0.5, display: 'flex', flexWrap: 'nowrap', '& .MuiFormControlLabel-root': { mr: 0.5, ml: 0, '& .MuiTypography-root': { color: '#334155', fontWeight: 600, fontSize: '0.75rem', whiteSpace: 'nowrap' }, '& .MuiRadio-root': { p: 0.5, color: '#94a3b8', '&.Mui-checked': { color: '#667eea', } } } }} > } label="All" /> } label="Favs" /> {loading && assets.length === 0 ? ( ) : error ? ( {error} ) : assets.length === 0 ? ( {search ? 'No matches found' : 'No images in library'} ) : ( <> {assets.map((asset) => ( asset.file_url && onSelect(asset.file_url)} > {isAuthenticatedUrl(asset.file_url) && !imageBlobUrls.has(asset.id) ? ( ) : ( )} {loadingImages.has(asset.id) && ( )} { e.stopPropagation(); toggleFavorite(asset.id); }} sx={{ bgcolor: 'rgba(255,255,255,0.8)', '&:hover': { bgcolor: 'white' }, width: 24, height: 24, p: 0.5 }} > {asset.is_favorite ? : } {selectedUrl === asset.file_url && ( )} ))} {/* Load More Button */} {total > limit && ( )} )} ); };