import React, { useState, useCallback, useEffect, useMemo } from 'react'; import { Dialog, DialogTitle, DialogContent, DialogActions, Button, Grid, Box, Typography, TextField, InputAdornment, CircularProgress, Card, CardMedia, CardContent, IconButton, Alert, Tooltip, Stack, } from '@mui/material'; import { Search, Close, CheckCircle, Favorite, FavoriteBorder, Collections, } from '@mui/icons-material'; import { useContentAssets, ContentAsset } from '../../hooks/useContentAssets'; import { fetchMediaBlobUrl } from '../../utils/fetchMediaBlobUrl'; export interface AssetLibraryImageModalProps { open: boolean; onClose: () => void; onSelect: (asset: ContentAsset) => void; title?: string; sourceModule?: string | string[]; // Optional filter by source module(s) (e.g., 'youtube_creator', 'podcast_maker', or ['youtube_creator', 'podcast_maker']) allowFavoritesOnly?: boolean; // Optional favorites-only filter toggle } /** * Reusable modal to browse and pick images from the Asset Library. * Image-only, with search and optional favorites/source filtering. */ export const AssetLibraryImageModal: React.FC = ({ open, onClose, onSelect, title = 'Select Image from Asset Library', sourceModule, allowFavoritesOnly = false, }) => { const [searchQuery, setSearchQuery] = useState(''); const [selectedAsset, setSelectedAsset] = useState(null); const [page, setPage] = useState(0); const [favoritesOnly, setFavoritesOnly] = useState(false); const [imageBlobUrls, setImageBlobUrls] = useState>(new Map()); const [loadingImages, setLoadingImages] = useState>(new Set()); const pageSize = 24; // Filter for images only const filters = { asset_type: 'image' as const, source_module: sourceModule, search: searchQuery || undefined, favorites_only: allowFavoritesOnly && favoritesOnly ? true : undefined, limit: pageSize, offset: page * pageSize, }; const { assets, loading, error, total, toggleFavorite, refetch } = useContentAssets(filters); // Check if a URL requires authentication (internal API endpoints) const isAuthenticatedUrl = 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 (!open || assets.length === 0) { // Clean up blob URLs when modal closes or no assets setImageBlobUrls(prev => { prev.forEach((url) => { if (url.startsWith('blob:')) { URL.revokeObjectURL(url); } }); return new Map(); }); setLoadingImages(new Set()); return; } const loadBlobUrls = async () => { const newBlobUrls = new Map(); const newLoadingImages = new Set(); for (const asset of assets) { if (!asset.file_url) continue; // Check if this is an authenticated endpoint 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(`[AssetLibraryImageModal] Failed to load image for asset ${asset.id}:`, err); } finally { newLoadingImages.delete(asset.id); } } else { // External URL, use directly newBlobUrls.set(asset.id, asset.file_url); } } setImageBlobUrls(prev => { // Clean up old blob URLs that are no longer needed prev.forEach((url, id) => { if (!newBlobUrls.has(id) && url.startsWith('blob:')) { URL.revokeObjectURL(url); } }); return newBlobUrls; }); setLoadingImages(newLoadingImages); }; loadBlobUrls(); // Cleanup function return () => { // Don't clean up here - let the next effect handle it }; }, [assets, open, isAuthenticatedUrl]); // Cleanup blob URLs on unmount useEffect(() => { return () => { imageBlobUrls.forEach((url) => { if (url.startsWith('blob:')) { URL.revokeObjectURL(url); } }); }; }, []); const handleSelect = useCallback(() => { if (selectedAsset) { onSelect(selectedAsset); handleClose(); } }, [selectedAsset, onSelect]); const handleClose = useCallback(() => { onClose(); setSelectedAsset(null); setSearchQuery(''); setPage(0); setFavoritesOnly(false); }, [onClose]); const handleAssetClick = useCallback((asset: ContentAsset) => { setSelectedAsset(asset); }, []); const handleFavoriteToggle = useCallback( async (assetId: number, e: React.MouseEvent) => { e.stopPropagation(); try { await toggleFavorite(assetId); refetch(); } catch (err) { console.error('Error toggling favorite:', err); } }, [toggleFavorite, refetch] ); return ( {title} {/* Search and Filters */} { setSearchQuery(e.target.value); setPage(0); }} InputProps={{ startAdornment: ( ), }} sx={{ '& .MuiOutlinedInput-root': { backgroundColor: '#ffffff', '& fieldset': { borderColor: '#d1d5db', }, }, }} /> {allowFavoritesOnly && ( )} {loading ? 'Loading...' : total > 0 ? `${total} image${total !== 1 ? 's' : ''} found` : 'No images found'} {/* Error State */} {error && ( {error} )} {/* Loading State */} {loading && assets.length === 0 ? ( ) : assets.length === 0 ? ( /* Empty State */ {searchQuery ? 'No images found matching your search.' : 'No images in your asset library yet.'} {searchQuery ? 'Try a different search term.' : 'Generate some images first to see them here.'} ) : ( /* Image Grid */ {assets.map((asset) => ( handleAssetClick(asset)} > {/* Image */} {loadingImages.has(asset.id) ? ( ) : ( { // Fallback if image fails to load const target = e.target as HTMLImageElement; target.style.display = 'none'; }} /> )} {/* Selected Indicator */} {selectedAsset?.id === asset.id && ( )} {/* Favorite Button */} handleFavoriteToggle(asset.id, e)} > {asset.is_favorite ? ( ) : ( )} {/* Title */} {asset.title && ( {asset.title} {asset.source_module && ( {asset.source_module.replace('_', ' ')} )} )} ))} {/* Load More (if needed) */} {total > (page + 1) * pageSize && ( )} )} ); };