/** * Tasks Page - Built with TablePageTemplate * Consistent with Keywords page layout, structure and design */ import { useState, useEffect, useMemo, useCallback, useRef } from 'react'; import TablePageTemplate from '../../templates/TablePageTemplate'; import { fetchTasks, createTask, updateTask, deleteTask, bulkDeleteTasks, bulkUpdateTasksStatus, autoGenerateContent, autoGenerateImages, Task, TasksFilters, TaskCreateData, fetchClusters, Cluster, } from '../../services/api'; import FormModal from '../../components/common/FormModal'; import ProgressModal from '../../components/common/ProgressModal'; import { useProgressModal } from '../../hooks/useProgressModal'; import { useResourceDebug } from '../../hooks/useResourceDebug'; import { useToast } from '../../components/ui/toast/ToastContainer'; import { TaskIcon, PlusIcon, DownloadIcon, FileIcon, ImageIcon, CheckCircleIcon } from '../../icons'; import { createTasksPageConfig } from '../../config/pages/tasks.config'; import { useSectorStore } from '../../store/sectorStore'; import { usePageSizeStore } from '../../store/pageSizeStore'; import PageHeader from '../../components/common/PageHeader'; import ModuleNavigationTabs from '../../components/navigation/ModuleNavigationTabs'; export default function Tasks() { const toast = useToast(); const { activeSector } = useSectorStore(); const { pageSize } = usePageSizeStore(); // Data state const [tasks, setTasks] = useState([]); const [clusters, setClusters] = useState([]); const [loading, setLoading] = useState(true); // Filter state const [searchTerm, setSearchTerm] = useState(''); const [statusFilter, setStatusFilter] = useState(''); const [clusterFilter, setClusterFilter] = useState(''); const [structureFilter, setStructureFilter] = useState(''); const [typeFilter, setTypeFilter] = useState(''); const [sourceFilter, setSourceFilter] = useState(''); const [selectedIds, setSelectedIds] = useState([]); // Pagination state const [currentPage, setCurrentPage] = useState(1); const [totalPages, setTotalPages] = useState(1); const [totalCount, setTotalCount] = useState(0); // Sorting state const [sortBy, setSortBy] = useState('created_at'); const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('desc'); const [showContent, setShowContent] = useState(false); // Modal state const [isModalOpen, setIsModalOpen] = useState(false); const [isEditMode, setIsEditMode] = useState(false); const [editingTask, setEditingTask] = useState(null); const [formData, setFormData] = useState({ title: '', description: '', keywords: '', cluster_id: null, content_structure: 'article', content_type: 'post', status: 'queued', word_count: 0, }); // Progress modal for AI functions const progressModal = useProgressModal(); // AI Function Logs state const [aiLogs, setAiLogs] = useState>([]); // Resource Debug toggle - controls AI Function Logs const resourceDebugEnabled = useResourceDebug(); // Track last logged step to avoid duplicates const lastLoggedStepRef = useRef(null); const lastLoggedPercentageRef = useRef(-1); const hasReloadedRef = useRef(false); // Helper function to add log entry (only if Resource Debug is enabled) const addAiLog = useCallback((log: { timestamp: string; type: 'request' | 'success' | 'error' | 'step'; action: string; data: any; stepName?: string; percentage?: number; }) => { if (resourceDebugEnabled) { setAiLogs(prev => [...prev, log]); } }, [resourceDebugEnabled]); // Load clusters for filter dropdown useEffect(() => { const loadClusters = async () => { try { const data = await fetchClusters({ ordering: 'name' }); setClusters(data.results || []); } catch (error) { console.error('Error fetching clusters:', error); } }; loadClusters(); }, []); // Load tasks - wrapped in useCallback const loadTasks = useCallback(async () => { setLoading(true); setShowContent(false); try { const ordering = sortBy ? `${sortDirection === 'desc' ? '-' : ''}${sortBy}` : '-created_at'; // Build search term - combine user search with Site Builder filter if needed let finalSearchTerm = searchTerm; if (sourceFilter === 'site_builder') { // If user has a search term, combine it with Site Builder prefix finalSearchTerm = searchTerm ? `[Site Builder] ${searchTerm}` : '[Site Builder]'; } const filters: TasksFilters = { ...(finalSearchTerm && { search: finalSearchTerm }), ...(statusFilter && { status: statusFilter }), ...(clusterFilter && { cluster_id: clusterFilter }), ...(structureFilter && { content_structure: structureFilter }), ...(typeFilter && { content_type: typeFilter }), page: currentPage, page_size: pageSize, ordering, }; const data = await fetchTasks(filters); setTasks(data.results || []); setTotalCount(data.count || 0); setTotalPages(Math.ceil((data.count || 0) / pageSize)); setTimeout(() => { setShowContent(true); setLoading(false); }, 100); } catch (error: any) { console.error('Error loading tasks:', error); toast.error(`Failed to load tasks: ${error.message}`); setShowContent(true); setLoading(false); } }, [currentPage, statusFilter, clusterFilter, structureFilter, typeFilter, sortBy, sortDirection, searchTerm, activeSector, pageSize]); useEffect(() => { loadTasks(); }, [loadTasks]); // Listen for site and sector changes and refresh data useEffect(() => { const handleSiteChange = () => { loadTasks(); }; const handleSectorChange = () => { loadTasks(); }; window.addEventListener('siteChanged', handleSiteChange); window.addEventListener('sectorChanged', handleSectorChange); return () => { window.removeEventListener('siteChanged', handleSiteChange); window.removeEventListener('sectorChanged', handleSectorChange); }; }, [loadTasks]); // Reset to page 1 when pageSize changes useEffect(() => { setCurrentPage(1); }, [pageSize]); // Debounced search useEffect(() => { const timer = setTimeout(() => { if (currentPage === 1) { loadTasks(); } else { setCurrentPage(1); } }, 500); return () => clearTimeout(timer); }, [searchTerm, currentPage, loadTasks]); // Handle sorting const handleSort = (field: string, direction: 'asc' | 'desc') => { setSortBy(field || 'created_at'); setSortDirection(direction); setCurrentPage(1); }; // Bulk status update handler const handleBulkUpdateStatus = useCallback(async (ids: string[], status: string) => { try { const numIds = ids.map(id => parseInt(id)); await bulkUpdateTasksStatus(numIds, status); await loadTasks(); } catch (error: any) { throw error; } }, [loadTasks]); // Bulk export handler const handleBulkExport = useCallback(async (ids: string[]) => { try { if (!ids || ids.length === 0) { throw new Error('No records selected for export'); } toast.info('Export functionality coming soon'); } catch (error: any) { throw error; } }, []); // Row action handler for single task actions const handleRowAction = useCallback(async (action: string, row: Task) => { if (action === 'generate_content') { // Validate task has required data if (!row.title) { toast.error('Task must have a title to generate content'); return; } // Optional: Validate task status (can generate for any status) // if (row.status !== 'queued') { // toast.error(`Only tasks with status "queued" can generate content. Current status: ${row.status}`); // return; // } const requestData = { ids: [row.id], task_title: row.title, task_id: row.id, }; // Log request addAiLog({ timestamp: new Date().toISOString(), type: 'request', action: 'generate_content (Row Action)', data: requestData, }); try { const result = await autoGenerateContent([row.id]); if (result.success) { if (result.task_id) { // Log success with task_id addAiLog({ timestamp: new Date().toISOString(), type: 'success', action: 'generate_content (Row Action)', data: { task_id: result.task_id, message: result.message }, }); // Async task - show progress modal progressModal.openModal(result.task_id, 'Generating Content', 'ai-generate-content-03'); toast.success('Content generation started'); } else { // Log success with results addAiLog({ timestamp: new Date().toISOString(), type: 'success', action: 'generate_content (Row Action)', data: { tasks_updated: result.tasks_updated || 0, message: result.message }, }); // Synchronous completion toast.success(`Content generated successfully: ${result.tasks_updated || 0} article generated`); await loadTasks(); } } else { // Log error addAiLog({ timestamp: new Date().toISOString(), type: 'error', action: 'generate_content (Row Action)', data: { error: result.error || 'Failed to generate content' }, }); toast.error(result.error || 'Failed to generate content'); } } catch (error: any) { // Log error addAiLog({ timestamp: new Date().toISOString(), type: 'error', action: 'generate_content (Row Action)', data: { error: error.message || 'Unknown error occurred' }, }); toast.error(`Failed to generate content: ${error.message}`); } } }, [toast, loadTasks, progressModal]); // Bulk action handler const handleBulkAction = useCallback(async (action: string, ids: string[]) => { // generate_content removed from bulk actions - only available as row action if (action === 'generate_images') { if (ids.length === 0) { toast.error('Please select at least one task to generate images'); return; } if (ids.length > 10) { toast.error('Maximum 10 tasks allowed for image generation'); return; } const numIds = ids.map(id => parseInt(id)); const selectedTasks = tasks.filter(t => numIds.includes(t.id)); const requestData = { ids: numIds, task_count: numIds.length, task_titles: selectedTasks.map(t => t.title), }; // Log request addAiLog({ timestamp: new Date().toISOString(), type: 'request', action: 'generate_images (Bulk Action)', data: requestData, }); try { const result = await autoGenerateImages(numIds); if (result.success) { if (result.task_id) { // Log success with task_id addAiLog({ timestamp: new Date().toISOString(), type: 'success', action: 'generate_images (Bulk Action)', data: { task_id: result.task_id, message: result.message, task_count: numIds.length }, }); // Async task - show progress modal progressModal.openModal(result.task_id, 'Generating Images'); toast.success('Image generation started'); } else { // Log success with results addAiLog({ timestamp: new Date().toISOString(), type: 'success', action: 'generate_images (Bulk Action)', data: { images_created: result.images_created || 0, message: result.message, task_count: numIds.length }, }); // Synchronous completion toast.success(`Image generation complete: ${result.images_created || 0} images generated`); await loadTasks(); } } else { // Log error addAiLog({ timestamp: new Date().toISOString(), type: 'error', action: 'generate_images (Bulk Action)', data: { error: result.error || 'Failed to generate images', task_count: numIds.length }, }); toast.error(result.error || 'Failed to generate images'); } } catch (error: any) { // Log error addAiLog({ timestamp: new Date().toISOString(), type: 'error', action: 'generate_images (Bulk Action)', data: { error: error.message || 'Unknown error occurred', task_count: numIds.length }, }); toast.error(`Failed to generate images: ${error.message}`); } } else { toast.info(`Bulk action "${action}" for ${ids.length} items`); } }, [toast, loadTasks, progressModal, tasks]); // Log AI function progress steps useEffect(() => { if (!progressModal.taskId || !progressModal.isOpen) { return; } const progress = progressModal.progress; const currentStep = progress.details?.phase || ''; const currentPercentage = progress.percentage; const currentMessage = progress.message; const currentStatus = progress.status; // Log step changes if (currentStep && currentStep !== lastLoggedStepRef.current) { const stepType = currentStatus === 'error' ? 'error' : currentStatus === 'completed' ? 'success' : 'step'; addAiLog({ timestamp: new Date().toISOString(), type: stepType, action: progressModal.title || 'AI Function', stepName: currentStep, percentage: currentPercentage, data: { step: currentStep, message: currentMessage, percentage: currentPercentage, status: currentStatus, details: progress.details, }, }); lastLoggedStepRef.current = currentStep; lastLoggedPercentageRef.current = currentPercentage; } // Log percentage changes for same step (if significant change) else if (currentStep && Math.abs(currentPercentage - lastLoggedPercentageRef.current) >= 10) { const stepType = currentStatus === 'error' ? 'error' : currentStatus === 'completed' ? 'success' : 'step'; addAiLog({ timestamp: new Date().toISOString(), type: stepType, action: progressModal.title || 'AI Function', stepName: currentStep, percentage: currentPercentage, data: { step: currentStep, message: currentMessage, percentage: currentPercentage, status: currentStatus, details: progress.details, }, }); lastLoggedPercentageRef.current = currentPercentage; } // Log status changes (error, completed) else if (currentStatus === 'error' || currentStatus === 'completed') { // Only log if we haven't already logged this status for this step if (currentStep !== lastLoggedStepRef.current || (currentStatus === 'error' && lastLoggedStepRef.current !== 'error') || (currentStatus === 'completed' && lastLoggedStepRef.current !== 'completed')) { const stepType = currentStatus === 'error' ? 'error' : 'success'; addAiLog({ timestamp: new Date().toISOString(), type: stepType, action: progressModal.title || 'AI Function', stepName: currentStep || 'Final', percentage: currentPercentage, data: { step: currentStep || 'Final', message: currentMessage, percentage: currentPercentage, status: currentStatus, details: progress.details, }, }); lastLoggedStepRef.current = currentStep || currentStatus; } } }, [progressModal.progress, progressModal.taskId, progressModal.isOpen, progressModal.title, addAiLog]); // Reset step tracking when modal closes or opens useEffect(() => { if (!progressModal.isOpen) { lastLoggedStepRef.current = null; lastLoggedPercentageRef.current = -1; hasReloadedRef.current = false; // Reset reload flag when modal closes } else { // Reset reload flag when modal opens for a new task hasReloadedRef.current = false; } }, [progressModal.isOpen, progressModal.taskId]); // Create page config const pageConfig = useMemo(() => { return createTasksPageConfig({ clusters, activeSector, formData, setFormData, searchTerm, setSearchTerm, statusFilter, setStatusFilter, clusterFilter, sourceFilter, setSourceFilter, setClusterFilter, structureFilter, setStructureFilter, typeFilter, setTypeFilter, setCurrentPage, }); }, [clusters, activeSector, formData, searchTerm, statusFilter, clusterFilter, structureFilter, typeFilter, sourceFilter]); // Calculate header metrics const headerMetrics = useMemo(() => { if (!pageConfig?.headerMetrics) return []; return pageConfig.headerMetrics.map((metric) => ({ label: metric.label, value: metric.calculate({ tasks, totalCount }), accentColor: metric.accentColor, })); }, [pageConfig?.headerMetrics, tasks, totalCount]); const resetForm = useCallback(() => { setFormData({ title: '', description: '', keywords: '', cluster_id: null, content_structure: 'article', content_type: 'post', status: 'queued', word_count: 0, }); setIsEditMode(false); setEditingTask(null); }, []); // Handle create/edit const handleSave = async () => { try { if (isEditMode && editingTask) { await updateTask(editingTask.id, formData); toast.success('Task updated successfully'); } else { await createTask(formData); toast.success('Task created successfully'); } setIsModalOpen(false); resetForm(); loadTasks(); } catch (error: any) { toast.error(`Failed to save: ${error.message}`); } }; // Writer navigation tabs const writerTabs = [ { label: 'Queue', path: '/writer/tasks', icon: }, { label: 'Drafts', path: '/writer/content', icon: }, { label: 'Images', path: '/writer/images', icon: }, { label: 'Review', path: '/writer/review', icon: }, { label: 'Published', path: '/writer/published', icon: }, ]; return ( <> , color: 'indigo' }} navigation={} /> { const stringValue = value === null || value === undefined ? '' : String(value); if (key === 'search') { setSearchTerm(stringValue); } else if (key === 'status') { setStatusFilter(stringValue); } else if (key === 'cluster_id') { setClusterFilter(stringValue); } else if (key === 'content_structure') { setStructureFilter(stringValue); } else if (key === 'content_type') { setTypeFilter(stringValue); } else if (key === 'source') { setSourceFilter(stringValue); } setCurrentPage(1); }} onEdit={(row) => { setEditingTask(row); setFormData({ title: row.title || '', description: row.description || '', keywords: row.keywords || '', cluster_id: row.cluster_id || null, content_structure: row.content_structure || 'article', content_type: row.content_type || 'post', status: row.status || 'queued', word_count: row.word_count || 0, }); setIsEditMode(true); setIsModalOpen(true); }} onCreate={() => { resetForm(); setIsModalOpen(true); }} createLabel="Add Task" onCreateIcon={} onDelete={async (id: number) => { await deleteTask(id); loadTasks(); }} onBulkDelete={async (ids: number[]) => { const result = await bulkDeleteTasks(ids); // Clear selection first setSelectedIds([]); // Reset to page 1 if we deleted all items on current page if (currentPage > 1 && tasks.length <= ids.length) { setCurrentPage(1); } // Always reload data to refresh the table await loadTasks(); return result; }} onBulkExport={handleBulkExport} onBulkUpdateStatus={handleBulkUpdateStatus} onBulkAction={handleBulkAction} onRowAction={handleRowAction} getItemDisplayName={(row: Task) => row.title} onExport={async () => { toast.info('Export functionality coming soon'); }} onExportIcon={} selectionLabel="task" pagination={{ currentPage, totalPages, totalCount, onPageChange: setCurrentPage, }} selection={{ selectedIds, onSelectionChange: setSelectedIds, }} sorting={{ sortBy, sortDirection, onSort: handleSort, }} headerMetrics={headerMetrics} onFilterReset={() => { setSearchTerm(''); setStatusFilter(''); setClusterFilter(''); setStructureFilter(''); setTypeFilter(''); setCurrentPage(1); }} /> {/* Progress Modal for AI Functions */} { const wasCompleted = progressModal.progress.status === 'completed'; progressModal.closeModal(); // Reload data after modal closes (if completed) if (wasCompleted && !hasReloadedRef.current) { hasReloadedRef.current = true; loadTasks(); setTimeout(() => { hasReloadedRef.current = false; }, 1000); } }} /> {/* AI Function Logs - Display below table (only when Resource Debug is enabled) */} {resourceDebugEnabled && aiLogs.length > 0 && (

AI Function Logs

{aiLogs.slice().reverse().map((log, index) => (
[{log.type.toUpperCase()}] {log.action} {log.stepName && ( {log.stepName} )} {log.percentage !== undefined && ( {log.percentage}% )}
{new Date(log.timestamp).toLocaleTimeString()}
                  {JSON.stringify(log.data, null, 2)}
                
))}
)} {/* Create/Edit Modal */} { setIsModalOpen(false); resetForm(); }} onSubmit={handleSave} title={isEditMode ? 'Edit Task' : 'Add Task'} submitLabel={isEditMode ? 'Update' : 'Create'} fields={pageConfig.formFields(clusters)} /> ); }