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:
@@ -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