292 lines
13 KiB
JavaScript
292 lines
13 KiB
JavaScript
// 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 <div className={styles.loading}>Loading project data...</div>;
|
|
if (!currentProjectId && !isLoading) return <div className={styles.noProjectSelected}>Select a project from the sidebar to view details.</div>;
|
|
if (error && !detailUrlId) return <div className={styles.error}>{error}</div>;
|
|
if (!projectDetails && !isLoading && !error && currentProjectId && !detailUrlId) return <div className={styles.error}>Could not load details for the selected project.</div>;
|
|
|
|
return (
|
|
<div className={styles.mainContent} key={currentProjectId}>
|
|
{detailUrlId ? (
|
|
<UrlDetailPage urlId={detailUrlId} onBack={handleBackToList} />
|
|
) : (
|
|
<>
|
|
{projectDetails && ( <ProjectHeader /* ...props... */ /> )}
|
|
<UrlCardList
|
|
urls={urls}
|
|
onOrderChange={handleUrlOrderChange}
|
|
onDelete={handleDeleteUrl}
|
|
onRegenerate={handleRegenerateSummary}
|
|
onViewDetails={handleViewUrlDetails}
|
|
/>
|
|
<div className={styles.actionBar}>
|
|
{/* ... action buttons ... */}
|
|
<button className={styles.actionButton} onClick={handleAskAi} disabled={!currentProjectId || isLoading}>
|
|
<FaMagic className={styles.actionIcon} /> Ask AI
|
|
</button>
|
|
<button className={styles.actionButton} onClick={handleAddUrl} disabled={!currentProjectId || isLoading}>
|
|
Add url <FaPlus className={styles.actionIcon} />
|
|
</button>
|
|
</div>
|
|
{aiResponse && <div className={styles.aiResponseArea}>{aiResponse}</div>}
|
|
</>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export default MainContent;
|