2025-06-09 17:53:19 +08:00

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;