diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index eea88fbc..a1f14ae4 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -275,13 +275,8 @@ export default function App() { } /> {/* Thinker Module */} - - - - - - } /> + {/* Thinker Module - Redirect dashboard to prompts */} + } /> diff --git a/frontend/src/components/onboarding/WorkflowGuide.tsx b/frontend/src/components/onboarding/WorkflowGuide.tsx index a2f51ba4..e2661b70 100644 --- a/frontend/src/components/onboarding/WorkflowGuide.tsx +++ b/frontend/src/components/onboarding/WorkflowGuide.tsx @@ -1,13 +1,15 @@ /** * WorkflowGuide Component * Inline welcome/guide screen for new users - * Shows complete workflow explainer with visual flow maps + * Shows complete workflow explainer with visual flow maps and progress tracking */ -import React from 'react'; +import React, { useState, useEffect } from 'react'; import { useNavigate } from 'react-router-dom'; import { Card } from '../ui/card'; import Button from '../ui/button/Button'; import Badge from '../ui/badge/Badge'; +import { ProgressBar } from '../ui/progress'; +import Checkbox from '../form/input/Checkbox'; import { CloseIcon, ArrowRightIcon, @@ -22,15 +24,93 @@ import { } from '../../icons'; import { useOnboardingStore } from '../../store/onboardingStore'; import { useSiteStore } from '../../store/siteStore'; +import { fetchSites, fetchKeywords, fetchClusters, fetchContent } from '../../services/api'; +import { useAuthStore } from '../../store/authStore'; + +interface WorkflowProgress { + hasSite: boolean; + keywordsCount: number; + clustersCount: number; + contentCount: number; + publishedCount: number; + completionPercentage: number; +} export default function WorkflowGuide() { const navigate = useNavigate(); - const { isGuideVisible, dismissGuide } = useOnboardingStore(); + const { isGuideVisible, dismissGuide, loadFromBackend } = useOnboardingStore(); const { activeSite } = useSiteStore(); + const { isAuthenticated } = useAuthStore(); + const [progress, setProgress] = useState({ + hasSite: false, + keywordsCount: 0, + clustersCount: 0, + contentCount: 0, + publishedCount: 0, + completionPercentage: 0, + }); + const [loadingProgress, setLoadingProgress] = useState(true); + const [dontShowAgain, setDontShowAgain] = useState(false); + + // Load dismissal state from backend on mount + useEffect(() => { + if (isAuthenticated) { + loadFromBackend().catch(() => { + // Silently fail - local state will be used + }); + } + }, [isAuthenticated, loadFromBackend]); + + // Load progress data + useEffect(() => { + if (!isAuthenticated || !isGuideVisible) return; + + const loadProgress = async () => { + try { + setLoadingProgress(true); + const [sitesRes, keywordsRes, clustersRes, contentRes] = await Promise.all([ + fetchSites().catch(() => ({ results: [], count: 0 })), + fetchKeywords({ page_size: 1 }).catch(() => ({ count: 0 })), + fetchClusters({ page_size: 1 }).catch(() => ({ count: 0 })), + fetchContent({ page_size: 1 }).catch(() => ({ count: 0 })), + ]); + + const sitesCount = sitesRes.results?.filter((s: any) => s.is_active).length || 0; + const keywordsCount = keywordsRes.count || 0; + const clustersCount = clustersRes.count || 0; + const contentCount = contentRes.count || 0; + const publishedCount = 0; // TODO: Add published content count when API is available + + // Calculate completion percentage + // Milestones: Site (20%), Keywords (20%), Clusters (20%), Content (20%), Published (20%) + let completion = 0; + if (sitesCount > 0) completion += 20; + if (keywordsCount > 0) completion += 20; + if (clustersCount > 0) completion += 20; + if (contentCount > 0) completion += 20; + if (publishedCount > 0) completion += 20; + + setProgress({ + hasSite: sitesCount > 0, + keywordsCount, + clustersCount, + contentCount, + publishedCount, + completionPercentage: completion, + }); + } catch (error) { + console.error('Failed to load progress:', error); + } finally { + setLoadingProgress(false); + } + }; + + loadProgress(); + }, [isAuthenticated, isGuideVisible]); if (!isGuideVisible) return null; - const hasSite = !!activeSite; + const { hasSite, keywordsCount, clustersCount, contentCount, publishedCount, completionPercentage } = progress; return (
@@ -55,7 +135,7 @@ export default function WorkflowGuide() {
- {/* Progress Indicator (if user has started) */} - {hasSite && ( -
- + {/* Progress Tracking */} +
+
+

+ Your Progress +

+ + {completionPercentage}% Complete + +
+ + + {/* Milestones */} +
+ {[ + { + label: 'Site Created', + completed: hasSite, + count: hasSite ? 1 : 0, + path: '/sites', + icon: + }, + { + label: 'Keywords', + completed: keywordsCount > 0, + count: keywordsCount, + path: '/planner/keywords', + icon: + }, + { + label: 'Clusters', + completed: clustersCount > 0, + count: clustersCount, + path: '/planner/clusters', + icon: + }, + { + label: 'Content', + completed: contentCount > 0, + count: contentCount, + path: '/writer/content', + icon: + }, + { + label: 'Published', + completed: publishedCount > 0, + count: publishedCount, + path: '/writer/published', + icon: + }, + ].map((milestone, index) => ( + + ))} +
+
+ + {/* Contextual CTA based on progress */} + {completionPercentage === 0 && ( +
+
- Great! You've created your first site + Get started by creating your first site
- Continue with keyword research and content planning + Choose one of the options above to begin +
+
+
+ )} + + {hasSite && keywordsCount === 0 && ( +
+ +
+
+ Great! You've created your first site +
+
+ Now discover keywords to start your content planning
+
+ )} + + {keywordsCount > 0 && clustersCount === 0 && ( +
+ +
+
+ {keywordsCount} keywords added! +
+
+ Group them into clusters to organize your content strategy +
+
+ +
+ )} + + {clustersCount > 0 && contentCount === 0 && ( +
+ +
+
+ {clustersCount} clusters ready! +
+
+ Generate content ideas and start writing +
+
+ +
+ )} + + {contentCount > 0 && publishedCount === 0 && ( +
+ +
+
+ {contentCount} content pieces created! +
+
+ Review and publish your content to go live +
+
+
)} {/* Footer Actions */} -
-

- You can always access this guide from the header -

-
- - +
+
+
+ +
+
+ + +
+

+ You can always access this guide from the orange "Show Guide" button in the header +

diff --git a/frontend/src/pages/Dashboard/Home.tsx b/frontend/src/pages/Dashboard/Home.tsx index 1b4b9353..458308df 100644 --- a/frontend/src/pages/Dashboard/Home.tsx +++ b/frontend/src/pages/Dashboard/Home.tsx @@ -117,12 +117,19 @@ export default function Home() { const toast = useToast(); const { activeSite } = useSiteStore(); const { activeSector } = useSectorStore(); - const { isGuideDismissed, showGuide } = useOnboardingStore(); + const { isGuideDismissed, showGuide, loadFromBackend } = useOnboardingStore(); const [insights, setInsights] = useState(null); const [loading, setLoading] = useState(true); const [lastUpdated, setLastUpdated] = useState(new Date()); + // Load guide state from backend on mount + useEffect(() => { + loadFromBackend().catch(() => { + // Silently fail - local state will be used + }); + }, [loadFromBackend]); + // Show guide on first visit if not dismissed useEffect(() => { if (!isGuideDismissed) { diff --git a/frontend/src/pages/Linker/ContentList.tsx b/frontend/src/pages/Linker/ContentList.tsx index bfab8ba1..c14e4cbe 100644 --- a/frontend/src/pages/Linker/ContentList.tsx +++ b/frontend/src/pages/Linker/ContentList.tsx @@ -2,12 +2,14 @@ import { useState, useEffect, useCallback } from 'react'; import { useNavigate } from 'react-router'; import PageMeta from '../../components/common/PageMeta'; import PageHeader from '../../components/common/PageHeader'; +import ModuleNavigationTabs from '../../components/navigation/ModuleNavigationTabs'; +import ModuleMetricsFooter, { MetricItem, ProgressMetric } from '../../components/dashboard/ModuleMetricsFooter'; import { linkerApi } from '../../api/linker.api'; import { fetchContent, Content as ContentType } from '../../services/api'; import { useToast } from '../../components/ui/toast/ToastContainer'; import { SourceBadge, ContentSource } from '../../components/content/SourceBadge'; import { LinkResults } from '../../components/linker/LinkResults'; -import { PlugInIcon } from '../../icons'; +import { PlugInIcon, CheckCircleIcon, FileIcon } from '../../icons'; import { useSectorStore } from '../../store/sectorStore'; import { usePageSizeStore } from '../../store/pageSizeStore'; @@ -103,6 +105,9 @@ export default function LinkerContentList() { title="Link Content" description="Add internal links to your content" /> + }, + ]} /> {loading ? (
@@ -240,6 +245,41 @@ export default function LinkerContentList() { )}
)} + + {/* Module Metrics Footer */} + (c.internal_links?.length || 0) > 0).length} with links`, + icon: , + accentColor: 'blue', + href: '/linker/content', + }, + { + title: 'Links Added', + value: content.reduce((sum, c) => sum + (c.internal_links?.length || 0), 0).toLocaleString(), + subtitle: `${Object.keys(linkResults).length} processed`, + icon: , + accentColor: 'purple', + }, + { + title: 'Avg Links/Content', + value: content.length > 0 + ? (content.reduce((sum, c) => sum + (c.internal_links?.length || 0), 0) / content.length).toFixed(1) + : '0', + subtitle: `${content.filter(c => c.linker_version && c.linker_version > 0).length} optimized`, + icon: , + accentColor: 'green', + }, + ]} + progress={{ + label: 'Content Linking Progress', + value: totalCount > 0 ? Math.round((content.filter(c => (c.internal_links?.length || 0) > 0).length / totalCount) * 100) : 0, + color: 'primary', + }} + />
); diff --git a/frontend/src/pages/Optimizer/ContentSelector.tsx b/frontend/src/pages/Optimizer/ContentSelector.tsx index 2834dcdb..85f259cc 100644 --- a/frontend/src/pages/Optimizer/ContentSelector.tsx +++ b/frontend/src/pages/Optimizer/ContentSelector.tsx @@ -2,6 +2,8 @@ import { useState, useEffect, useCallback } from 'react'; import { useNavigate } from 'react-router'; import PageMeta from '../../components/common/PageMeta'; import PageHeader from '../../components/common/PageHeader'; +import ModuleNavigationTabs from '../../components/navigation/ModuleNavigationTabs'; +import ModuleMetricsFooter, { MetricItem, ProgressMetric } from '../../components/dashboard/ModuleMetricsFooter'; import { optimizerApi, EntryPoint } from '../../api/optimizer.api'; import { fetchContent, Content as ContentType } from '../../services/api'; import { useToast } from '../../components/ui/toast/ToastContainer'; @@ -9,7 +11,7 @@ import { SourceBadge, ContentSource } from '../../components/content/SourceBadge import { SyncStatusBadge, SyncStatus } from '../../components/content/SyncStatusBadge'; import { ContentFilter, FilterState } from '../../components/content/ContentFilter'; import { OptimizationScores } from '../../components/optimizer/OptimizationScores'; -import { BoltIcon, CheckCircleIcon } from '../../icons'; +import { BoltIcon, CheckCircleIcon, FileIcon } from '../../icons'; import { useSectorStore } from '../../store/sectorStore'; import { usePageSizeStore } from '../../store/pageSizeStore'; @@ -146,15 +148,18 @@ export default function OptimizerContentSelector() {
+ , + color: 'orange', + }} + /> + }, + ]} />
- , - color: 'orange', - }} - />
onChange('industry', e.target.value)} - placeholder="Supply chain automation" + placeholder={userPreferences?.selectedIndustry || "Supply chain automation"} /> -