// frontend/src/components/MainContent/MainContent.jsx import React, { useState, useEffect, useRef } from 'react'; // Import useRef import ProjectHeader from '../ProjectHeader/ProjectHeader.jsx'; import UrlCardList from '../UrlCardList/UrlCardList.jsx'; import UrlDetailPage from '../UrlDetailPage/UrlDetailPage.jsx'; import styles from './MainContent.module.css'; import { fetchProjectDetails, fetchProjectUrls, addUrlToProject, askAiAboutProject, deleteUrlFromProject, regenerateSummary, fetchUrlDetails // Import fetchUrlDetails for polling } from '../../services/api'; import { FaPlus, FaMagic } from 'react-icons/fa'; // --- Constants --- const POLLING_INTERVAL_MS = 5000; // Check every 5 seconds function MainContent({ currentProjectId }) { const [projectDetails, setProjectDetails] = useState(null); const [urls, setUrls] = useState([]); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); const [aiResponse, setAiResponse] = useState(''); const [detailUrlId, setDetailUrlId] = useState(null); // --- Polling State --- // Use useRef to store the interval ID so it doesn't trigger re-renders const pollIntervalRef = useRef(null); // --- End Polling State --- // Function to update a single URL in the state const updateSingleUrlState = (updatedUrlData) => { if (!updatedUrlData || !updatedUrlData.id) return; setUrls(currentUrls => { // Create a flag to see if an update actually happened let updated = false; const newUrls = currentUrls.map(url => { if (url.id === updatedUrlData.id) { // Only update if data has actually changed to avoid infinite loops // Compare relevant fields like status, title, summary if (url.processingStatus !== updatedUrlData.processingStatus || url.title !== updatedUrlData.title || url.summary !== updatedUrlData.summary) { updated = true; return { ...url, ...updatedUrlData, isLoading: false }; // Merge new data } } return url; }); // Only set state if an update occurred return updated ? newUrls : currentUrls; }); }; // Effect for initial data load when project changes useEffect(() => { setDetailUrlId(null); // Clear any existing polling interval when project changes if (pollIntervalRef.current) { clearInterval(pollIntervalRef.current); pollIntervalRef.current = null; } if (!currentProjectId) { setProjectDetails(null); setUrls([]); setIsLoading(false); setError(null); return; }; setIsLoading(true); setError(null); setAiResponse(''); Promise.all([ fetchProjectDetails(currentProjectId), fetchProjectUrls(currentProjectId) ]) .then(([details, urlsData]) => { setProjectDetails(details); setUrls(urlsData || []); setIsLoading(false); }) .catch(err => { console.error("MainContent: Failed to load project data:", err); // ... (error handling as before) ... setError(`Failed to load data for project ${currentProjectId}.`); setProjectDetails(null); setUrls([]); setIsLoading(false); }); // Cleanup function for when component unmounts or currentProjectId changes return () => { if (pollIntervalRef.current) { clearInterval(pollIntervalRef.current); pollIntervalRef.current = null; console.log("Polling interval cleared on project change/unmount."); } }; }, [currentProjectId]); // --- Effect for Polling Pending URLs --- useEffect(() => { const pendingUrls = urls.filter(url => url.processingStatus === 'pending'); if (pendingUrls.length > 0 && !pollIntervalRef.current) { // Start polling only if there are pending URLs and polling isn't already running console.log(`Polling started for ${pendingUrls.length} pending URL(s).`); pollIntervalRef.current = setInterval(async () => { console.log("Polling: Checking status of pending URLs..."); const currentPendingIds = urls .filter(u => u.processingStatus === 'pending') .map(u => u.id); if (currentPendingIds.length === 0) { console.log("Polling: No more pending URLs, stopping interval."); clearInterval(pollIntervalRef.current); pollIntervalRef.current = null; return; } // Fetch details for each pending URL // Using Promise.allSettled to handle individual fetch failures const results = await Promise.allSettled( currentPendingIds.map(id => fetchUrlDetails(id)) ); let anyUpdates = false; results.forEach((result, index) => { const urlId = currentPendingIds[index]; if (result.status === 'fulfilled') { const updatedData = result.value; // Check if the status is no longer pending if (updatedData && updatedData.processingStatus !== 'pending') { console.log(`Polling: URL ${urlId} status updated to ${updatedData.processingStatus}. Updating state.`); updateSingleUrlState(updatedData); // Update the specific URL in state anyUpdates = true; } } else { // Handle fetch error for a specific URL during polling console.error(`Polling: Failed to fetch details for URL ${urlId}:`, result.reason); // Optionally mark this URL as failed in the state if fetch fails consistently? // updateSingleUrlState({ id: urlId, processingStatus: 'failed', summary: 'Failed to fetch status.' }); // anyUpdates = true; } }); // If all polled URLs are now completed/failed, stop the interval early // Check the main 'urls' state again after potential updates const stillPending = urls.some(u => u.processingStatus === 'pending'); if (!stillPending && pollIntervalRef.current) { console.log("Polling: All polled URLs completed/failed, stopping interval."); clearInterval(pollIntervalRef.current); pollIntervalRef.current = null; } }, POLLING_INTERVAL_MS); } else if (pendingUrls.length === 0 && pollIntervalRef.current) { // Stop polling if no pending URLs remain console.log("Polling stopped: No pending URLs."); clearInterval(pollIntervalRef.current); pollIntervalRef.current = null; } // Cleanup function for this specific effect (when urls state changes) // This ensures the interval is cleared if the component unmounts while polling return () => { if (pollIntervalRef.current) { clearInterval(pollIntervalRef.current); // console.log("Polling interval cleared on effect cleanup."); // Setting ref to null here might cause issues if another effect relies on it immediately // It's better handled by the main cleanup in the projectId effect } }; }, [urls]); // Re-run this effect whenever the urls state changes // --- End Polling Effect --- const handleViewUrlDetails = (urlId) => { setDetailUrlId(urlId); }; const handleBackToList = () => { setDetailUrlId(null); }; const handleAddUrl = () => { // ... (implementation as before, setting initial status to 'pending') ... if (!currentProjectId) { alert("Please select a project first."); return; } let newUrl = prompt("Enter the new URL (e.g., https://example.com or example.com):"); if (newUrl && newUrl.trim() !== '') { let processedUrl = newUrl.trim(); if (!/^(https?:\/\/|\/\/)/i.test(processedUrl)) { processedUrl = 'https://' + processedUrl; } const placeholderId = `temp-${Date.now()}`; const placeholderCard = { id: placeholderId, url: processedUrl, title: '(Processing...)', summary: '', note: '', keywords: [], starred: false, favicon: null, processingStatus: 'pending', }; setUrls(prevUrls => [placeholderCard, ...prevUrls]); addUrlToProject(currentProjectId, processedUrl) .then(addedUrlData => { setUrls(prevUrls => prevUrls.map(url => { if (url.id === placeholderId) { return { id: addedUrlData.id, url: processedUrl, title: '', summary: '', note: '', keywords: [], starred: false, favicon: null, processingStatus: 'pending', // Set correct initial status createdAt: new Date().toISOString(), updatedAt: new Date().toISOString() }; } else { return url; } })); }) .catch(err => { console.error("Failed to add URL:", err); setUrls(prevUrls => prevUrls.filter(url => url.id !== placeholderId)); alert(`Failed to add URL: ${err.message || 'An unknown error occurred.'}`); }); } else if (newUrl !== null) { alert("URL cannot be empty."); } }; const handleAskAi = () => { /* ... */ }; const handleUrlOrderChange = (newOrder) => { /* ... */ }; const handleDeleteUrl = (urlIdToDelete) => { /* ... */ }; const handleRegenerateSummary = (urlIdToRegen) => { // ... (implementation as before, sets isLoading on the specific card) ... // This function should now also ensure the status becomes 'pending' // so the poller can pick it up if needed, or update directly on success. if (!currentProjectId) return; setUrls(prevUrls => prevUrls.map(url => url.id === urlIdToRegen ? { ...url, isLoading: true, processingStatus: 'pending', summary: 'Regenerating...' } : url // Set status to pending )); regenerateSummary(urlIdToRegen) .then(updatedUrlData => { setUrls(prevUrls => prevUrls.map(url => { if (url.id === urlIdToRegen) { // Merge result, ensure isLoading is false // API returns status 'pending' if queued, or full data on sync completion if (updatedUrlData.status === 'pending') { return { ...url, isLoading: false, processingStatus: 'pending', summary: 'Regeneration queued...' }; } else { // Assume completion if status isn't pending return { ...updatedUrlData, id: urlIdToRegen, isLoading: false }; // Ensure ID is correct } } return url; })); }) .catch(err => { console.error("Failed to regenerate summary:", err); setUrls(prevUrls => prevUrls.map(url => // Set status back? Or maybe to failed? Let's mark failed. url.id === urlIdToRegen ? { ...url, isLoading: false, processingStatus: 'failed', summary: 'Regeneration failed.' } : url )); alert(`Regeneration failed: ${err.message}`); }); }; // --- Render Logic --- if (isLoading) return