metrics adn insihigts
This commit is contained in:
@@ -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: <GroupIcon />, color: 'purple' }}
|
||||
navigation={<ModuleNavigationTabs tabs={plannerTabs} />}
|
||||
workflowInsights={workflowInsights}
|
||||
/>
|
||||
<TablePageTemplate
|
||||
columns={pageConfig.columns}
|
||||
@@ -490,6 +549,40 @@ export default function Clusters() {
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Module Metrics Footer - Pipeline Style with Cross-Module Links */}
|
||||
<ModuleMetricsFooter
|
||||
metrics={[
|
||||
{
|
||||
title: 'Keywords',
|
||||
value: clusters.reduce((sum, c) => sum + (c.keywords_count || 0), 0).toLocaleString(),
|
||||
subtitle: `in ${totalCount} clusters`,
|
||||
icon: <ListIcon className="w-5 h-5" />,
|
||||
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: <BoltIcon className="w-5 h-5" />,
|
||||
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: <GroupIcon className="w-5 h-5" />,
|
||||
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 */}
|
||||
<ProgressModal
|
||||
isOpen={progressModal.isOpen}
|
||||
|
||||
@@ -29,6 +29,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 Ideas() {
|
||||
const toast = useToast();
|
||||
@@ -76,6 +78,57 @@ export default function Ideas() {
|
||||
// Progress modal for AI functions
|
||||
const progressModal = useProgressModal();
|
||||
|
||||
// Calculate workflow insights
|
||||
const workflowInsights: WorkflowInsight[] = useMemo(() => {
|
||||
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: <BoltIcon />, color: 'orange' }}
|
||||
navigation={<ModuleNavigationTabs tabs={plannerTabs} />}
|
||||
workflowInsights={workflowInsights}
|
||||
/>
|
||||
<TablePageTemplate
|
||||
columns={pageConfig.columns}
|
||||
@@ -413,6 +468,48 @@ export default function Ideas() {
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Module Metrics Footer - Pipeline Style with Cross-Module Links */}
|
||||
<ModuleMetricsFooter
|
||||
metrics={[
|
||||
{
|
||||
title: 'Clusters',
|
||||
value: clusters.length.toLocaleString(),
|
||||
subtitle: 'topic groups',
|
||||
icon: <GroupIcon className="w-5 h-5" />,
|
||||
accentColor: 'purple',
|
||||
href: '/planner/clusters',
|
||||
},
|
||||
{
|
||||
title: 'Ready to Queue',
|
||||
value: ideas.filter(i => i.status === 'new').length.toLocaleString(),
|
||||
subtitle: 'awaiting approval',
|
||||
icon: <BoltIcon className="w-5 h-5" />,
|
||||
accentColor: 'orange',
|
||||
},
|
||||
{
|
||||
title: 'In Queue',
|
||||
value: ideas.filter(i => i.status === 'queued').length.toLocaleString(),
|
||||
subtitle: 'ready for tasks',
|
||||
icon: <ListIcon className="w-5 h-5" />,
|
||||
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: <BoltIcon className="w-5 h-5" />,
|
||||
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 */}
|
||||
<ProgressModal
|
||||
isOpen={progressModal.isOpen}
|
||||
|
||||
@@ -651,9 +651,69 @@ export default function Keywords() {
|
||||
label: metric.label,
|
||||
value: metric.calculate({ keywords, totalCount, clusters }),
|
||||
accentColor: metric.accentColor,
|
||||
tooltip: (metric as any).tooltip, // Add tooltip support
|
||||
}));
|
||||
}, [pageConfig?.headerMetrics, keywords, totalCount, clusters]);
|
||||
|
||||
// Calculate workflow insights based on UX doc principles
|
||||
const workflowInsights = useMemo(() => {
|
||||
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: <ListIcon />, color: 'green' }}
|
||||
navigation={<ModuleNavigationTabs tabs={plannerTabs} />}
|
||||
workflowInsights={workflowInsights}
|
||||
/>
|
||||
<TablePageTemplate
|
||||
columns={pageConfig.columns}
|
||||
@@ -857,9 +918,9 @@ export default function Keywords() {
|
||||
<ModuleMetricsFooter
|
||||
metrics={[
|
||||
{
|
||||
title: 'Total Keywords',
|
||||
title: 'Keywords',
|
||||
value: totalCount.toLocaleString(),
|
||||
subtitle: `${clusters.length} clusters`,
|
||||
subtitle: `in ${clusters.length} clusters`,
|
||||
icon: <ListIcon className="w-5 h-5" />,
|
||||
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: <GroupIcon className="w-5 h-5" />,
|
||||
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: <BoltIcon className="w-5 h-5" />,
|
||||
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',
|
||||
}}
|
||||
|
||||
Reference in New Issue
Block a user