import { useState, useEffect, useCallback, useMemo, useRef } from 'react'; import { useAuth } from '@clerk/clerk-react'; export interface ContentAsset { id: number; user_id: string; asset_type: 'text' | 'image' | 'video' | 'audio'; source_module: string; filename: string; file_url: string; file_path?: string; file_size?: number; mime_type?: string; title?: string; description?: string; prompt?: string; tags: string[]; asset_metadata: Record; provider?: string; model?: string; cost: number; generation_time?: number; is_favorite: boolean; download_count: number; share_count: number; created_at: string; updated_at: string; } export interface AssetFilters { asset_type?: 'text' | 'image' | 'video' | 'audio'; source_module?: string; search?: string; tags?: string[]; favorites_only?: boolean; limit?: number; offset?: number; } export interface AssetListResponse { assets: ContentAsset[]; total: number; limit: number; offset: number; } const API_BASE_URL = process.env.REACT_APP_API_BASE_URL || 'http://localhost:8000'; export const useContentAssets = (filters: AssetFilters = {}) => { const { getToken } = useAuth(); const [assets, setAssets] = useState([]); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [total, setTotal] = useState(0); const isFetchingRef = useRef(false); const abortControllerRef = useRef(null); // Memoize filters to create stable reference - only changes when actual values change const stableFilters = useMemo(() => { return { asset_type: filters.asset_type, source_module: filters.source_module, search: filters.search, tags: filters.tags, favorites_only: filters.favorites_only, limit: filters.limit, offset: filters.offset, }; }, [ filters.asset_type, filters.source_module, filters.search, filters.tags?.join(','), filters.favorites_only, filters.limit, filters.offset, ]); // Create stable filter key for comparison const filterKey = useMemo(() => { return JSON.stringify(stableFilters); }, [stableFilters]); // Store latest filters in ref for use in fetch function const filtersRef = useRef(stableFilters); useEffect(() => { filtersRef.current = stableFilters; }, [stableFilters]); // Fetch function - exposed for manual retry, not called automatically on errors const fetchAssets = useCallback(async () => { // Prevent concurrent fetches if (isFetchingRef.current) { return; } // Cancel any pending request if (abortControllerRef.current) { abortControllerRef.current.abort(); } // Create new abort controller for this request const abortController = new AbortController(); abortControllerRef.current = abortController; try { isFetchingRef.current = true; setLoading(true); setError(null); const token = await getToken(); if (!token) { setLoading(false); isFetchingRef.current = false; return; } // Use ref to get latest filters const currentFilters = filtersRef.current; const params = new URLSearchParams(); if (currentFilters.asset_type) params.append('asset_type', currentFilters.asset_type); if (currentFilters.source_module) params.append('source_module', currentFilters.source_module); if (currentFilters.search) params.append('search', currentFilters.search); if (currentFilters.tags && currentFilters.tags.length > 0) params.append('tags', currentFilters.tags.join(',')); if (currentFilters.favorites_only) params.append('favorites_only', 'true'); params.append('limit', String(currentFilters.limit || 100)); params.append('offset', String(currentFilters.offset || 0)); const response = await fetch(`${API_BASE_URL}/api/content-assets/?${params.toString()}`, { headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json', }, signal: abortController.signal, }); if (!response.ok) { if (response.status === 429) { setError('Rate limit exceeded. Please try again later.'); setAssets([]); setTotal(0); setLoading(false); isFetchingRef.current = false; return; } throw new Error(`Failed to fetch assets: ${response.statusText}`); } const data: AssetListResponse = await response.json(); setAssets(data.assets); setTotal(data.total); } catch (err) { // Don't set error for aborted requests if (err instanceof Error && err.name === 'AbortError') { return; } if (err instanceof TypeError && err.message.includes('fetch')) { setError('Network error. Please check your connection.'); } else { setError(err instanceof Error ? err.message : 'Failed to fetch assets'); } setAssets([]); } finally { setLoading(false); isFetchingRef.current = false; } }, [getToken]); // Only depend on getToken, use ref for filters // Fetch on mount and when filters change - but only once per filter change // NO automatic retry on errors - user must call refetch() manually useEffect(() => { fetchAssets(); // Cleanup: abort on unmount or filter change return () => { if (abortControllerRef.current) { abortControllerRef.current.abort(); } }; }, [filterKey, fetchAssets]); // Include fetchAssets but it's stable due to ref usage const toggleFavorite = useCallback(async (assetId: number) => { try { const token = await getToken(); if (!token) { throw new Error('Not authenticated'); } const response = await fetch(`${API_BASE_URL}/api/content-assets/${assetId}/favorite`, { method: 'POST', headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json', }, }); if (!response.ok) { throw new Error('Failed to toggle favorite'); } const data = await response.json(); // Update local state setAssets(prev => prev.map(asset => asset.id === assetId ? { ...asset, is_favorite: data.is_favorite } : asset ) ); return data.is_favorite; } catch (err) { console.error('Error toggling favorite:', err); throw err; } }, [getToken]); const deleteAsset = useCallback(async (assetId: number) => { try { const token = await getToken(); if (!token) { throw new Error('Not authenticated'); } const response = await fetch(`${API_BASE_URL}/api/content-assets/${assetId}`, { method: 'DELETE', headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json', }, }); if (!response.ok) { throw new Error('Failed to delete asset'); } // Remove from local state setAssets(prev => prev.filter(asset => asset.id !== assetId)); setTotal(prev => prev - 1); return true; } catch (err) { console.error('Error deleting asset:', err); throw err; } }, [getToken]); const trackUsage = useCallback(async (assetId: number, action: 'download' | 'share' | 'access') => { try { const token = await getToken(); if (!token) { return; } await fetch(`${API_BASE_URL}/api/content-assets/${assetId}/usage?action=${action}`, { method: 'POST', headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json', }, }); } catch (err) { console.error('Error tracking usage:', err); } }, [getToken]); const updateAsset = useCallback(async ( assetId: number, updates: { title?: string; description?: string; tags?: string[] } ) => { try { const token = await getToken(); if (!token) { throw new Error('Not authenticated'); } const body: any = {}; if (updates.title !== undefined) body.title = updates.title; if (updates.description !== undefined) body.description = updates.description; if (updates.tags !== undefined) body.tags = updates.tags; // Send as array, not comma-separated const response = await fetch(`${API_BASE_URL}/api/content-assets/${assetId}`, { method: 'PUT', headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json', }, body: JSON.stringify(body), }); if (!response.ok) { throw new Error('Failed to update asset'); } const updatedAsset = await response.json(); // Update local state setAssets(prev => prev.map(asset => asset.id === assetId ? { ...asset, ...updatedAsset } : asset ) ); return updatedAsset; } catch (err) { console.error('Error updating asset:', err); throw err; } }, [getToken]); return { assets, loading, error, total, refetch: fetchAssets, toggleFavorite, deleteAsset, updateAsset, trackUsage, }; };