feat(seo-copilot): caching + freshness UI; glassomorphic styling; CopilotKit HITL modular actions; provider fixes; DB sessions & action types; seed 17 actions
This commit is contained in:
305
frontend/src/stores/seoCopilotStore.ts
Normal file
305
frontend/src/stores/seoCopilotStore.ts
Normal file
@@ -0,0 +1,305 @@
|
||||
// SEO CopilotKit Store
|
||||
// Zustand store for managing SEO CopilotKit state and actions
|
||||
|
||||
import { create } from 'zustand';
|
||||
import { devtools } from 'zustand/middleware';
|
||||
import {
|
||||
SEOCopilotState,
|
||||
SEOAnalysisData,
|
||||
PersonalizationData,
|
||||
DashboardLayout,
|
||||
CopilotSuggestion,
|
||||
SEOCategory,
|
||||
SEOExperienceLevel,
|
||||
BusinessType,
|
||||
TimeRange,
|
||||
ChartType
|
||||
} from '../types/seoCopilotTypes';
|
||||
import { seoApiService } from '../services/seoApiService';
|
||||
|
||||
// Default dashboard layout
|
||||
const defaultDashboardLayout: DashboardLayout = {
|
||||
focusArea: 'overview',
|
||||
layout: 'overview',
|
||||
hiddenSections: [],
|
||||
chartConfigs: []
|
||||
};
|
||||
|
||||
// Default suggestions
|
||||
const defaultSuggestions: CopilotSuggestion[] = [
|
||||
{
|
||||
id: 'analyze-seo',
|
||||
title: '🔍 Analyze my SEO health',
|
||||
message: 'perform a comprehensive SEO analysis and identify priority issues',
|
||||
icon: '🔍',
|
||||
category: 'analysis',
|
||||
priority: 'high',
|
||||
action: 'analyzeSEOComprehensive'
|
||||
},
|
||||
{
|
||||
id: 'generate-meta',
|
||||
title: '📝 Generate meta descriptions',
|
||||
message: 'create optimized meta descriptions for my website pages',
|
||||
icon: '📝',
|
||||
category: 'optimization',
|
||||
priority: 'medium',
|
||||
action: 'generateMetaDescriptions'
|
||||
},
|
||||
{
|
||||
id: 'analyze-speed',
|
||||
title: '⚡ Check page speed',
|
||||
message: 'analyze my website performance and get optimization recommendations',
|
||||
icon: '⚡',
|
||||
category: 'analysis',
|
||||
priority: 'high',
|
||||
action: 'analyzePageSpeed'
|
||||
},
|
||||
{
|
||||
id: 'explain-seo',
|
||||
title: '🎓 Learn SEO basics',
|
||||
message: 'explain SEO concepts and best practices for my business',
|
||||
icon: '🎓',
|
||||
category: 'education',
|
||||
priority: 'medium',
|
||||
action: 'explainSEOConcept'
|
||||
}
|
||||
];
|
||||
|
||||
// Create the store
|
||||
export const useSEOCopilotStore = create<SEOCopilotState>()(
|
||||
devtools(
|
||||
(set, get) => ({
|
||||
// Initial state
|
||||
isLoading: false,
|
||||
isAnalyzing: false,
|
||||
isGenerating: false,
|
||||
analysisData: null,
|
||||
personalizationData: null,
|
||||
activeChart: null,
|
||||
dashboardLayout: defaultDashboardLayout,
|
||||
suggestions: defaultSuggestions,
|
||||
error: null,
|
||||
lastError: null,
|
||||
|
||||
// Actions
|
||||
setLoading: (loading: boolean) => set({ isLoading: loading }),
|
||||
setAnalyzing: (analyzing: boolean) => set({ isAnalyzing: analyzing }),
|
||||
setGenerating: (generating: boolean) => set({ isGenerating: generating }),
|
||||
|
||||
setAnalysisData: (data: SEOAnalysisData | null) => {
|
||||
set({ analysisData: data });
|
||||
|
||||
// Update suggestions based on analysis data
|
||||
if (data) {
|
||||
const newSuggestions = get().generateContextualSuggestions(data);
|
||||
set({ suggestions: newSuggestions });
|
||||
}
|
||||
},
|
||||
|
||||
setPersonalizationData: (data: PersonalizationData | null) => set({ personalizationData: data }),
|
||||
setActiveChart: (chart: string | null) => set({ activeChart: chart }),
|
||||
|
||||
setDashboardLayout: (layout: DashboardLayout) => {
|
||||
set({ dashboardLayout: layout });
|
||||
// Save layout to backend
|
||||
seoApiService.updateDashboardLayout(layout).catch(console.error);
|
||||
},
|
||||
|
||||
setSuggestions: (suggestions: CopilotSuggestion[]) => set({ suggestions }),
|
||||
setError: (error: string | null) => set({ error, lastError: error ? new Error(error) : null }),
|
||||
clearError: () => set({ error: null, lastError: null }),
|
||||
|
||||
// Additional helper methods
|
||||
generateContextualSuggestions: (analysisData: SEOAnalysisData): CopilotSuggestion[] => {
|
||||
const suggestions: CopilotSuggestion[] = [...defaultSuggestions];
|
||||
|
||||
// Add contextual suggestions based on analysis data (defensive checks)
|
||||
const criticalCount = (analysisData as any)?.critical_issues?.length || 0;
|
||||
if (criticalCount > 0) {
|
||||
suggestions.unshift({
|
||||
id: 'fix-critical-issues',
|
||||
title: `🚨 Fix ${criticalCount} critical issues`,
|
||||
message: `generate action plans for my ${criticalCount} critical SEO issues`,
|
||||
icon: '🚨',
|
||||
category: 'optimization',
|
||||
priority: 'high',
|
||||
action: 'identifySEOOpportunities'
|
||||
});
|
||||
}
|
||||
|
||||
const healthScore = (analysisData as any)?.health_score ?? (analysisData as any)?.overall_score;
|
||||
if (typeof healthScore === 'number' && healthScore < 70) {
|
||||
suggestions.unshift({
|
||||
id: 'improve-score',
|
||||
title: '⚠️ Improve SEO score',
|
||||
message: 'help me improve my SEO health score with specific recommendations',
|
||||
icon: '⚠️',
|
||||
category: 'optimization',
|
||||
priority: 'high',
|
||||
action: 'analyzeSEOComprehensive'
|
||||
});
|
||||
}
|
||||
|
||||
// Mobile performance fallback paths
|
||||
const mobileScore = (analysisData as any)?.mobile_speed?.mobile_score
|
||||
?? (analysisData as any)?.data?.mobile_speed?.mobile_score
|
||||
?? (analysisData as any)?.performance?.mobile_score
|
||||
?? (analysisData as any)?.data?.performance?.mobile_score;
|
||||
|
||||
if (typeof mobileScore === 'number' && mobileScore < 80) {
|
||||
suggestions.push({
|
||||
id: 'optimize-mobile',
|
||||
title: '📱 Optimize mobile performance',
|
||||
message: 'focus on mobile SEO performance and optimization opportunities',
|
||||
icon: '📱',
|
||||
category: 'optimization',
|
||||
priority: 'medium',
|
||||
action: 'analyzePageSpeed'
|
||||
});
|
||||
}
|
||||
|
||||
return suggestions;
|
||||
},
|
||||
|
||||
// API integration methods
|
||||
loadPersonalizationData: async () => {
|
||||
try {
|
||||
set({ isLoading: true, error: null });
|
||||
const data = await seoApiService.getPersonalizationData();
|
||||
set({ personalizationData: data, isLoading: false });
|
||||
} catch (error: any) {
|
||||
set({
|
||||
error: `Failed to load personalization data: ${error.message}`,
|
||||
isLoading: false
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
executeCopilotAction: async (action: string, params: any) => {
|
||||
try {
|
||||
set({ isGenerating: true, error: null });
|
||||
|
||||
const response = await seoApiService.executeCopilotAction(action, params);
|
||||
|
||||
if (response.success) {
|
||||
// Update analysis data if it's an analysis action
|
||||
if (action.includes('analyze') && response.data) {
|
||||
set({ analysisData: response.data });
|
||||
}
|
||||
|
||||
set({ isGenerating: false });
|
||||
return response;
|
||||
} else {
|
||||
set({
|
||||
error: response.message,
|
||||
isGenerating: false
|
||||
});
|
||||
return response;
|
||||
}
|
||||
} catch (error: any) {
|
||||
set({
|
||||
error: `Failed to execute ${action}: ${error.message}`,
|
||||
isGenerating: false
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
// Chart and visualization methods
|
||||
updateChart: (chartType: ChartType, timeRange?: TimeRange, metrics?: string[]) => {
|
||||
const currentLayout = get().dashboardLayout;
|
||||
const updatedConfigs = currentLayout.chartConfigs.map(config => {
|
||||
if (config.chartKey === chartType) {
|
||||
return {
|
||||
...config,
|
||||
timeRange: timeRange || config.timeRange,
|
||||
metrics: metrics || config.metrics
|
||||
};
|
||||
}
|
||||
return config;
|
||||
});
|
||||
|
||||
set({
|
||||
dashboardLayout: {
|
||||
...currentLayout,
|
||||
chartConfigs: updatedConfigs
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
// Utility methods
|
||||
getHealthScoreColor: (score: number): string => {
|
||||
if (score >= 90) return '#4CAF50'; // Green
|
||||
if (score >= 70) return '#FF9800'; // Orange
|
||||
return '#F44336'; // Red
|
||||
},
|
||||
|
||||
getSeverityColor: (severity: string): string => {
|
||||
switch (severity) {
|
||||
case 'critical': return '#F44336';
|
||||
case 'high': return '#FF9800';
|
||||
case 'medium': return '#FFC107';
|
||||
case 'low': return '#4CAF50';
|
||||
default: return '#9E9E9E';
|
||||
}
|
||||
},
|
||||
|
||||
getEffortColor: (effort: string): string => {
|
||||
switch (effort) {
|
||||
case 'easy': return '#4CAF50';
|
||||
case 'medium': return '#FF9800';
|
||||
case 'hard': return '#F44336';
|
||||
default: return '#9E9E9E';
|
||||
}
|
||||
},
|
||||
|
||||
// Reset methods
|
||||
resetAnalysis: () => {
|
||||
set({
|
||||
analysisData: null,
|
||||
suggestions: defaultSuggestions,
|
||||
error: null
|
||||
});
|
||||
},
|
||||
|
||||
resetAll: () => {
|
||||
set({
|
||||
isLoading: false,
|
||||
isAnalyzing: false,
|
||||
isGenerating: false,
|
||||
analysisData: null,
|
||||
personalizationData: null,
|
||||
activeChart: null,
|
||||
dashboardLayout: defaultDashboardLayout,
|
||||
suggestions: defaultSuggestions,
|
||||
error: null,
|
||||
lastError: null
|
||||
});
|
||||
}
|
||||
}),
|
||||
{
|
||||
name: 'seo-copilot-store',
|
||||
enabled: process.env.NODE_ENV === 'development'
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
// Export store hooks for specific use cases
|
||||
export const useSEOCopilotAnalysis = () => useSEOCopilotStore(state => ({
|
||||
analysisData: state.analysisData,
|
||||
isAnalyzing: state.isAnalyzing,
|
||||
error: state.error,
|
||||
executeCopilotAction: state.executeCopilotAction
|
||||
}));
|
||||
|
||||
export const useSEOCopilotSuggestions = () => useSEOCopilotStore(state => (
|
||||
state.suggestions
|
||||
));
|
||||
|
||||
export const useSEOCopilotDashboard = () => useSEOCopilotStore(state => ({
|
||||
dashboardLayout: state.dashboardLayout,
|
||||
setDashboardLayout: state.setDashboardLayout,
|
||||
updateChart: state.updateChart
|
||||
}));
|
||||
|
||||
export default useSEOCopilotStore;
|
||||
@@ -4,6 +4,40 @@ import { SEODashboardData } from '../api/seoDashboard';
|
||||
import { SEOAnalysisData } from '../components/shared/types';
|
||||
import { seoAnalysisAPI } from '../api/seoAnalysis';
|
||||
|
||||
// Simple localStorage cache for analysis data
|
||||
const ANALYSIS_CACHE_KEY = 'seo-dashboard-analysis-cache';
|
||||
type AnalysisCache = {
|
||||
data: SEOAnalysisData;
|
||||
updatedAt: number;
|
||||
url?: string;
|
||||
};
|
||||
|
||||
function loadAnalysisCache(): AnalysisCache | null {
|
||||
try {
|
||||
const raw = localStorage.getItem(ANALYSIS_CACHE_KEY);
|
||||
if (!raw) return null;
|
||||
const parsed = JSON.parse(raw) as AnalysisCache;
|
||||
if (parsed && parsed.data && typeof parsed.updatedAt === 'number') {
|
||||
return parsed;
|
||||
}
|
||||
return null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function saveAnalysisCache(payload: AnalysisCache | null) {
|
||||
try {
|
||||
if (!payload) {
|
||||
localStorage.removeItem(ANALYSIS_CACHE_KEY);
|
||||
return;
|
||||
}
|
||||
localStorage.setItem(ANALYSIS_CACHE_KEY, JSON.stringify(payload));
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
export interface SEODashboardStore {
|
||||
// State
|
||||
data: SEODashboardData | null;
|
||||
@@ -13,6 +47,8 @@ export interface SEODashboardStore {
|
||||
analysisLoading: boolean;
|
||||
analysisError: string | null;
|
||||
hasRunInitialAnalysis: boolean;
|
||||
analysisUpdatedAt: number | null;
|
||||
analysisUrl?: string;
|
||||
|
||||
// Actions
|
||||
setData: (data: SEODashboardData) => void;
|
||||
@@ -24,6 +60,9 @@ export interface SEODashboardStore {
|
||||
runSEOAnalysis: () => Promise<void>;
|
||||
clearAnalysisError: () => void;
|
||||
checkAndRunInitialAnalysis: () => void;
|
||||
refreshSEOAnalysis: () => Promise<void>;
|
||||
clearAnalysisCache: () => void;
|
||||
getAnalysisFreshness: () => { label: string; minutes: number; isStale: boolean };
|
||||
}
|
||||
|
||||
export const useSEODashboardStore = create<SEODashboardStore>()(
|
||||
@@ -33,16 +72,29 @@ export const useSEODashboardStore = create<SEODashboardStore>()(
|
||||
data: null,
|
||||
loading: false,
|
||||
error: null,
|
||||
analysisData: null,
|
||||
analysisData: loadAnalysisCache()?.data || null,
|
||||
analysisLoading: false,
|
||||
analysisError: null,
|
||||
hasRunInitialAnalysis: false,
|
||||
analysisUpdatedAt: loadAnalysisCache()?.updatedAt || null,
|
||||
analysisUrl: loadAnalysisCache()?.url || undefined,
|
||||
|
||||
// Actions
|
||||
setData: (data) => set({ data }),
|
||||
setLoading: (loading) => set({ loading }),
|
||||
setError: (error) => set({ error }),
|
||||
setAnalysisData: (data) => set({ analysisData: data }),
|
||||
setAnalysisData: (data) => {
|
||||
const updatedAt = data ? Date.now() : null;
|
||||
set({ analysisData: data, analysisUpdatedAt: updatedAt });
|
||||
if (data) {
|
||||
const currentUrl = get().data?.website_url || get().analysisUrl;
|
||||
saveAnalysisCache({ data, updatedAt: updatedAt!, url: currentUrl });
|
||||
set({ analysisUrl: currentUrl });
|
||||
} else {
|
||||
saveAnalysisCache(null);
|
||||
set({ analysisUrl: undefined });
|
||||
}
|
||||
},
|
||||
setAnalysisLoading: (loading) => set({ analysisLoading: loading }),
|
||||
setAnalysisError: (error) => set({ analysisError: error }),
|
||||
|
||||
@@ -100,11 +152,15 @@ export const useSEODashboardStore = create<SEODashboardStore>()(
|
||||
|
||||
if (result) {
|
||||
console.log('SEO analysis completed successfully:', result);
|
||||
const updatedAt = Date.now();
|
||||
set({
|
||||
analysisData: result,
|
||||
analysisData: result,
|
||||
analysisUpdatedAt: updatedAt,
|
||||
analysisUrl: url,
|
||||
analysisLoading: false,
|
||||
hasRunInitialAnalysis: true
|
||||
});
|
||||
saveAnalysisCache({ data: result, updatedAt, url });
|
||||
|
||||
console.log('Store state after setting analysis data:', get());
|
||||
|
||||
@@ -161,10 +217,32 @@ export const useSEODashboardStore = create<SEODashboardStore>()(
|
||||
},
|
||||
|
||||
checkAndRunInitialAnalysis: () => {
|
||||
const { analysisData, hasRunInitialAnalysis, data } = get();
|
||||
if (!analysisData && !hasRunInitialAnalysis && data) {
|
||||
get().runSEOAnalysis();
|
||||
// Hydrate from cache only; do not auto-trigger network analysis.
|
||||
const cache = loadAnalysisCache();
|
||||
if (cache) {
|
||||
set({ analysisData: cache.data, analysisUpdatedAt: cache.updatedAt, analysisUrl: cache.url, hasRunInitialAnalysis: true });
|
||||
} else {
|
||||
set({ hasRunInitialAnalysis: true });
|
||||
}
|
||||
},
|
||||
|
||||
refreshSEOAnalysis: async () => {
|
||||
// Explicit user-triggered refresh: clears cache and runs analysis
|
||||
saveAnalysisCache(null);
|
||||
await get().runSEOAnalysis();
|
||||
},
|
||||
|
||||
clearAnalysisCache: () => {
|
||||
saveAnalysisCache(null);
|
||||
set({ analysisData: null, analysisUpdatedAt: null, analysisUrl: undefined });
|
||||
},
|
||||
|
||||
getAnalysisFreshness: () => {
|
||||
const updatedAt = get().analysisUpdatedAt;
|
||||
if (!updatedAt) return { label: 'No analysis yet', minutes: Infinity, isStale: true };
|
||||
const minutes = Math.max(0, Math.floor((Date.now() - updatedAt) / 60000));
|
||||
const label = minutes === 0 ? 'Just now' : `${minutes}m ago`;
|
||||
return { label, minutes, isStale: minutes > 60 };
|
||||
}
|
||||
}),
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user