diff --git a/PENDING-ISSUES.md b/PENDING-ISSUES.md index 99b682ad..4e471554 100644 --- a/PENDING-ISSUES.md +++ b/PENDING-ISSUES.md @@ -1,18 +1,11 @@ -# Pending Issues - -## 🔴 Django Admin - Logo Missing on Subpages - -**Issue:** "IGNY8 Admin" logo with rocket icon appears on homepage but not on model list/detail/edit pages. - -**Expected:** Logo should appear consistently across all admin pages (homepage, app index, and all model subpages). - ---- ## 🔴 AI FUunctions progress modals texts and counts to be fixed ## 🔴 AUTOAMTION queue when run manualy completed count to be fixed, and progress abr to be imrpoved and fixed based on actual stage and all other data have bugs -## 🔴 Improve the metrics below tbale son all module pages, with actiionable metrics and steps - ## 🔴 Align prompts with teh strategy -## 🔴 user randomly logs out often, +## 🔴 user randomly logs out often + +## 🔴 MArketing site cotnetn + +## 🔴 docuementation adn help update diff --git a/frontend/src/components/common/PageHeader.tsx b/frontend/src/components/common/PageHeader.tsx index 7182a030..e884e8d0 100644 --- a/frontend/src/components/common/PageHeader.tsx +++ b/frontend/src/components/common/PageHeader.tsx @@ -9,6 +9,7 @@ import { useSectorStore } from '../../store/sectorStore'; import SiteAndSectorSelector from './SiteAndSectorSelector'; import { trackLoading } from './LoadingStateMonitor'; import { useErrorHandler } from '../../hooks/useErrorHandler'; +import { WorkflowInsights, WorkflowInsight } from './WorkflowInsights'; interface PageHeaderProps { title: string; @@ -22,6 +23,7 @@ interface PageHeaderProps { }; hideSiteSector?: boolean; // Hide site/sector selector and info for global pages navigation?: ReactNode; // Module navigation tabs + workflowInsights?: WorkflowInsight[]; // Actionable insights for current page } export default function PageHeader({ @@ -33,6 +35,7 @@ export default function PageHeader({ badge, hideSiteSector = false, navigation, + workflowInsights, }: PageHeaderProps) { const { activeSite } = useSiteStore(); const { activeSector, loadSectorsForSite } = useSectorStore(); @@ -98,77 +101,87 @@ export default function PageHeader({ }; return ( -
- {/* Left side: Title, badge, and site/sector info */} -
-
- {badge && ( -
- {badge.icon && typeof badge.icon === 'object' && 'type' in badge.icon - ? React.cloneElement(badge.icon as React.ReactElement, { className: 'text-white size-5' }) - : badge.icon} +
+ {/* Main header row - single row with 3 sections */} +
+ {/* Left side: Title, badge, and site/sector info */} +
+
+ {badge && ( +
+ {badge.icon && typeof badge.icon === 'object' && 'type' in badge.icon + ? React.cloneElement(badge.icon as React.ReactElement, { className: 'text-white size-5' }) + : badge.icon} +
+ )} +

{title}

+
+ {!hideSiteSector && ( +
+ {lastUpdated && ( + <> +

+ Last updated: {lastUpdated.toLocaleTimeString()} +

+ + )} + {activeSite && ( + <> + {lastUpdated && •} +

+ Site: {activeSite.name} +

+ + )} + {activeSector && ( + <> + • +

+ Sector: {activeSector.name} +

+ + )} + {!activeSector && activeSite && ( + <> + • +

+ Sector: All Sectors +

+ + )}
)} -

{title}

-
- {!hideSiteSector && ( -
- {lastUpdated && ( - <> -

- Last updated: {lastUpdated.toLocaleTimeString()} -

- - )} - {activeSite && ( - <> - {lastUpdated && •} -

- Site: {activeSite.name} -

- - )} - {activeSector && ( - <> - • -

- Sector: {activeSector.name} -

- - )} - {!activeSector && activeSite && ( - <> - • -

- Sector: All Sectors -

- - )} -
- )} - {hideSiteSector && lastUpdated && ( -
-

- Last updated: {lastUpdated.toLocaleTimeString()} -

-
- )} -
- - {/* Right side: Navigation bar stacked above site/sector selector */} -
- {navigation &&
{navigation}
} -
- {!hideSiteSector && } - {showRefresh && onRefresh && ( - + {hideSiteSector && lastUpdated && ( +
+

+ Last updated: {lastUpdated.toLocaleTimeString()} +

+
)}
+ + {/* Middle: Workflow insights - takes available space */} + {workflowInsights && workflowInsights.length > 0 && ( +
+ +
+ )} + + {/* Right side: Navigation bar stacked above site/sector selector */} +
+ {navigation &&
{navigation}
} +
+ {!hideSiteSector && } + {showRefresh && onRefresh && ( + + )} +
+
); diff --git a/frontend/src/components/common/WorkflowInsights.tsx b/frontend/src/components/common/WorkflowInsights.tsx new file mode 100644 index 00000000..8b68006a --- /dev/null +++ b/frontend/src/components/common/WorkflowInsights.tsx @@ -0,0 +1,92 @@ +/** + * WorkflowInsights Component + * Shows actionable insights in a collapsible notification box + * Placed between page title and navigation tabs + */ +import React, { useState } from 'react'; +import { CheckCircleIcon, AlertIcon, InfoIcon, BoltIcon, CloseIcon } from '../../icons'; + +export interface WorkflowInsight { + type: 'success' | 'warning' | 'info' | 'action'; + message: string; +} + +interface WorkflowInsightsProps { + insights: WorkflowInsight[]; + className?: string; +} + +export const WorkflowInsights: React.FC = ({ insights, className = '' }) => { + const [isCollapsed, setIsCollapsed] = useState(false); + + if (!insights || insights.length === 0 || isCollapsed) return null; + + // Determine notification color based on most critical insight type + const hasCritical = insights.some(i => i.type === 'warning' || i.type === 'action'); + const hasSuccess = insights.some(i => i.type === 'success'); + + const notificationColors = hasCritical + ? { + bg: 'bg-amber-50 dark:bg-amber-500/10', + border: 'border-amber-300 dark:border-amber-700', + iconBg: 'bg-amber-100 dark:bg-amber-500/20', + } + : hasSuccess + ? { + bg: 'bg-green-50 dark:bg-green-500/10', + border: 'border-green-300 dark:border-green-700', + iconBg: 'bg-green-100 dark:bg-green-500/20', + } + : { + bg: 'bg-blue-50 dark:bg-blue-500/10', + border: 'border-blue-300 dark:border-blue-700', + iconBg: 'bg-blue-100 dark:bg-blue-500/20', + }; + + const getIcon = (type: string) => { + switch (type) { + case 'success': + return ; + case 'warning': + return ; + case 'action': + return ; + default: + return ; + } + }; + + return ( +
+
+ {/* Icon */} +
+ {getIcon(insights[0].type)} +
+ + {/* Content */} +
+ {insights.map((insight, index) => ( +
+ {insights.length > 1 && ( + • + )} +

+ {insight.message} +

+
+ ))} +
+ + {/* Close button */} + +
+
+ ); +}; diff --git a/frontend/src/components/header/HeaderMetrics.tsx b/frontend/src/components/header/HeaderMetrics.tsx index 1d530936..a563d8f7 100644 --- a/frontend/src/components/header/HeaderMetrics.tsx +++ b/frontend/src/components/header/HeaderMetrics.tsx @@ -1,5 +1,7 @@ import React from 'react'; import { useHeaderMetrics } from '../../context/HeaderMetricsContext'; +import { Tooltip } from '../ui/tooltip/Tooltip'; +import { InfoIcon } from '../../icons'; export const HeaderMetrics: React.FC = () => { const { metrics } = useHeaderMetrics(); @@ -8,18 +10,35 @@ export const HeaderMetrics: React.FC = () => { return (
- {metrics.map((metric, index) => ( - -
+ {metrics.map((metric, index) => { + const metricElement = ( +
- {metric.label} + + {metric.label} + {metric.tooltip && ( + + )} + {typeof metric.value === 'number' ? metric.value.toLocaleString() : metric.value}
- {index < metrics.length - 1 &&
} - - ))} + ); + + return ( + + {metric.tooltip ? ( + + {metricElement} + + ) : ( + metricElement + )} + {index < metrics.length - 1 &&
} +
+ ); + })}
); }; diff --git a/frontend/src/config/pages/clusters.config.tsx b/frontend/src/config/pages/clusters.config.tsx index ab9312a8..36183c01 100644 --- a/frontend/src/config/pages/clusters.config.tsx +++ b/frontend/src/config/pages/clusters.config.tsx @@ -420,28 +420,32 @@ export const createClustersPageConfig = ( ], headerMetrics: [ { - label: 'Total Clusters', + label: 'Clusters', value: 0, accentColor: 'blue' as const, calculate: (data) => data.totalCount || 0, + tooltip: 'Topic clusters organizing your keywords. Each cluster should have 3-7 related keywords.', }, { - label: 'Active', - value: 0, - accentColor: 'green' as const, - calculate: (data) => data.clusters.filter((c: Cluster) => c.status === 'active').length, - }, - { - label: 'Total Keywords', + label: 'New', value: 0, accentColor: 'amber' as const, - calculate: (data) => data.clusters.reduce((sum: number, c: Cluster) => sum + (c.keywords_count || 0), 0), + calculate: (data) => data.clusters.filter((c: Cluster) => (c.ideas_count || 0) === 0).length, + tooltip: 'Clusters without content ideas yet. Generate ideas for these clusters to move them into the pipeline.', }, { - label: 'Total Volume', + label: 'Keywords', value: 0, accentColor: 'purple' as const, + calculate: (data) => data.clusters.reduce((sum: number, c: Cluster) => sum + (c.keywords_count || 0), 0), + tooltip: 'Total keywords organized across all clusters. More keywords = better topic coverage.', + }, + { + label: 'Volume', + value: 0, + accentColor: 'green' as const, calculate: (data) => data.clusters.reduce((sum: number, c: Cluster) => sum + (c.volume || 0), 0), + tooltip: 'Combined search volume across all clusters. Prioritize high-volume clusters for maximum traffic.', }, ], }; diff --git a/frontend/src/config/pages/content.config.tsx b/frontend/src/config/pages/content.config.tsx index 43fbcf17..21196fea 100644 --- a/frontend/src/config/pages/content.config.tsx +++ b/frontend/src/config/pages/content.config.tsx @@ -440,22 +440,32 @@ export const createContentPageConfig = ( ], headerMetrics: [ { - label: 'Total Content', + label: 'Content', value: 0, accentColor: 'blue' as const, calculate: (data) => data.totalCount || 0, + tooltip: 'Total content pieces generated. Includes drafts, review, and published content.', }, { label: 'Draft', value: 0, accentColor: 'amber' as const, calculate: (data) => data.content.filter((c: Content) => c.status === 'draft').length, + tooltip: 'Content in draft stage. Edit and refine before moving to review.', + }, + { + label: 'In Review', + value: 0, + accentColor: 'blue' as const, + calculate: (data) => data.content.filter((c: Content) => c.status === 'review').length, + tooltip: 'Content awaiting review and approval. Review for quality before publishing.', }, { label: 'Published', value: 0, accentColor: 'green' as const, calculate: (data) => data.content.filter((c: Content) => c.status === 'published').length, + tooltip: 'Published content ready for WordPress sync. Track your published library.', }, ], }; diff --git a/frontend/src/config/pages/ideas.config.tsx b/frontend/src/config/pages/ideas.config.tsx index bf7612a2..19aa86ab 100644 --- a/frontend/src/config/pages/ideas.config.tsx +++ b/frontend/src/config/pages/ideas.config.tsx @@ -387,28 +387,32 @@ export const createIdeasPageConfig = ( ], headerMetrics: [ { - label: 'Total Ideas', + label: 'Ideas', value: 0, accentColor: 'blue' as const, calculate: (data) => data.totalCount || 0, + tooltip: 'Total content ideas generated. Ideas become tasks in the content queue for writing.', }, { label: 'New', value: 0, accentColor: 'amber' as const, calculate: (data) => data.ideas.filter((i: ContentIdea) => i.status === 'new').length, + tooltip: 'New ideas waiting for review. Approve ideas to queue them for content creation.', }, { label: 'Queued', value: 0, accentColor: 'blue' as const, calculate: (data) => data.ideas.filter((i: ContentIdea) => i.status === 'queued').length, + tooltip: 'Ideas queued for content generation. These will be converted to writing tasks automatically.', }, { label: 'Completed', value: 0, accentColor: 'green' as const, - calculate: (data) => data.ideas.filter((i: ContentIdea) => i.status === 'published').length, + calculate: (data) => data.ideas.filter((i: ContentIdea) => i.status === 'completed').length, + tooltip: 'Ideas that have been successfully turned into content. Track your content creation progress.', }, ], }; diff --git a/frontend/src/config/pages/images.config.tsx b/frontend/src/config/pages/images.config.tsx index 4c669323..b3658eda 100644 --- a/frontend/src/config/pages/images.config.tsx +++ b/frontend/src/config/pages/images.config.tsx @@ -217,28 +217,32 @@ export const createImagesPageConfig = ( ], headerMetrics: [ { - label: 'Total Content', + label: 'Content', value: 0, accentColor: 'blue' as const, calculate: (data) => data.totalCount || 0, + tooltip: 'Total content pieces with image generation. Track image coverage across all content.', }, { label: 'Complete', value: 0, accentColor: 'green' as const, calculate: (data) => data.images.filter((i: ContentImagesGroup) => i.overall_status === 'complete').length, + tooltip: 'Content with all images generated. Ready for publishing with full visual coverage.', }, { label: 'Partial', value: 0, - accentColor: 'info' as const, + accentColor: 'blue' as const, calculate: (data) => data.images.filter((i: ContentImagesGroup) => i.overall_status === 'partial').length, + tooltip: 'Content with some images missing. Generate remaining images to complete visual assets.', }, { label: 'Pending', value: 0, accentColor: 'amber' as const, calculate: (data) => data.images.filter((i: ContentImagesGroup) => i.overall_status === 'pending').length, + tooltip: 'Content waiting for image generation. Queue these to start creating visual assets.', }, ], maxInArticleImages: maxImages, diff --git a/frontend/src/config/pages/keywords.config.tsx b/frontend/src/config/pages/keywords.config.tsx index 73ee4f28..3c646499 100644 --- a/frontend/src/config/pages/keywords.config.tsx +++ b/frontend/src/config/pages/keywords.config.tsx @@ -476,28 +476,32 @@ export const createKeywordsPageConfig = ( ], headerMetrics: [ { - label: 'Total Keywords', + label: 'Keywords', value: 0, accentColor: 'blue' as const, calculate: (data) => data.totalCount || 0, + tooltip: 'Total keywords added to site wrokflow. Minimum 5 Keywords are needed for clustering.', }, { - label: 'Mapped', + label: 'Clustered', value: 0, accentColor: 'green' as const, calculate: (data) => data.keywords.filter((k: Keyword) => k.cluster_id).length, + tooltip: 'Keywords grouped into topical clusters. Clustered keywords are ready for content ideation.', }, { label: 'Unmapped', value: 0, accentColor: 'amber' as const, calculate: (data) => data.keywords.filter((k: Keyword) => !k.cluster_id).length, + tooltip: 'Unclustered keywords waiting to be organized. Select keywords and use Auto-Cluster to group them.', }, { - label: 'Total Volume', + label: 'Volume', value: 0, accentColor: 'purple' as const, calculate: (data) => data.keywords.reduce((sum: number, k: Keyword) => sum + (k.volume || 0), 0), + tooltip: 'Total monthly search volume across all keywords. Higher volume = more traffic potential.', }, ], // bulkActions and rowActions are now global - defined in table-actions.config.ts diff --git a/frontend/src/config/pages/published.config.tsx b/frontend/src/config/pages/published.config.tsx index fd16eed8..d8550a96 100644 --- a/frontend/src/config/pages/published.config.tsx +++ b/frontend/src/config/pages/published.config.tsx @@ -305,15 +305,38 @@ export function createPublishedPageConfig(params: { const headerMetrics: HeaderMetricConfig[] = [ { - label: 'Total Published', + label: 'Published', accentColor: 'green', calculate: (data: { totalCount: number }) => data.totalCount, + tooltip: 'Total published content. Track your complete content library.', }, { - label: 'On WordPress', + label: 'Synced', accentColor: 'blue', calculate: (data: { content: Content[] }) => data.content.filter(c => c.external_id).length, + tooltip: 'Content synced to WordPress. Successfully published on your website.', + }, + { + label: 'This Month', + accentColor: 'purple', + calculate: (data: { content: Content[] }) => { + const now = new Date(); + const thisMonth = now.getMonth(); + const thisYear = now.getFullYear(); + return data.content.filter(c => { + const date = new Date(c.created_at); + return date.getMonth() === thisMonth && date.getFullYear() === thisYear; + }).length; + }, + tooltip: 'Content published this month. Track your monthly publishing velocity.', + }, + { + label: 'Pending Sync', + accentColor: 'amber', + calculate: (data: { content: Content[] }) => + data.content.filter(c => !c.external_id).length, + tooltip: 'Published content not yet synced to WordPress. Sync these to make them live.', }, ]; diff --git a/frontend/src/config/pages/review.config.tsx b/frontend/src/config/pages/review.config.tsx index 88ec2a35..1cc7e7a9 100644 --- a/frontend/src/config/pages/review.config.tsx +++ b/frontend/src/config/pages/review.config.tsx @@ -262,19 +262,28 @@ export function createReviewPageConfig(params: { ], headerMetrics: [ { - label: 'Total Ready', + label: 'Ready', accentColor: 'blue', calculate: ({ totalCount }) => totalCount, + tooltip: 'Content ready for final review. Review quality, SEO, and images before publishing.', }, { - label: 'Has Images', + label: 'Images', accentColor: 'green', calculate: ({ content }) => content.filter(c => c.has_generated_images).length, + tooltip: 'Content with generated images. Visual assets complete and ready for review.', }, { label: 'Optimized', accentColor: 'purple', - calculate: ({ content }) => content.filter(c => (c as any).optimization_score >= 80).length, + calculate: ({ content }) => content.filter(c => c.optimization_scores && c.optimization_scores.overall_score >= 80).length, + tooltip: 'Content with high SEO optimization scores (80%+). Well-optimized for search engines.', + }, + { + label: 'Sync Ready', + accentColor: 'amber', + calculate: ({ content }) => content.filter(c => c.has_generated_images && c.optimization_scores && c.optimization_scores.overall_score >= 70).length, + tooltip: 'Content ready for WordPress sync. Has images and good optimization score.', }, ], }; diff --git a/frontend/src/config/pages/tasks.config.tsx b/frontend/src/config/pages/tasks.config.tsx index d1240488..bbe3f300 100644 --- a/frontend/src/config/pages/tasks.config.tsx +++ b/frontend/src/config/pages/tasks.config.tsx @@ -454,22 +454,39 @@ export const createTasksPageConfig = ( ], headerMetrics: [ { - label: 'Total Tasks', + label: 'Tasks', value: 0, accentColor: 'blue' as const, calculate: (data) => data.totalCount || 0, + tooltip: 'Total content generation tasks. Tasks process ideas into written content automatically.', }, { - label: 'Queued', + label: 'In Queue', value: 0, accentColor: 'amber' as const, calculate: (data) => data.tasks.filter((t: Task) => t.status === 'queued').length, + tooltip: 'Tasks queued for processing. These will be picked up by the content generation system.', + }, + { + label: 'Processing', + value: 0, + accentColor: 'blue' as const, + calculate: (data) => data.tasks.filter((t: Task) => t.status === 'in_progress').length, + tooltip: 'Tasks currently being processed. Content is being generated by AI right now.', }, { label: 'Completed', value: 0, accentColor: 'green' as const, calculate: (data) => data.tasks.filter((t: Task) => t.status === 'completed').length, + tooltip: 'Successfully completed tasks. Generated content is ready for review and publishing.', + }, + { + label: 'Failed', + value: 0, + accentColor: 'red' as const, + calculate: (data) => data.tasks.filter((t: Task) => t.status === 'failed').length, + tooltip: 'Failed tasks that need attention. Review error logs and retry or modify the task.', }, ], }; diff --git a/frontend/src/context/HeaderMetricsContext.tsx b/frontend/src/context/HeaderMetricsContext.tsx index a2a38ab6..d3de4a83 100644 --- a/frontend/src/context/HeaderMetricsContext.tsx +++ b/frontend/src/context/HeaderMetricsContext.tsx @@ -4,6 +4,7 @@ interface HeaderMetric { label: string; value: string | number; accentColor: 'blue' | 'green' | 'amber' | 'purple'; + tooltip?: string; // Actionable insight for this metric } interface HeaderMetricsContextType { diff --git a/frontend/src/pages/Planner/Clusters.tsx b/frontend/src/pages/Planner/Clusters.tsx index d6994a98..dd7d565b 100644 --- a/frontend/src/pages/Planner/Clusters.tsx +++ b/frontend/src/pages/Planner/Clusters.tsx @@ -28,6 +28,8 @@ import { usePageSizeStore } from '../../store/pageSizeStore'; import { getDifficultyLabelFromNumber, getDifficultyRange } from '../../utils/difficulty'; import PageHeader from '../../components/common/PageHeader'; import ModuleNavigationTabs from '../../components/navigation/ModuleNavigationTabs'; +import ModuleMetricsFooter, { MetricItem, ProgressMetric } from '../../components/dashboard/ModuleMetricsFooter'; +import { WorkflowInsight } from '../../components/common/WorkflowInsights'; export default function Clusters() { const toast = useToast(); @@ -75,6 +77,61 @@ export default function Clusters() { const progressModal = useProgressModal(); const hasReloadedRef = useRef(false); + // Calculate workflow insights + const workflowInsights: WorkflowInsight[] = useMemo(() => { + const insights: WorkflowInsight[] = []; + const clustersWithIdeas = clusters.filter(c => (c.ideas_count || 0) > 0).length; + const totalIdeas = clusters.reduce((sum, c) => sum + (c.ideas_count || 0), 0); + const emptyClusters = clusters.filter(c => (c.keywords_count || 0) === 0).length; + const thinClusters = clusters.filter(c => (c.keywords_count || 0) > 0 && (c.keywords_count || 0) < 3).length; + const readyForGeneration = clustersWithIdeas; + const generationRate = totalCount > 0 ? Math.round((readyForGeneration / totalCount) * 100) : 0; + + if (totalCount === 0) { + insights.push({ + type: 'info', + message: 'Create clusters to organize keywords into topical groups for better content planning', + }); + return insights; + } + + // Content generation readiness + if (generationRate < 30) { + insights.push({ + type: 'warning', + message: `Only ${generationRate}% of clusters have content ideas - Generate ideas to unlock content pipeline`, + }); + } else if (generationRate >= 70) { + insights.push({ + type: 'success', + message: `${generationRate}% of clusters have ideas (${totalIdeas} total) - Strong content pipeline ready`, + }); + } + + // Empty or thin clusters + if (emptyClusters > 0) { + insights.push({ + type: 'warning', + message: `${emptyClusters} clusters have no keywords - Map keywords or delete unused clusters`, + }); + } else if (thinClusters > 2) { + insights.push({ + type: 'info', + message: `${thinClusters} clusters have fewer than 3 keywords - Consider adding more related keywords for better coverage`, + }); + } + + // Actionable next step + if (totalIdeas === 0) { + insights.push({ + type: 'action', + message: 'Select clusters and use Auto-Generate Ideas to create content briefs', + }); + } + + return insights; + }, [clusters, totalCount]); + // Load clusters - wrapped in useCallback to prevent infinite loops const loadClusters = useCallback(async () => { setLoading(true); @@ -353,6 +410,7 @@ export default function Clusters() { label: metric.label, value: metric.calculate({ clusters, totalCount }), accentColor: metric.accentColor, + tooltip: (metric as any).tooltip, })); }, [pageConfig?.headerMetrics, clusters, totalCount]); @@ -397,6 +455,7 @@ export default function Clusters() { title="Keyword Clusters" badge={{ icon: , color: 'purple' }} navigation={} + workflowInsights={workflowInsights} /> + {/* Module Metrics Footer - Pipeline Style with Cross-Module Links */} + sum + (c.keywords_count || 0), 0).toLocaleString(), + subtitle: `in ${totalCount} clusters`, + icon: , + accentColor: 'blue', + href: '/planner/keywords', + }, + { + title: 'Content Ideas', + value: clusters.reduce((sum, c) => sum + (c.ideas_count || 0), 0).toLocaleString(), + subtitle: `across ${clusters.filter(c => (c.ideas_count || 0) > 0).length} clusters`, + icon: , + accentColor: 'green', + href: '/planner/ideas', + }, + { + title: 'Ready to Write', + value: clusters.filter(c => (c.ideas_count || 0) > 0 && c.status === 'active').length.toLocaleString(), + subtitle: 'clusters with approved ideas', + icon: , + accentColor: 'purple', + }, + ]} + progress={{ + label: 'Idea Generation Pipeline: Clusters with content ideas generated (ready for downstream content creation)', + value: totalCount > 0 ? Math.round((clusters.filter(c => (c.ideas_count || 0) > 0).length / totalCount) * 100) : 0, + color: 'purple', + }} + /> + {/* Progress Modal for AI Functions */} { + const insights: WorkflowInsight[] = []; + const newCount = ideas.filter(i => i.status === 'new').length; + const queuedCount = ideas.filter(i => i.status === 'queued').length; + const completedCount = ideas.filter(i => i.status === 'completed').length; + const queueActivationRate = totalCount > 0 ? Math.round((queuedCount / totalCount) * 100) : 0; + const completionRate = totalCount > 0 ? Math.round((completedCount / totalCount) * 100) : 0; + + if (totalCount === 0) { + insights.push({ + type: 'info', + message: 'Generate ideas from your keyword clusters to build your content pipeline', + }); + return insights; + } + + // Queue activation insights + if (newCount > 0 && queuedCount === 0) { + insights.push({ + type: 'warning', + message: `${newCount} new ideas waiting - Queue them to activate the content pipeline`, + }); + } else if (queueActivationRate > 0 && queueActivationRate < 40) { + insights.push({ + type: 'info', + message: `${queueActivationRate}% of ideas queued (${queuedCount} ideas) - Queue more ideas to maintain steady content flow`, + }); + } else if (queuedCount > 0) { + insights.push({ + type: 'success', + message: `${queuedCount} ideas in queue - Content pipeline is active and ready for task generation`, + }); + } + + // Completion velocity + if (completionRate >= 50) { + insights.push({ + type: 'success', + message: `Strong completion rate (${completionRate}%) - ${completedCount} ideas converted to content`, + }); + } else if (completionRate > 0) { + insights.push({ + type: 'info', + message: `${completedCount} ideas completed (${completionRate}%) - Continue queuing ideas to grow content library`, + }); + } + + return insights; + }, [ideas, totalCount]); + // Load clusters for filter dropdown useEffect(() => { const loadClusters = async () => { @@ -258,6 +311,7 @@ export default function Ideas() { label: metric.label, value: metric.calculate({ ideas, totalCount }), accentColor: metric.accentColor, + tooltip: (metric as any).tooltip, })); }, [pageConfig?.headerMetrics, ideas, totalCount]); @@ -307,6 +361,7 @@ export default function Ideas() { title="Content Ideas" badge={{ icon: , color: 'orange' }} navigation={} + workflowInsights={workflowInsights} /> + {/* Module Metrics Footer - Pipeline Style with Cross-Module Links */} + , + accentColor: 'purple', + href: '/planner/clusters', + }, + { + title: 'Ready to Queue', + value: ideas.filter(i => i.status === 'new').length.toLocaleString(), + subtitle: 'awaiting approval', + icon: , + accentColor: 'orange', + }, + { + title: 'In Queue', + value: ideas.filter(i => i.status === 'queued').length.toLocaleString(), + subtitle: 'ready for tasks', + icon: , + accentColor: 'blue', + href: '/writer/tasks', + }, + { + title: 'Content Created', + value: ideas.filter(i => i.status === 'completed').length.toLocaleString(), + subtitle: `${totalCount > 0 ? Math.round((ideas.filter(i => i.status === 'completed').length / totalCount) * 100) : 0}% completion`, + icon: , + accentColor: 'green', + href: '/writer/content', + }, + ]} + progress={{ + label: 'Idea-to-Content Pipeline: Ideas successfully converted into written content', + value: totalCount > 0 ? Math.round((ideas.filter(i => i.status === 'completed').length / totalCount) * 100) : 0, + color: 'success', + }} + /> + {/* Progress Modal for AI Functions */} { + const insights = []; + const clusteredCount = keywords.filter(k => k.cluster_id).length; + const unclusteredCount = totalCount - clusteredCount; + const pipelineReadiness = totalCount > 0 ? Math.round((clusteredCount / totalCount) * 100) : 0; + + if (totalCount === 0) { + insights.push({ + type: 'info' as const, + message: 'Import keywords to begin building your content strategy and unlock SEO opportunities', + }); + return insights; + } + + // Pipeline Readiness Score insight + if (pipelineReadiness < 30) { + insights.push({ + type: 'warning' as const, + message: `Pipeline readiness at ${pipelineReadiness}% - Most keywords need clustering before content ideation can begin`, + }); + } else if (pipelineReadiness < 60) { + insights.push({ + type: 'info' as const, + message: `Pipeline readiness at ${pipelineReadiness}% - Clustering progress is moderate, continue organizing keywords`, + }); + } else if (pipelineReadiness >= 85) { + insights.push({ + type: 'success' as const, + message: `Excellent pipeline readiness (${pipelineReadiness}%) - Ready for content ideation phase`, + }); + } + + // Clustering Potential (minimum batch size check) + if (unclusteredCount >= 5) { + insights.push({ + type: 'action' as const, + message: `${unclusteredCount} keywords available for auto-clustering (minimum batch size met)`, + }); + } else if (unclusteredCount > 0 && unclusteredCount < 5) { + insights.push({ + type: 'info' as const, + message: `${unclusteredCount} unclustered keywords - Need ${5 - unclusteredCount} more to run auto-cluster`, + }); + } + + // Coverage Gaps - thin clusters that need more research + const thinClusters = clusters.filter(c => (c.keywords_count || 0) === 1); + if (thinClusters.length > 3) { + const thinVolume = thinClusters.reduce((sum, c) => sum + (c.volume || 0), 0); + insights.push({ + type: 'warning' as const, + message: `${thinClusters.length} clusters have only 1 keyword each (${thinVolume.toLocaleString()} monthly volume) - Consider expanding research`, + }); + } + + return insights; + }, [keywords, totalCount, clusters]); + // Handle create/edit const handleSave = async () => { try { @@ -736,6 +796,7 @@ export default function Keywords() { title="Keywords" badge={{ icon: , color: 'green' }} navigation={} + workflowInsights={workflowInsights} /> , accentColor: 'blue', href: '/planner/keywords', @@ -867,21 +928,21 @@ export default function Keywords() { { title: 'Clustered', value: keywords.filter(k => k.cluster_id).length.toLocaleString(), - subtitle: `${Math.round((keywords.filter(k => k.cluster_id).length / Math.max(totalCount, 1)) * 100)}% coverage`, + subtitle: `${Math.round((keywords.filter(k => k.cluster_id).length / Math.max(totalCount, 1)) * 100)}% organized`, icon: , accentColor: 'purple', href: '/planner/clusters', }, { - title: 'Active', - value: keywords.filter(k => k.status === 'active').length.toLocaleString(), - subtitle: `${keywords.filter(k => k.status === 'pending').length} pending`, + title: 'Easy Wins', + value: keywords.filter(k => k.difficulty && k.difficulty <= 3 && (k.volume || 0) > 0).length.toLocaleString(), + subtitle: `Low difficulty with ${keywords.filter(k => k.difficulty && k.difficulty <= 3).reduce((sum, k) => sum + (k.volume || 0), 0).toLocaleString()} volume`, icon: , accentColor: 'green', }, ]} progress={{ - label: 'Keyword Clustering Progress', + label: 'Keyword Clustering Pipeline: Keywords organized into topical clusters', value: totalCount > 0 ? Math.round((keywords.filter(k => k.cluster_id).length / totalCount) * 100) : 0, color: 'primary', }} diff --git a/frontend/src/pages/Writer/Content.tsx b/frontend/src/pages/Writer/Content.tsx index 2003c729..4cfda9f4 100644 --- a/frontend/src/pages/Writer/Content.tsx +++ b/frontend/src/pages/Writer/Content.tsx @@ -25,6 +25,7 @@ import { useProgressModal } from '../../hooks/useProgressModal'; import PageHeader from '../../components/common/PageHeader'; import ModuleNavigationTabs from '../../components/navigation/ModuleNavigationTabs'; import ModuleMetricsFooter, { MetricItem, ProgressMetric } from '../../components/dashboard/ModuleMetricsFooter'; +import { WorkflowInsight } from '../../components/common/WorkflowInsights'; export default function Content() { const toast = useToast(); @@ -55,6 +56,59 @@ export default function Content() { const progressModal = useProgressModal(); const hasReloadedRef = useRef(false); + // Calculate workflow insights + const workflowInsights: WorkflowInsight[] = useMemo(() => { + const insights: WorkflowInsight[] = []; + const draftCount = content.filter(c => c.status === 'draft').length; + const reviewCount = content.filter(c => c.status === 'review').length; + const publishedCount = content.filter(c => c.status === 'published').length; + const publishingRate = totalCount > 0 ? Math.round((publishedCount / totalCount) * 100) : 0; + + if (totalCount === 0) { + insights.push({ + type: 'info', + message: 'No content yet - Generate content from tasks to build your content library', + }); + return insights; + } + + // Draft vs Review status + if (draftCount > reviewCount * 3 && draftCount >= 5) { + insights.push({ + type: 'warning', + message: `${draftCount} drafts waiting for review - Move content to review stage for quality assurance`, + }); + } else if (draftCount > 0) { + insights.push({ + type: 'info', + message: `${draftCount} drafts in progress - Review and refine before moving to publish stage`, + }); + } + + // Review queue status + if (reviewCount > 0) { + insights.push({ + type: 'action', + message: `${reviewCount} pieces awaiting final review - Approve and publish when ready`, + }); + } + + // Publishing readiness + if (publishingRate >= 60 && publishedCount >= 10) { + insights.push({ + type: 'success', + message: `Strong publishing rate (${publishingRate}%) - ${publishedCount} articles ready for WordPress sync`, + }); + } else if (publishedCount > 0) { + insights.push({ + type: 'success', + message: `${publishedCount} articles published (${publishingRate}%) - Continue moving content through the pipeline`, + }); + } + + return insights; + }, [content, totalCount]); + // Load content - wrapped in useCallback const loadContent = useCallback(async () => { setLoading(true); @@ -167,6 +221,7 @@ export default function Content() { label: metric.label, value: metric.calculate({ content, totalCount }), accentColor: metric.accentColor, + tooltip: (metric as any).tooltip, })); }, [pageConfig?.headerMetrics, content, totalCount]); @@ -238,6 +293,7 @@ export default function Content() { title="Content Drafts" badge={{ icon: , color: 'purple' }} navigation={} + workflowInsights={workflowInsights} /> row.title || `Content #${row.id}`} /> - {/* Module Metrics Footer */} + {/* Module Metrics Footer - Pipeline Style with Cross-Module Links */} c.status === 'published').length} published`, - icon: , - accentColor: 'green', - href: '/writer/content', + title: 'Tasks', + value: content.length.toLocaleString(), + subtitle: 'generated from queue', + icon: , + accentColor: 'blue', + href: '/writer/tasks', }, { title: 'Draft', value: content.filter(c => c.status === 'draft').length.toLocaleString(), - subtitle: `${content.filter(c => c.status === 'published').length} published`, - icon: , + subtitle: 'needs editing', + icon: , + accentColor: 'amber', + }, + { + title: 'In Review', + value: content.filter(c => c.status === 'review').length.toLocaleString(), + subtitle: 'awaiting approval', + icon: , accentColor: 'blue', + href: '/writer/review', + }, + { + title: 'Published', + value: content.filter(c => c.status === 'published').length.toLocaleString(), + subtitle: 'ready for sync', + icon: , + accentColor: 'green', + href: '/writer/published', }, ]} progress={{ - label: 'Content Publishing Progress', + label: 'Content Publishing Pipeline: Content moved from draft through review to published (Draft \u2192 Review \u2192 Published)', value: totalCount > 0 ? Math.round((content.filter(c => c.status === 'published').length / totalCount) * 100) : 0, color: 'success', }} diff --git a/frontend/src/pages/Writer/Images.tsx b/frontend/src/pages/Writer/Images.tsx index 2f41aa94..3feb2114 100644 --- a/frontend/src/pages/Writer/Images.tsx +++ b/frontend/src/pages/Writer/Images.tsx @@ -492,6 +492,7 @@ export default function Images() { label: metric.label, value: metric.calculate({ images, totalCount }), accentColor: metric.accentColor, + tooltip: (metric as any).tooltip, })); }, [pageConfig?.headerMetrics, images, totalCount]); diff --git a/frontend/src/pages/Writer/Review.tsx b/frontend/src/pages/Writer/Review.tsx index 39037179..19be6247 100644 --- a/frontend/src/pages/Writer/Review.tsx +++ b/frontend/src/pages/Writer/Review.tsx @@ -133,6 +133,7 @@ export default function Review() { pageConfig.headerMetrics.map(metric => ({ ...metric, value: metric.calculate({ content, totalCount }), + tooltip: (metric as any).tooltip, })), [pageConfig.headerMetrics, content, totalCount] ); diff --git a/frontend/src/pages/Writer/Tasks.tsx b/frontend/src/pages/Writer/Tasks.tsx index 32613a1e..4c8841b5 100644 --- a/frontend/src/pages/Writer/Tasks.tsx +++ b/frontend/src/pages/Writer/Tasks.tsx @@ -31,6 +31,8 @@ import { useSectorStore } from '../../store/sectorStore'; import { usePageSizeStore } from '../../store/pageSizeStore'; import PageHeader from '../../components/common/PageHeader'; import ModuleNavigationTabs from '../../components/navigation/ModuleNavigationTabs'; +import ModuleMetricsFooter, { MetricItem, ProgressMetric } from '../../components/dashboard/ModuleMetricsFooter'; +import { WorkflowInsight } from '../../components/common/WorkflowInsights'; export default function Tasks() { const toast = useToast(); @@ -79,6 +81,63 @@ export default function Tasks() { // Progress modal for AI functions const progressModal = useProgressModal(); + // Calculate workflow insights + const workflowInsights: WorkflowInsight[] = useMemo(() => { + const insights: WorkflowInsight[] = []; + const queuedCount = tasks.filter(t => t.status === 'queued').length; + const processingCount = tasks.filter(t => t.status === 'in_progress').length; + const completedCount = tasks.filter(t => t.status === 'completed').length; + const failedCount = tasks.filter(t => t.status === 'failed').length; + const completionRate = totalCount > 0 ? Math.round((completedCount / totalCount) * 100) : 0; + + if (totalCount === 0) { + insights.push({ + type: 'info', + message: 'No tasks yet - Queue ideas from Planner to start generating content automatically', + }); + return insights; + } + + // Queue status + if (queuedCount > 10) { + insights.push({ + type: 'warning', + message: `Large queue detected (${queuedCount} tasks) - Content generation may take time, consider prioritizing`, + }); + } else if (queuedCount > 0) { + insights.push({ + type: 'info', + message: `${queuedCount} tasks in queue - Content generation pipeline is active`, + }); + } + + // Processing status + if (processingCount > 0) { + insights.push({ + type: 'action', + message: `${processingCount} tasks actively generating content - Check back soon for completed drafts`, + }); + } + + // Failed tasks + if (failedCount > 0) { + insights.push({ + type: 'warning', + message: `${failedCount} tasks failed - Review errors and retry or adjust task parameters`, + }); + } + + // Completion success + if (completionRate >= 70 && completedCount >= 5) { + insights.push({ + type: 'success', + message: `High completion rate (${completionRate}%) - ${completedCount} pieces of content ready for review`, + }); + } + + return insights; + }, [tasks, totalCount]); + // AI Function Logs state const [aiLogs, setAiLogs] = useState, color: 'indigo' }} navigation={} + workflowInsights={workflowInsights} /> + {/* Module Metrics Footer - Pipeline Style with Cross-Module Links */} + sum + (c.ideas_count || 0), 0).toLocaleString(), + subtitle: 'from planner', + icon: , + accentColor: 'orange', + href: '/planner/ideas', + }, + { + title: 'In Queue', + value: tasks.filter(t => t.status === 'queued').length.toLocaleString(), + subtitle: 'waiting for processing', + icon: , + accentColor: 'amber', + }, + { + title: 'Processing', + value: tasks.filter(t => t.status === 'in_progress').length.toLocaleString(), + subtitle: 'generating content', + icon: , + accentColor: 'blue', + }, + { + title: 'Ready for Review', + value: tasks.filter(t => t.status === 'completed').length.toLocaleString(), + subtitle: 'content generated', + icon: , + accentColor: 'green', + href: '/writer/content', + }, + ]} + progress={{ + label: 'Content Generation Pipeline: Tasks successfully completed (Queued → Processing → Completed)', + value: totalCount > 0 ? Math.round((tasks.filter(t => t.status === 'completed').length / totalCount) * 100) : 0, + color: 'success', + }} + /> + {/* Progress Modal for AI Functions */}