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:
132
frontend/src/components/SEODashboard/SEOCopilotActions.tsx
Normal file
132
frontend/src/components/SEODashboard/SEOCopilotActions.tsx
Normal file
@@ -0,0 +1,132 @@
|
||||
// SEO CopilotKit Actions Component
|
||||
// Registers all SEO-related actions with CopilotKit
|
||||
|
||||
import React from 'react';
|
||||
import { useCopilotAction } from '@copilotkit/react-core';
|
||||
import { useSEOCopilotStore } from '../../stores/seoCopilotStore';
|
||||
import RegisterPageSpeed from './actions/RegisterPageSpeed';
|
||||
import RegisterSitemap from './actions/RegisterSitemap';
|
||||
import RegisterOnPage from './actions/RegisterOnPage';
|
||||
import RegisterTechnical from './actions/RegisterTechnical';
|
||||
import RegisterMetaDescription from './actions/RegisterMetaDescription';
|
||||
|
||||
const SEOCopilotActions: React.FC = () => {
|
||||
const { executeCopilotAction } = useSEOCopilotStore();
|
||||
const useCopilotActionTyped = useCopilotAction as any;
|
||||
const getDefaultUrl = () => useSEOCopilotStore.getState().analysisData?.url;
|
||||
|
||||
// Lightweight actions without custom UI
|
||||
useCopilotActionTyped({
|
||||
name: 'generateImageAltText',
|
||||
description: 'Generate SEO-friendly alt text for images',
|
||||
parameters: [
|
||||
{ name: 'imageUrl', type: 'string', description: 'Image URL', required: true },
|
||||
{ name: 'context', type: 'string', description: 'Context about the image', required: false },
|
||||
{ name: 'keywords', type: 'string[]', description: 'Keywords to include', required: false }
|
||||
],
|
||||
handler: async (args: any) => executeCopilotAction('generateImageAltText', args)
|
||||
});
|
||||
|
||||
useCopilotActionTyped({
|
||||
name: 'generateOpenGraphTags',
|
||||
description: 'Generate OpenGraph tags for social media optimization',
|
||||
parameters: [
|
||||
{ name: 'url', type: 'string', description: 'URL (optional)', required: false },
|
||||
{ name: 'title', type: 'string', description: 'Title', required: false },
|
||||
{ name: 'description', type: 'string', description: 'Description', required: false }
|
||||
],
|
||||
handler: async (args: any) => executeCopilotAction('generateOpenGraphTags', { ...args, url: args?.url || getDefaultUrl() })
|
||||
});
|
||||
|
||||
useCopilotActionTyped({
|
||||
name: 'analyzeSEOComprehensive',
|
||||
description: 'Comprehensive SEO analysis',
|
||||
parameters: [
|
||||
{ name: 'url', type: 'string', description: 'URL (optional)', required: false },
|
||||
{ name: 'focusAreas', type: 'string[]', description: 'Focus areas', required: false }
|
||||
],
|
||||
handler: async (args: any) => executeCopilotAction('analyzeSEOComprehensive', { ...args, url: args?.url || getDefaultUrl() })
|
||||
});
|
||||
|
||||
useCopilotActionTyped({
|
||||
name: 'analyzeEnterpriseSEO',
|
||||
description: 'Enterprise-level SEO analysis',
|
||||
parameters: [
|
||||
{ name: 'url', type: 'string', description: 'URL (optional)', required: false },
|
||||
{ name: 'competitorUrls', type: 'string[]', description: 'Competitor URLs', required: false }
|
||||
],
|
||||
handler: async (args: any) => executeCopilotAction('analyzeEnterpriseSEO', { ...args, url: args?.url || getDefaultUrl() })
|
||||
});
|
||||
|
||||
useCopilotActionTyped({
|
||||
name: 'analyzeContentStrategy',
|
||||
description: 'Analyze content strategy and recommendations',
|
||||
parameters: [
|
||||
{ name: 'url', type: 'string', description: 'URL (optional)', required: false },
|
||||
{ name: 'contentType', type: 'string', description: 'Content type', required: false }
|
||||
],
|
||||
handler: async (args: any) => executeCopilotAction('analyzeContentStrategy', { ...args, url: args?.url || getDefaultUrl() })
|
||||
});
|
||||
|
||||
useCopilotActionTyped({
|
||||
name: 'performWebsiteAudit',
|
||||
description: 'Perform comprehensive website SEO audit',
|
||||
parameters: [
|
||||
{ name: 'url', type: 'string', description: 'URL (optional)', required: false },
|
||||
{ name: 'auditType', type: 'string', description: 'Audit type', required: false }
|
||||
],
|
||||
handler: async (args: any) => executeCopilotAction('performWebsiteAudit', { ...args, url: args?.url || getDefaultUrl() })
|
||||
});
|
||||
|
||||
useCopilotActionTyped({
|
||||
name: 'analyzeContentComprehensive',
|
||||
description: 'Analyze content comprehensively',
|
||||
parameters: [
|
||||
{ name: 'content', type: 'string', description: 'Content to analyze', required: true },
|
||||
{ name: 'targetKeywords', type: 'string[]', description: 'Target keywords', required: false }
|
||||
],
|
||||
handler: async (args: any) => executeCopilotAction('analyzeContentComprehensive', args)
|
||||
});
|
||||
|
||||
useCopilotActionTyped({
|
||||
name: 'checkSEOHealth',
|
||||
description: 'Check overall SEO health',
|
||||
parameters: [
|
||||
{ name: 'url', type: 'string', description: 'URL (optional)', required: false }
|
||||
],
|
||||
handler: async (args: any) => executeCopilotAction('checkSEOHealth', { ...args, url: args?.url || getDefaultUrl() })
|
||||
});
|
||||
|
||||
useCopilotActionTyped({
|
||||
name: 'explainSEOConcept',
|
||||
description: 'Explain SEO concepts in simple terms',
|
||||
parameters: [
|
||||
{ name: 'concept', type: 'string', description: 'Concept to explain', required: true },
|
||||
{ name: 'audience', type: 'string', description: 'Audience (optional)', required: false }
|
||||
],
|
||||
handler: async (args: any) => executeCopilotAction('explainSEOConcept', args)
|
||||
});
|
||||
|
||||
useCopilotActionTyped({
|
||||
name: 'updateSEOCharts',
|
||||
description: 'Update SEO charts and visualizations',
|
||||
parameters: [
|
||||
{ name: 'chartType', type: 'string', description: 'Chart type', required: true },
|
||||
{ name: 'timeRange', type: 'string', description: 'Time range', required: false }
|
||||
],
|
||||
handler: async (args: any) => executeCopilotAction('updateSEOCharts', args)
|
||||
});
|
||||
|
||||
// Modular registrars (HITL UIs)
|
||||
return (
|
||||
<>
|
||||
<RegisterMetaDescription />
|
||||
<RegisterPageSpeed />
|
||||
<RegisterSitemap />
|
||||
<RegisterOnPage />
|
||||
<RegisterTechnical />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default SEOCopilotActions;
|
||||
99
frontend/src/components/SEODashboard/SEOCopilotContext.tsx
Normal file
99
frontend/src/components/SEODashboard/SEOCopilotContext.tsx
Normal file
@@ -0,0 +1,99 @@
|
||||
// SEO CopilotKit Context Component
|
||||
// Provides real-time context and instructions to CopilotKit
|
||||
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import { useCopilotReadable } from '@copilotkit/react-core';
|
||||
import { useSEOCopilotStore } from '../../stores/seoCopilotStore';
|
||||
|
||||
const SEOCopilotContext: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||
const {
|
||||
analysisData,
|
||||
personalizationData,
|
||||
dashboardLayout,
|
||||
suggestions,
|
||||
isLoading,
|
||||
isAnalyzing,
|
||||
isGenerating,
|
||||
error,
|
||||
loadPersonalizationData
|
||||
} = useSEOCopilotStore();
|
||||
|
||||
const hasLoadedPersonalization = useRef(false);
|
||||
|
||||
// Load personalization data on mount
|
||||
useEffect(() => {
|
||||
if (!hasLoadedPersonalization.current && !personalizationData) {
|
||||
useSEOCopilotStore.getState().loadPersonalizationData();
|
||||
hasLoadedPersonalization.current = true;
|
||||
}
|
||||
}, [personalizationData]);
|
||||
|
||||
// Register SEO analysis data with CopilotKit
|
||||
useCopilotReadable({
|
||||
description: "Current SEO analysis data and insights",
|
||||
value: analysisData,
|
||||
categories: ["seo", "analysis"]
|
||||
});
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
console.log('[CopilotContext] Registered analysis data', !!analysisData);
|
||||
}
|
||||
|
||||
// Provide a flat, explicit website URL for the LLM
|
||||
useCopilotReadable({
|
||||
description: "Current website URL the user is working on",
|
||||
value: analysisData?.url || '',
|
||||
categories: ["seo", "context"]
|
||||
});
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
console.log('[CopilotContext] Registered website URL', analysisData?.url);
|
||||
}
|
||||
|
||||
// Register personalization data with CopilotKit
|
||||
useCopilotReadable({
|
||||
description: "User personalization preferences and settings",
|
||||
value: personalizationData,
|
||||
categories: ["user", "preferences"]
|
||||
});
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
console.log('[CopilotContext] Registered personalization', !!personalizationData);
|
||||
}
|
||||
|
||||
// Register dashboard layout with CopilotKit
|
||||
useCopilotReadable({
|
||||
description: "Current dashboard layout and configuration",
|
||||
value: dashboardLayout,
|
||||
categories: ["ui", "layout"]
|
||||
});
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
console.log('[CopilotContext] Registered layout', !!dashboardLayout);
|
||||
}
|
||||
|
||||
// Register suggestions with CopilotKit
|
||||
useCopilotReadable({
|
||||
description: "Available SEO actions and suggestions",
|
||||
value: suggestions,
|
||||
categories: ["actions", "suggestions"]
|
||||
});
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
console.log('[CopilotContext] Registered suggestions', Array.isArray(suggestions) ? suggestions.length : 0);
|
||||
}
|
||||
|
||||
// Register loading states with CopilotKit
|
||||
useCopilotReadable({
|
||||
description: "Current loading and processing states",
|
||||
value: {
|
||||
isLoading,
|
||||
isAnalyzing,
|
||||
isGenerating,
|
||||
error
|
||||
},
|
||||
categories: ["status", "loading"]
|
||||
});
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
console.log('[CopilotContext] Registered status', { isLoading, isAnalyzing, isGenerating, hasError: !!error });
|
||||
}
|
||||
|
||||
return <>{children}</>;
|
||||
};
|
||||
|
||||
export default SEOCopilotContext;
|
||||
336
frontend/src/components/SEODashboard/SEOCopilotKitProvider.tsx
Normal file
336
frontend/src/components/SEODashboard/SEOCopilotKitProvider.tsx
Normal file
@@ -0,0 +1,336 @@
|
||||
// SEO CopilotKit Provider Component
|
||||
// Main provider that wraps all SEO CopilotKit functionality
|
||||
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import { CopilotKit } from '@copilotkit/react-core';
|
||||
import { CopilotSidebar } from '@copilotkit/react-ui';
|
||||
import '@copilotkit/react-ui/styles.css';
|
||||
import SEOCopilotContext from './SEOCopilotContext';
|
||||
import SEOCopilotActions from './SEOCopilotActions';
|
||||
import { useSEOCopilotStore } from '../../stores/seoCopilotStore';
|
||||
|
||||
interface SEOCopilotKitProviderProps {
|
||||
children: React.ReactNode;
|
||||
enableDebugMode?: boolean;
|
||||
}
|
||||
|
||||
const SEOCopilotKitProvider: React.FC<SEOCopilotKitProviderProps> = ({
|
||||
children,
|
||||
enableDebugMode = false
|
||||
}) => {
|
||||
const {
|
||||
loadPersonalizationData,
|
||||
error,
|
||||
clearError,
|
||||
isLoading
|
||||
} = useSEOCopilotStore();
|
||||
const { analysisData } = useSEOCopilotStore();
|
||||
|
||||
// Get the CopilotKit API key from environment variables
|
||||
const publicApiKey = process.env.REACT_APP_COPILOTKIT_API_KEY;
|
||||
|
||||
// Suggestions model: progressive disclosure
|
||||
const topLevelGroups = useMemo(() => ([
|
||||
{ title: 'Content analysis', message: 'Content analysis' },
|
||||
{ title: 'Website/URL analysis', message: 'Web URL analysis' },
|
||||
{ title: 'Technical SEO', message: 'Technical SEO' },
|
||||
{ title: 'Strategy & planning', message: 'Strategy and planning' },
|
||||
{ title: 'Monitoring & health', message: 'Monitoring and health' }
|
||||
]), []);
|
||||
|
||||
const subSuggestionsByGroup = useMemo(() => ({
|
||||
'Content analysis': [
|
||||
{ title: 'Comprehensive content analysis', message: 'Analyze content comprehensively for my site' },
|
||||
{ title: 'Optimize page content', message: 'Optimize page content for SEO' },
|
||||
{ title: 'Generate meta descriptions', message: 'Generate meta descriptions for key pages' }
|
||||
],
|
||||
'Web URL analysis': [
|
||||
{ title: 'Comprehensive SEO analysis', message: 'Run comprehensive SEO analysis for a URL' },
|
||||
{ title: 'Analyze page speed', message: 'Analyze page speed for a URL' },
|
||||
{ title: 'Analyze sitemap', message: 'Analyze sitemap for my site' },
|
||||
{ title: 'Generate OpenGraph tags', message: 'Generate OpenGraph tags for a URL' }
|
||||
],
|
||||
'Technical SEO': [
|
||||
{ title: 'Technical SEO audit', message: 'Run a technical SEO audit' },
|
||||
{ title: 'Check SEO health', message: 'Check overall SEO health' },
|
||||
{ title: 'Image alt text', message: 'Generate image alt text for pages' }
|
||||
],
|
||||
'Strategy and planning': [
|
||||
{ title: 'Enterprise SEO analysis', message: 'Run enterprise SEO analysis' },
|
||||
{ title: 'Content strategy', message: 'Analyze content strategy and recommendations' },
|
||||
{ title: 'Customize SEO dashboard', message: 'Customize the SEO dashboard' }
|
||||
],
|
||||
'Monitoring and health': [
|
||||
{ title: 'Website audit', message: 'Perform a website audit' },
|
||||
{ title: 'Update SEO charts', message: 'Update SEO charts and visualizations' },
|
||||
{ title: 'Explain an SEO concept', message: 'Explain an SEO concept in simple terms' }
|
||||
]
|
||||
}), []);
|
||||
|
||||
const [chatSuggestions, setChatSuggestions] = useState(topLevelGroups);
|
||||
const backChip = useMemo(() => ({ title: '← Back to categories', message: 'back' }), []);
|
||||
const displayedSuggestions = useMemo(() => {
|
||||
// Always show a back chip when not on top-level
|
||||
const isTop = chatSuggestions === topLevelGroups;
|
||||
return isTop ? chatSuggestions : [...chatSuggestions, backChip];
|
||||
}, [chatSuggestions, topLevelGroups, backChip]);
|
||||
|
||||
// Initialize the provider
|
||||
useEffect(() => {
|
||||
const initializeProvider = async () => {
|
||||
try {
|
||||
// Load personalization data on mount
|
||||
await loadPersonalizationData();
|
||||
|
||||
if (enableDebugMode) {
|
||||
console.log('🔧 SEO CopilotKit Provider initialized successfully');
|
||||
console.log('🔑 CopilotKit API Key:', publicApiKey ? 'Configured' : 'Missing');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ Failed to initialize SEO CopilotKit Provider:', error);
|
||||
}
|
||||
};
|
||||
|
||||
initializeProvider();
|
||||
}, [loadPersonalizationData, enableDebugMode, publicApiKey]);
|
||||
|
||||
// Error handling
|
||||
useEffect(() => {
|
||||
if (error && enableDebugMode) {
|
||||
console.error('🚨 SEO CopilotKit Error:', error);
|
||||
}
|
||||
}, [error, enableDebugMode]);
|
||||
|
||||
// Auto-clear errors after 5 seconds
|
||||
useEffect(() => {
|
||||
if (error) {
|
||||
const timer = setTimeout(() => {
|
||||
clearError();
|
||||
}, 5000);
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}
|
||||
}, [error, clearError]);
|
||||
|
||||
return (
|
||||
<CopilotKit publicApiKey={publicApiKey}>
|
||||
<CopilotSidebar
|
||||
labels={{
|
||||
title: "SEO Assistant",
|
||||
initial: "Hi! 👋 I'm your SEO expert assistant. I can help you analyze your website, generate meta descriptions, check page speed, and much more. What would you like to work on today?",
|
||||
}}
|
||||
suggestions={displayedSuggestions}
|
||||
makeSystemMessage={(context: string, additionalInstructions?: string) => {
|
||||
const websiteUrl = analysisData?.url;
|
||||
const urlLine = websiteUrl ? `The user's current website URL is ${websiteUrl}. If the user does not provide a URL explicitly, default to this URL.` : '';
|
||||
const guidance = `
|
||||
You are ALwrity's SEO Expert Assistant. ${urlLine}
|
||||
Never ask for the URL if you already have it in context unless the user wants to switch URLs.
|
||||
Focus on actionable recommendations and use the registered tools.
|
||||
`.trim();
|
||||
return [guidance, additionalInstructions].filter(Boolean).join('\n\n');
|
||||
}}
|
||||
onSubmitMessage={(message: string) => {
|
||||
const text = (message || '').trim();
|
||||
const match = Object.keys(subSuggestionsByGroup).find(key => key.toLowerCase() === text.toLowerCase());
|
||||
if (match) {
|
||||
setChatSuggestions(subSuggestionsByGroup[match as keyof typeof subSuggestionsByGroup]);
|
||||
} else if (text.toLowerCase() === 'back' || text.toLowerCase() === 'categories') {
|
||||
setChatSuggestions(topLevelGroups);
|
||||
}
|
||||
}}
|
||||
observabilityHooks={{
|
||||
onChatExpanded: () => {
|
||||
if (enableDebugMode) {
|
||||
console.log('🔧 SEO CopilotKit Sidebar opened');
|
||||
}
|
||||
},
|
||||
onChatMinimized: () => {
|
||||
if (enableDebugMode) {
|
||||
console.log('🔧 SEO CopilotKit Sidebar closed');
|
||||
}
|
||||
},
|
||||
}}
|
||||
>
|
||||
<div className="seo-copilotkit-provider">
|
||||
{/* Suggestions controller sets progressive suggestions */}
|
||||
{/* SEOSuggestionsController */}
|
||||
{/* SEO CopilotKit Context - Provides data and instructions */}
|
||||
<SEOCopilotContext>
|
||||
{/* SEO CopilotKit Actions - Defines available actions */}
|
||||
<SEOCopilotActions />
|
||||
|
||||
{/* Loading indicator */}
|
||||
{isLoading && (
|
||||
<div className="seo-copilotkit-loading">
|
||||
<div className="loading-spinner">
|
||||
<div className="spinner"></div>
|
||||
<p>Loading SEO Assistant...</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Error display */}
|
||||
{error && (
|
||||
<div className="seo-copilotkit-error">
|
||||
<div className="error-message">
|
||||
<span className="error-icon">⚠️</span>
|
||||
<span className="error-text">{error}</span>
|
||||
<button
|
||||
className="error-dismiss"
|
||||
onClick={clearError}
|
||||
aria-label="Dismiss error"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Main content */}
|
||||
<div className="seo-copilotkit-content">
|
||||
{children}
|
||||
</div>
|
||||
</SEOCopilotContext>
|
||||
|
||||
{/* Copilot debug info removed */}
|
||||
|
||||
<style>{`
|
||||
.seo-copilotkit-provider {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.seo-copilotkit-loading {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.spinner {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border: 4px solid #f3f3f3;
|
||||
border-top: 4px solid #3498db;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
margin: 0 auto 10px;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.seo-copilotkit-error {
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
z-index: 1001;
|
||||
max-width: 400px;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
background: #fee;
|
||||
border: 1px solid #fcc;
|
||||
border-radius: 6px;
|
||||
padding: 12px 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.error-icon {
|
||||
font-size: 16px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.error-text {
|
||||
flex: 1;
|
||||
color: #c33;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.error-dismiss {
|
||||
background: none;
|
||||
border: none;
|
||||
color: #c33;
|
||||
font-size: 18px;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.error-dismiss:hover {
|
||||
background: rgba(204, 51, 51, 0.1);
|
||||
}
|
||||
|
||||
.seo-copilotkit-content {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.seo-copilotkit-debug {
|
||||
position: fixed;
|
||||
bottom: 20px;
|
||||
left: 20px;
|
||||
z-index: 999;
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
color: white;
|
||||
border-radius: 6px;
|
||||
padding: 8px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.seo-copilotkit-debug summary {
|
||||
cursor: pointer;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.seo-copilotkit-debug summary:hover {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.debug-content {
|
||||
padding: 8px;
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.2);
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.debug-content p {
|
||||
margin: 4px 0;
|
||||
font-size: 11px;
|
||||
}
|
||||
`}</style>
|
||||
</div>
|
||||
</CopilotSidebar>
|
||||
</CopilotKit>
|
||||
);
|
||||
};
|
||||
|
||||
export default SEOCopilotKitProvider;
|
||||
409
frontend/src/components/SEODashboard/SEOCopilotSuggestions.tsx
Normal file
409
frontend/src/components/SEODashboard/SEOCopilotSuggestions.tsx
Normal file
@@ -0,0 +1,409 @@
|
||||
// SEO CopilotKit Suggestions Component
|
||||
// Displays contextual suggestions based on current SEO data and user state
|
||||
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import { useSEOCopilotSuggestions } from '../../stores/seoCopilotStore';
|
||||
import { CopilotSuggestion } from '../../types/seoCopilotTypes';
|
||||
|
||||
interface SEOCopilotSuggestionsProps {
|
||||
maxSuggestions?: number;
|
||||
showCategories?: boolean;
|
||||
onSuggestionClick?: (suggestion: CopilotSuggestion) => void;
|
||||
}
|
||||
|
||||
const SEOCopilotSuggestionsComponent: React.FC<SEOCopilotSuggestionsProps> = ({
|
||||
maxSuggestions = 4,
|
||||
showCategories = true,
|
||||
onSuggestionClick
|
||||
}) => {
|
||||
const suggestions = useSEOCopilotSuggestions();
|
||||
const [expandedCategory, setExpandedCategory] = useState<string | null>(null);
|
||||
|
||||
// Group suggestions by category (memoized)
|
||||
const groupedSuggestions = useMemo(() => {
|
||||
return suggestions.reduce((acc, suggestion) => {
|
||||
if (!acc[suggestion.category]) {
|
||||
acc[suggestion.category] = [];
|
||||
}
|
||||
acc[suggestion.category].push(suggestion);
|
||||
return acc;
|
||||
}, {} as Record<string, CopilotSuggestion[]>);
|
||||
}, [suggestions]);
|
||||
|
||||
// Get category display info
|
||||
const getCategoryInfo = (category: string) => {
|
||||
const categoryInfo = {
|
||||
analysis: { icon: '🔍', name: 'Analysis', color: '#3B82F6' },
|
||||
optimization: { icon: '⚡', name: 'Optimization', color: '#10B981' },
|
||||
education: { icon: '🎓', name: 'Education', color: '#F59E0B' },
|
||||
monitoring: { icon: '📊', name: 'Monitoring', color: '#8B5CF6' }
|
||||
};
|
||||
return categoryInfo[category as keyof typeof categoryInfo] || { icon: '💡', name: category, color: '#6B7280' };
|
||||
};
|
||||
|
||||
// Get priority badge
|
||||
const getPriorityBadge = (priority: string) => {
|
||||
const priorityInfo = {
|
||||
high: { label: 'High', color: '#EF4444', bgColor: '#FEE2E2' },
|
||||
medium: { label: 'Medium', color: '#F59E0B', bgColor: '#FEF3C7' },
|
||||
low: { label: 'Low', color: '#10B981', bgColor: '#D1FAE5' }
|
||||
};
|
||||
return priorityInfo[priority as keyof typeof priorityInfo] || { label: priority, color: '#6B7280', bgColor: '#F3F4F6' };
|
||||
};
|
||||
|
||||
// Handle suggestion click
|
||||
const handleSuggestionClick = (suggestion: CopilotSuggestion) => {
|
||||
if (onSuggestionClick) {
|
||||
onSuggestionClick(suggestion);
|
||||
} else {
|
||||
// Default behavior - trigger the action
|
||||
console.log('Suggestion clicked:', suggestion);
|
||||
// Here you would typically trigger the CopilotKit action
|
||||
}
|
||||
};
|
||||
|
||||
// Render individual suggestion
|
||||
const renderSuggestion = (suggestion: CopilotSuggestion) => {
|
||||
const priorityBadge = getPriorityBadge(suggestion.priority);
|
||||
|
||||
return (
|
||||
<div
|
||||
key={suggestion.id}
|
||||
className="suggestion-item"
|
||||
onClick={() => handleSuggestionClick(suggestion)}
|
||||
>
|
||||
<div className="suggestion-header">
|
||||
<div className="suggestion-icon">{suggestion.icon}</div>
|
||||
<div className="suggestion-content">
|
||||
<h4 className="suggestion-title">{suggestion.title}</h4>
|
||||
<p className="suggestion-message">{suggestion.message}</p>
|
||||
</div>
|
||||
<div
|
||||
className="priority-badge"
|
||||
style={{
|
||||
color: priorityBadge.color,
|
||||
backgroundColor: priorityBadge.bgColor
|
||||
}}
|
||||
>
|
||||
{priorityBadge.label}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// Render category section
|
||||
const renderCategory = (category: string, categorySuggestions: CopilotSuggestion[]) => {
|
||||
const categoryInfo = getCategoryInfo(category);
|
||||
const isExpanded = expandedCategory === category;
|
||||
const displaySuggestions = isExpanded ? categorySuggestions : categorySuggestions.slice(0, 2);
|
||||
|
||||
return (
|
||||
<div key={category} className="suggestion-category">
|
||||
<div
|
||||
className="category-header"
|
||||
onClick={() => setExpandedCategory(isExpanded ? null : category)}
|
||||
>
|
||||
<div className="category-info">
|
||||
<span className="category-icon">{categoryInfo.icon}</span>
|
||||
<span className="category-name">{categoryInfo.name}</span>
|
||||
<span className="suggestion-count">({categorySuggestions.length})</span>
|
||||
</div>
|
||||
<div className="expand-icon">
|
||||
{isExpanded ? '−' : '+'}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={`category-suggestions ${isExpanded ? 'expanded' : ''}`}>
|
||||
{displaySuggestions.map(renderSuggestion)}
|
||||
{categorySuggestions.length > 2 && !isExpanded && (
|
||||
<div className="show-more">
|
||||
<button
|
||||
onClick={() => setExpandedCategory(category)}
|
||||
className="show-more-btn"
|
||||
>
|
||||
Show {categorySuggestions.length - 2} more suggestions
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
if (suggestions.length === 0) {
|
||||
return (
|
||||
<div className="seo-copilotkit-suggestions empty">
|
||||
<div className="empty-state">
|
||||
<div className="empty-icon">💡</div>
|
||||
<h3>No suggestions available</h3>
|
||||
<p>Start by analyzing your website to get personalized SEO suggestions.</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="seo-copilotkit-suggestions">
|
||||
<div className="suggestions-header">
|
||||
<h3 className="suggestions-title">
|
||||
<span className="title-icon">🎯</span>
|
||||
SEO Suggestions
|
||||
</h3>
|
||||
<p className="suggestions-subtitle">
|
||||
Personalized recommendations based on your current SEO data
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="suggestions-content">
|
||||
{showCategories ? (
|
||||
// Grouped by category
|
||||
Object.entries(groupedSuggestions).map(([category, categorySuggestions]) =>
|
||||
renderCategory(category, categorySuggestions)
|
||||
)
|
||||
) : (
|
||||
// Flat list
|
||||
<div className="suggestions-list">
|
||||
{suggestions.slice(0, maxSuggestions).map(renderSuggestion)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<style>{`
|
||||
.seo-copilotkit-suggestions {
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
overflow: hidden;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
}
|
||||
|
||||
.suggestions-header {
|
||||
padding: 20px 20px 16px;
|
||||
border-bottom: 1px solid #f3f4f6;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.suggestions-title {
|
||||
margin: 0 0 4px 0;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.title-icon {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.suggestions-subtitle {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.suggestions-content {
|
||||
max-height: 500px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.suggestion-category {
|
||||
border-bottom: 1px solid #f3f4f6;
|
||||
}
|
||||
|
||||
.suggestion-category:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.category-header {
|
||||
padding: 16px 20px;
|
||||
background: #f9fafb;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.category-header:hover {
|
||||
background: #f3f4f6;
|
||||
}
|
||||
|
||||
.category-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.category-icon {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.category-name {
|
||||
font-weight: 600;
|
||||
color: #374151;
|
||||
}
|
||||
|
||||
.suggestion-count {
|
||||
font-size: 12px;
|
||||
color: #6b7280;
|
||||
background: #e5e7eb;
|
||||
padding: 2px 6px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.expand-icon {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
color: #6b7280;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.category-suggestions {
|
||||
max-height: 0;
|
||||
overflow: hidden;
|
||||
transition: max-height 0.3s ease-out;
|
||||
}
|
||||
|
||||
.category-suggestions.expanded {
|
||||
max-height: 1000px;
|
||||
}
|
||||
|
||||
.suggestion-item {
|
||||
padding: 16px 20px;
|
||||
border-bottom: 1px solid #f3f4f6;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.suggestion-item:hover {
|
||||
background: #f9fafb;
|
||||
}
|
||||
|
||||
.suggestion-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.suggestion-header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.suggestion-icon {
|
||||
font-size: 20px;
|
||||
flex-shrink: 0;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.suggestion-content {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.suggestion-title {
|
||||
margin: 0 0 4px 0;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #111827;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.suggestion-message {
|
||||
margin: 0;
|
||||
font-size: 13px;
|
||||
color: #6b7280;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.priority-badge {
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
padding: 2px 6px;
|
||||
border-radius: 8px;
|
||||
flex-shrink: 0;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.show-more {
|
||||
padding: 12px 20px;
|
||||
text-align: center;
|
||||
border-top: 1px solid #f3f4f6;
|
||||
}
|
||||
|
||||
.show-more-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
color: #3b82f6;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.show-more-btn:hover {
|
||||
background: #eff6ff;
|
||||
}
|
||||
|
||||
.suggestions-list {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.empty {
|
||||
padding: 40px 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
font-size: 48px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.empty-state h3 {
|
||||
margin: 0 0 8px 0;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #374151;
|
||||
}
|
||||
|
||||
.empty-state p {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* Scrollbar styling */
|
||||
.suggestions-content::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
.suggestions-content::-webkit-scrollbar-track {
|
||||
background: #f1f5f9;
|
||||
}
|
||||
|
||||
.suggestions-content::-webkit-scrollbar-thumb {
|
||||
background: #cbd5e1;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.suggestions-content::-webkit-scrollbar-thumb:hover {
|
||||
background: #94a3b8;
|
||||
}
|
||||
`}</style>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const SEOCopilotSuggestions = React.memo(SEOCopilotSuggestionsComponent);
|
||||
export default SEOCopilotSuggestions;
|
||||
143
frontend/src/components/SEODashboard/SEOCopilotTest.tsx
Normal file
143
frontend/src/components/SEODashboard/SEOCopilotTest.tsx
Normal file
@@ -0,0 +1,143 @@
|
||||
// SEO CopilotKit Test Component
|
||||
// Simple test to verify CopilotKit sidebar functionality
|
||||
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Box, Button, Typography, Paper, Alert } from '@mui/material';
|
||||
import { useCopilotAction } from '@copilotkit/react-core';
|
||||
|
||||
const SEOCopilotTest: React.FC = () => {
|
||||
const [testResults, setTestResults] = useState<string[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
// Use type assertion to bypass TypeScript compilation issues
|
||||
const useCopilotActionTyped = useCopilotAction as any;
|
||||
|
||||
// Test action to verify CopilotKit is working
|
||||
useCopilotActionTyped({
|
||||
name: "testSEOCopilot",
|
||||
description: "Test action to verify SEO CopilotKit is working",
|
||||
parameters: [
|
||||
{
|
||||
name: "message",
|
||||
type: "string",
|
||||
description: "Test message to display",
|
||||
required: true
|
||||
}
|
||||
],
|
||||
handler: async (args: any) => {
|
||||
const { message } = args;
|
||||
setTestResults(prev => [...prev, `✅ CopilotKit Action Test: ${message}`]);
|
||||
return {
|
||||
success: true,
|
||||
message: `Test completed successfully: ${message}`,
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
const runTest = async () => {
|
||||
setIsLoading(true);
|
||||
setTestResults([]);
|
||||
|
||||
try {
|
||||
// Test 1: Check if CopilotKit context is available
|
||||
setTestResults(prev => [...prev, '🔍 Testing CopilotKit Context...']);
|
||||
|
||||
// Test 2: Check if actions are registered
|
||||
setTestResults(prev => [...prev, '🔍 Testing Action Registration...']);
|
||||
|
||||
// Test 3: Check if sidebar should be visible
|
||||
setTestResults(prev => [...prev, '🔍 Testing Sidebar Visibility...']);
|
||||
setTestResults(prev => [...prev, '💡 Look for the chat icon in the bottom right corner']);
|
||||
setTestResults(prev => [...prev, '💡 Try pressing Ctrl+/ (or Cmd+/ on Mac) to open the sidebar']);
|
||||
|
||||
// Test 4: Check environment variables
|
||||
const apiKey = process.env.REACT_APP_COPILOTKIT_API_KEY;
|
||||
setTestResults(prev => [...prev, `🔑 API Key Status: ${apiKey ? 'Configured' : 'Missing'}`]);
|
||||
|
||||
// Test 5: Check if provider is wrapped correctly
|
||||
setTestResults(prev => [...prev, '🔍 Testing Provider Wrapping...']);
|
||||
setTestResults(prev => [...prev, '✅ Provider should be wrapped around SEO Dashboard']);
|
||||
|
||||
} catch (error) {
|
||||
setTestResults(prev => [...prev, `❌ Test Error: ${error}`]);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const clearResults = () => {
|
||||
setTestResults([]);
|
||||
};
|
||||
|
||||
return (
|
||||
<Paper elevation={3} sx={{ p: 3, mb: 3 }}>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
🧪 SEO CopilotKit Test Panel
|
||||
</Typography>
|
||||
|
||||
<Typography variant="body2" sx={{ mb: 2, color: 'text.secondary' }}>
|
||||
This panel helps verify that the CopilotKit sidebar is working correctly.
|
||||
</Typography>
|
||||
|
||||
<Box sx={{ mb: 2 }}>
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={runTest}
|
||||
disabled={isLoading}
|
||||
sx={{ mr: 1 }}
|
||||
>
|
||||
{isLoading ? 'Running Tests...' : 'Run Tests'}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="outlined"
|
||||
onClick={clearResults}
|
||||
disabled={testResults.length === 0}
|
||||
>
|
||||
Clear Results
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
{testResults.length > 0 && (
|
||||
<Box sx={{ mt: 2 }}>
|
||||
<Typography variant="subtitle2" gutterBottom>
|
||||
Test Results:
|
||||
</Typography>
|
||||
|
||||
{testResults.map((result, index) => (
|
||||
<Alert
|
||||
key={index}
|
||||
severity={result.includes('❌') ? 'error' : result.includes('✅') ? 'success' : 'info'}
|
||||
sx={{ mb: 1 }}
|
||||
>
|
||||
{result}
|
||||
</Alert>
|
||||
))}
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<Box sx={{ mt: 3, p: 2, bgcolor: 'grey.50', borderRadius: 1 }}>
|
||||
<Typography variant="subtitle2" gutterBottom>
|
||||
📋 How to Test the CopilotKit Sidebar:
|
||||
</Typography>
|
||||
|
||||
<Typography variant="body2" component="div" sx={{ pl: 1 }}>
|
||||
<ol>
|
||||
<li>Look for a chat icon in the bottom right corner of the screen</li>
|
||||
<li>Click the icon to open the CopilotKit sidebar</li>
|
||||
<li>Try typing: "Test the SEO assistant"</li>
|
||||
<li>Ask: "What SEO actions are available?"</li>
|
||||
<li>Try: "Analyze my website SEO"</li>
|
||||
</ol>
|
||||
</Typography>
|
||||
|
||||
<Typography variant="body2" sx={{ mt: 1, fontStyle: 'italic' }}>
|
||||
💡 Keyboard shortcut: Press Ctrl+/ (or Cmd+/ on Mac) to quickly open the sidebar
|
||||
</Typography>
|
||||
</Box>
|
||||
</Paper>
|
||||
);
|
||||
};
|
||||
|
||||
export default SEOCopilotTest;
|
||||
@@ -6,13 +6,18 @@ import {
|
||||
Typography,
|
||||
Alert,
|
||||
Skeleton,
|
||||
useTheme
|
||||
useTheme,
|
||||
Chip,
|
||||
Button
|
||||
} from '@mui/material';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
|
||||
// Shared components
|
||||
import { DashboardContainer, GlassCard } from '../shared/styled';
|
||||
import SEOAnalyzerPanel from './components/SEOAnalyzerPanel';
|
||||
import { SEOCopilotKitProvider, SEOCopilotSuggestions } from './index';
|
||||
// Removed SEOCopilotTest
|
||||
import useSEOCopilotStore from '../../stores/seoCopilotStore';
|
||||
|
||||
// Zustand store
|
||||
import { useSEODashboardStore } from '../../stores/seoDashboardStore';
|
||||
@@ -37,38 +42,47 @@ const SEODashboard: React.FC = () => {
|
||||
setError,
|
||||
runSEOAnalysis,
|
||||
checkAndRunInitialAnalysis,
|
||||
refreshSEOAnalysis,
|
||||
getAnalysisFreshness,
|
||||
} = useSEODashboardStore();
|
||||
|
||||
// Sync dashboard analysis to Copilot store so readables have URL/context
|
||||
const setCopilotAnalysisData = useSEOCopilotStore(state => state.setAnalysisData);
|
||||
useEffect(() => {
|
||||
if (analysisData) {
|
||||
setCopilotAnalysisData(analysisData as any);
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
console.log('[CopilotSync] Pushed analysis to Copilot store', analysisData?.url);
|
||||
}
|
||||
}
|
||||
}, [analysisData, setCopilotAnalysisData]);
|
||||
|
||||
useEffect(() => {
|
||||
// Simulate fetching dashboard data
|
||||
const fetchData = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
// Try to get the website URL from the database
|
||||
let websiteUrl = null;
|
||||
try {
|
||||
websiteUrl = await userDataAPI.getWebsiteURL();
|
||||
console.log('Fetched website URL from database:', websiteUrl);
|
||||
} catch (error) {
|
||||
console.warn('Could not fetch website URL from database:', error);
|
||||
}
|
||||
setLoading(true);
|
||||
|
||||
// Mock data for now
|
||||
// Get user's website URL from user data
|
||||
const userData = await userDataAPI.getUserData();
|
||||
const websiteUrl = userData?.website_url || 'https://alwrity.com';
|
||||
|
||||
// Mock data for demonstration
|
||||
const mockData = {
|
||||
health_score: {
|
||||
score: 85,
|
||||
score: 84,
|
||||
change: 5,
|
||||
trend: 'up',
|
||||
label: 'GOOD',
|
||||
label: 'EXCELLENT',
|
||||
color: '#4CAF50'
|
||||
},
|
||||
key_insight: 'Your SEO is performing well with room for improvement',
|
||||
priority_alert: 'No critical issues detected',
|
||||
key_insight: 'Your website has excellent technical SEO foundation with room for improvement',
|
||||
priority_alert: 'Mobile page speed could be optimized further',
|
||||
metrics: {
|
||||
traffic: { value: 12500, change: 12, trend: 'up', description: 'Organic traffic', color: '#4CAF50' },
|
||||
rankings: { value: 8.5, change: -0.3, trend: 'down', description: 'Average ranking', color: '#2196F3' },
|
||||
mobile: { value: 92, change: 3, trend: 'up', description: 'Mobile speed', color: '#FF9800' },
|
||||
keywords: { value: 150, change: 5, trend: 'up', description: 'Keywords tracked', color: '#9C27B0' }
|
||||
traffic: { value: 12500, change: 15, trend: 'up', description: 'Organic traffic', color: '#4CAF50' },
|
||||
rankings: { value: 8.5, change: 2.3, trend: 'up', description: 'Average ranking', color: '#2196F3' },
|
||||
mobile: { value: 92, change: -3, trend: 'down', description: 'Mobile speed', color: '#FF9800' },
|
||||
keywords: { value: 150, change: 12, trend: 'up', description: 'Keywords tracked', color: '#9C27B0' }
|
||||
},
|
||||
platforms: {
|
||||
google: { status: 'connected', connected: true, last_sync: '2024-01-15T10:30:00Z', data_points: 1250 },
|
||||
@@ -76,6 +90,12 @@ const SEODashboard: React.FC = () => {
|
||||
yandex: { status: 'disconnected', connected: false }
|
||||
},
|
||||
ai_insights: [
|
||||
{
|
||||
insight: 'Your website has excellent technical SEO foundation',
|
||||
priority: 'low',
|
||||
category: 'technical',
|
||||
action_required: false
|
||||
},
|
||||
{
|
||||
insight: 'Consider adding more internal links to improve page authority',
|
||||
priority: 'medium',
|
||||
@@ -103,14 +123,15 @@ const SEODashboard: React.FC = () => {
|
||||
};
|
||||
|
||||
fetchData();
|
||||
}, [setData, setLoading, setError]);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
// Run initial SEO analysis if no data exists
|
||||
if (!loading && !error && data) {
|
||||
checkAndRunInitialAnalysis();
|
||||
// Call via store to avoid changing function identity in deps
|
||||
useSEODashboardStore.getState().checkAndRunInitialAnalysis();
|
||||
}
|
||||
}, [loading, error, data, checkAndRunInitialAnalysis]);
|
||||
}, [loading, error, data]);
|
||||
|
||||
if (loading) {
|
||||
return <Skeleton variant="rectangular" height={200} />;
|
||||
@@ -121,84 +142,127 @@ const SEODashboard: React.FC = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<DashboardContainer>
|
||||
<Container maxWidth="xl">
|
||||
<AnimatePresence>
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.6 }}
|
||||
>
|
||||
{/* Header */}
|
||||
<Box sx={{ mb: 4 }}>
|
||||
<Typography variant="h4" sx={{ color: 'white', fontWeight: 700 }}>
|
||||
🔍 SEO Dashboard
|
||||
</Typography>
|
||||
<Typography variant="subtitle1" sx={{ color: 'rgba(255, 255, 255, 0.7)' }}>
|
||||
AI-powered insights and actionable recommendations
|
||||
</Typography>
|
||||
</Box>
|
||||
<SEOCopilotKitProvider enableDebugMode={false}>
|
||||
<DashboardContainer>
|
||||
<Container maxWidth="xl">
|
||||
<AnimatePresence>
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.6 }}
|
||||
>
|
||||
{/* Header */}
|
||||
<Box sx={{ mb: 4, display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
<Box>
|
||||
<Typography variant="h4" sx={{ color: 'white', fontWeight: 700 }}>
|
||||
🔍 SEO Dashboard
|
||||
</Typography>
|
||||
<Typography variant="subtitle1" sx={{ color: 'rgba(255, 255, 255, 0.7)' }}>
|
||||
AI-powered insights and actionable recommendations
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
{(() => {
|
||||
const freshness = getAnalysisFreshness();
|
||||
const chipColor = freshness.isStale ? 'rgba(255, 193, 7, 0.25)' : 'rgba(76, 175, 80, 0.25)';
|
||||
const chipBorder = freshness.isStale ? 'rgba(255, 193, 7, 0.45)' : 'rgba(76, 175, 80, 0.45)';
|
||||
return (
|
||||
<Chip
|
||||
label={`Freshness: ${freshness.label}`}
|
||||
size="small"
|
||||
sx={{
|
||||
bgcolor: chipColor,
|
||||
border: `1px solid ${chipBorder}`,
|
||||
color: 'white',
|
||||
fontWeight: 600
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})()}
|
||||
<Button
|
||||
onClick={refreshSEOAnalysis}
|
||||
disabled={analysisLoading}
|
||||
variant="outlined"
|
||||
size="small"
|
||||
sx={{
|
||||
color: 'white',
|
||||
borderColor: 'rgba(255, 255, 255, 0.6)',
|
||||
'&:hover': { borderColor: 'rgba(255, 255, 255, 0.9)' }
|
||||
}}
|
||||
>
|
||||
{analysisLoading ? 'Refreshing…' : 'Refresh'}
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* Executive Summary */}
|
||||
<Box sx={{ mb: 4 }}>
|
||||
<Typography variant="h6" sx={{ color: 'white', fontWeight: 600, mb: 2 }}>
|
||||
📊 Performance Overview
|
||||
</Typography>
|
||||
<Grid container spacing={2}>
|
||||
<Grid item xs={6} sm={3}>
|
||||
<GlassCard sx={{ p: 2 }}>
|
||||
<Typography variant="body2" sx={{ color: 'rgba(255, 255, 255, 0.7)' }}>
|
||||
Organic Traffic
|
||||
</Typography>
|
||||
<Typography variant="h5" sx={{ color: '#4CAF50' }}>
|
||||
{data.metrics.traffic.value}
|
||||
</Typography>
|
||||
</GlassCard>
|
||||
</Grid>
|
||||
<Grid item xs={6} sm={3}>
|
||||
<GlassCard sx={{ p: 2 }}>
|
||||
<Typography variant="body2" sx={{ color: 'rgba(255, 255, 255, 0.7)' }}>
|
||||
Average Ranking
|
||||
</Typography>
|
||||
<Typography variant="h5" sx={{ color: '#2196F3' }}>
|
||||
{data.metrics.rankings.value}
|
||||
</Typography>
|
||||
</GlassCard>
|
||||
</Grid>
|
||||
<Grid item xs={6} sm={3}>
|
||||
<GlassCard sx={{ p: 2 }}>
|
||||
<Typography variant="body2" sx={{ color: 'rgba(255, 255, 255, 0.7)' }}>
|
||||
Mobile Speed
|
||||
</Typography>
|
||||
<Typography variant="h5" sx={{ color: '#FF9800' }}>
|
||||
{data.metrics.mobile.value}
|
||||
</Typography>
|
||||
</GlassCard>
|
||||
</Grid>
|
||||
<Grid item xs={6} sm={3}>
|
||||
<GlassCard sx={{ p: 2 }}>
|
||||
<Typography variant="body2" sx={{ color: 'rgba(255, 255, 255, 0.7)' }}>
|
||||
Keywords Tracked
|
||||
</Typography>
|
||||
<Typography variant="h5" sx={{ color: '#9C27B0' }}>
|
||||
{data.metrics.keywords.value}
|
||||
</Typography>
|
||||
</GlassCard>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Box>
|
||||
{/* CopilotKit Test Panel removed */}
|
||||
|
||||
{/* SEO Analyzer Panel */}
|
||||
<SEOAnalyzerPanel
|
||||
analysisData={analysisData}
|
||||
onRunAnalysis={runSEOAnalysis}
|
||||
loading={analysisLoading}
|
||||
error={analysisError}
|
||||
/>
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
</Container>
|
||||
</DashboardContainer>
|
||||
{/* Executive Summary */}
|
||||
<Box sx={{ mb: 4 }}>
|
||||
<Typography variant="h6" sx={{ color: 'white', fontWeight: 600, mb: 2 }}>
|
||||
📊 Performance Overview
|
||||
</Typography>
|
||||
<Grid container spacing={2}>
|
||||
<Grid item xs={6} sm={3}>
|
||||
<GlassCard sx={{ p: 2 }}>
|
||||
<Typography variant="body2" sx={{ color: 'rgba(255, 255, 255, 0.7)' }}>
|
||||
Organic Traffic
|
||||
</Typography>
|
||||
<Typography variant="h5" sx={{ color: '#4CAF50' }}>
|
||||
{data.metrics.traffic.value}
|
||||
</Typography>
|
||||
</GlassCard>
|
||||
</Grid>
|
||||
<Grid item xs={6} sm={3}>
|
||||
<GlassCard sx={{ p: 2 }}>
|
||||
<Typography variant="body2" sx={{ color: 'rgba(255, 255, 255, 0.7)' }}>
|
||||
Average Ranking
|
||||
</Typography>
|
||||
<Typography variant="h5" sx={{ color: '#2196F3' }}>
|
||||
{data.metrics.rankings.value}
|
||||
</Typography>
|
||||
</GlassCard>
|
||||
</Grid>
|
||||
<Grid item xs={6} sm={3}>
|
||||
<GlassCard sx={{ p: 2 }}>
|
||||
<Typography variant="body2" sx={{ color: 'rgba(255, 255, 255, 0.7)' }}>
|
||||
Mobile Speed
|
||||
</Typography>
|
||||
<Typography variant="h5" sx={{ color: '#FF9800' }}>
|
||||
{data.metrics.mobile.value}
|
||||
</Typography>
|
||||
</GlassCard>
|
||||
</Grid>
|
||||
<Grid item xs={6} sm={3}>
|
||||
<GlassCard sx={{ p: 2 }}>
|
||||
<Typography variant="body2" sx={{ color: 'rgba(255, 255, 255, 0.7)' }}>
|
||||
Keywords Tracked
|
||||
</Typography>
|
||||
<Typography variant="h5" sx={{ color: '#9C27B0' }}>
|
||||
{data.metrics.keywords.value}
|
||||
</Typography>
|
||||
</GlassCard>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Box>
|
||||
|
||||
{/* SEO Analyzer Panel */}
|
||||
<SEOAnalyzerPanel
|
||||
analysisData={analysisData}
|
||||
onRunAnalysis={runSEOAnalysis}
|
||||
loading={analysisLoading}
|
||||
error={analysisError}
|
||||
/>
|
||||
|
||||
{/* Copilot Suggestions Panel */}
|
||||
<Box sx={{ mt: 4 }}>
|
||||
<SEOCopilotSuggestions />
|
||||
</Box>
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
</Container>
|
||||
</DashboardContainer>
|
||||
</SEOCopilotKitProvider>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
import React, { useEffect, useMemo } from 'react';
|
||||
import { useCopilotChat } from '@copilotkit/react-core';
|
||||
|
||||
// A lightweight controller that sets top-level suggestion groups and
|
||||
// updates sub-suggestions based on the latest user message.
|
||||
const SEOSuggestionsController: React.FC = () => {
|
||||
// Use a permissive cast to support variations across library versions
|
||||
const chat = useCopilotChat() as any;
|
||||
const messages = (chat && chat.messages) || [];
|
||||
const setSuggestions: ((s: { title: string; message: string }[]) => void) =
|
||||
(chat && chat.setSuggestions) || (() => {});
|
||||
|
||||
// Top-level groups for progressive disclosure
|
||||
const topLevelGroups = useMemo(
|
||||
() => [
|
||||
{ title: 'Content analysis', message: 'Content analysis' },
|
||||
{ title: 'Website/URL analysis', message: 'Web URL analysis' },
|
||||
{ title: 'Technical SEO', message: 'Technical SEO' },
|
||||
{ title: 'Strategy & planning', message: 'Strategy and planning' },
|
||||
{ title: 'Monitoring & health', message: 'Monitoring and health' }
|
||||
],
|
||||
[]
|
||||
);
|
||||
|
||||
// Sub-suggestions mapped by group selection
|
||||
const subSuggestionsByGroup = useMemo(
|
||||
() => ({
|
||||
'Content analysis': [
|
||||
{ title: 'Comprehensive content analysis', message: 'Analyze content comprehensively for my site' },
|
||||
{ title: 'Optimize page content', message: 'Optimize page content for SEO' },
|
||||
{ title: 'Generate meta descriptions', message: 'Generate meta descriptions for key pages' }
|
||||
],
|
||||
'Web URL analysis': [
|
||||
{ title: 'Comprehensive SEO analysis', message: 'Run comprehensive SEO analysis for a URL' },
|
||||
{ title: 'Analyze page speed', message: 'Analyze page speed for a URL' },
|
||||
{ title: 'Analyze sitemap', message: 'Analyze sitemap for my site' },
|
||||
{ title: 'Generate OpenGraph tags', message: 'Generate OpenGraph tags for a URL' }
|
||||
],
|
||||
'Technical SEO': [
|
||||
{ title: 'Technical SEO audit', message: 'Run a technical SEO audit' },
|
||||
{ title: 'Check SEO health', message: 'Check overall SEO health' },
|
||||
{ title: 'Image alt text', message: 'Generate image alt text for pages' }
|
||||
],
|
||||
'Strategy and planning': [
|
||||
{ title: 'Enterprise SEO analysis', message: 'Run enterprise SEO analysis' },
|
||||
{ title: 'Content strategy', message: 'Analyze content strategy and recommendations' },
|
||||
{ title: 'Customize SEO dashboard', message: 'Customize the SEO dashboard' }
|
||||
],
|
||||
'Monitoring and health': [
|
||||
{ title: 'Website audit', message: 'Perform a website audit' },
|
||||
{ title: 'Update SEO charts', message: 'Update SEO charts and visualizations' },
|
||||
{ title: 'Explain an SEO concept', message: 'Explain an SEO concept in simple terms' }
|
||||
]
|
||||
}),
|
||||
[]
|
||||
);
|
||||
|
||||
// Initialize top-level suggestions on mount
|
||||
useEffect(() => {
|
||||
setSuggestions(topLevelGroups);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
// When the latest user message matches a group name, show its sub-suggestions
|
||||
useEffect(() => {
|
||||
if (!messages || messages.length === 0) return;
|
||||
const last = messages[messages.length - 1];
|
||||
if (last?.role !== 'user') return;
|
||||
|
||||
const text = (last.content || '').trim();
|
||||
const group = Object.keys(subSuggestionsByGroup).find(
|
||||
key => key.toLowerCase() === text.toLowerCase()
|
||||
);
|
||||
|
||||
if (group) {
|
||||
setSuggestions(subSuggestionsByGroup[group as keyof typeof subSuggestionsByGroup]);
|
||||
} else {
|
||||
if (text.length > 0 && !Object.keys(subSuggestionsByGroup).some(k => text.toLowerCase().includes(k.toLowerCase()))) {
|
||||
setSuggestions(topLevelGroups);
|
||||
}
|
||||
}
|
||||
}, [messages, setSuggestions, subSuggestionsByGroup, topLevelGroups]);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export default SEOSuggestionsController;
|
||||
@@ -0,0 +1,75 @@
|
||||
import React from 'react';
|
||||
import { useCopilotActionTyped, useExecute } from './helpers';
|
||||
import { seoApiService } from '../../../services/seoApiService';
|
||||
|
||||
const MetaUI: React.FC<{ args: any; respond: (data: any) => void }> = ({ args, respond }) => {
|
||||
const [keywords, setKeywords] = React.useState<string>((args?.keywords || []).join(', '));
|
||||
const [tone, setTone] = React.useState<string>(args?.tone || 'professional');
|
||||
const [isRunning, setIsRunning] = React.useState(false);
|
||||
const [result, setResult] = React.useState<any>(null);
|
||||
const [error, setError] = React.useState<string | null>(null);
|
||||
const tones = ['professional', 'casual', 'technical', 'friendly', 'persuasive'];
|
||||
|
||||
const run = async () => {
|
||||
try {
|
||||
setIsRunning(true);
|
||||
setError(null);
|
||||
const parsedKeywords = keywords.split(',').map(k => k.trim()).filter(Boolean);
|
||||
if (!parsedKeywords.length) throw new Error('Please provide at least one keyword');
|
||||
const res = await seoApiService.generateMetaDescriptions({ keywords: parsedKeywords, tone });
|
||||
setResult(res);
|
||||
respond({ success: true, keywords: parsedKeywords, tone, result: res });
|
||||
} catch (e: any) {
|
||||
setError(e?.message || 'Failed to generate meta descriptions');
|
||||
} finally {
|
||||
setIsRunning(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ padding: 12 }}>
|
||||
<div style={{ marginBottom: 8, fontWeight: 600 }}>Meta description generation</div>
|
||||
<div style={{ marginBottom: 8 }}>
|
||||
<div style={{ fontSize: 12, marginBottom: 4 }}>Target keywords (comma-separated)</div>
|
||||
<input type="text" value={keywords} onChange={(e) => setKeywords(e.target.value)} style={{ width: '100%', padding: 6, fontSize: 12 }} />
|
||||
</div>
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 6, marginBottom: 12 }}>
|
||||
{tones.map(t => (
|
||||
<button key={t} onClick={() => setTone(t)} style={{ padding: '4px 8px', fontSize: 12, borderRadius: 12, border: '1px solid #ddd', background: tone === t ? '#eef2ff' : 'white' }}>{t}</button>
|
||||
))}
|
||||
</div>
|
||||
<button onClick={run} disabled={isRunning} style={{ padding: '6px 10px' }}>{isRunning ? 'Generating…' : 'Generate'}</button>
|
||||
{error && <div style={{ marginTop: 10, color: '#c33', fontSize: 12 }}>{error}</div>}
|
||||
{result && (
|
||||
<div style={{ marginTop: 12, fontSize: 12 }}>
|
||||
<pre style={{ whiteSpace: 'pre-wrap' }}>{JSON.stringify(result, null, 2)}</pre>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const RegisterMetaDescription: React.FC = () => {
|
||||
const execute = useExecute();
|
||||
const useAction = useCopilotActionTyped();
|
||||
|
||||
useAction({
|
||||
name: 'generateMetaDescriptions',
|
||||
description: 'Generate optimized meta descriptions for web pages',
|
||||
parameters: [
|
||||
{ name: 'keywords', type: 'string[]', description: 'Target keywords', required: true },
|
||||
{ name: 'tone', type: 'string', description: 'Tone (professional, casual, technical, friendly, persuasive)', required: false }
|
||||
],
|
||||
renderAndWaitForResponse: ({ args, respond }: any) => <MetaUI args={args} respond={respond} />,
|
||||
handler: async (args: any) => {
|
||||
const parsedKeywords: string[] = Array.isArray(args?.keywords)
|
||||
? args.keywords
|
||||
: String(args?.keywords || '').split(',').map((k: string) => k.trim()).filter(Boolean);
|
||||
return await execute('generateMetaDescriptions', { keywords: parsedKeywords, tone: args?.tone });
|
||||
}
|
||||
});
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export default RegisterMetaDescription;
|
||||
@@ -0,0 +1,87 @@
|
||||
import React from 'react';
|
||||
import { useCopilotActionTyped, useExecute, getDefaultUrl } from './helpers';
|
||||
import { seoApiService } from '../../../services/seoApiService';
|
||||
|
||||
const OnPageUI: React.FC<{ args: any; respond: (data: any) => void }> = ({ args, respond }) => {
|
||||
const [keywords, setKeywords] = React.useState<string>((args?.targetKeywords || []).join(', '));
|
||||
const [analyzeImages, setAnalyzeImages] = React.useState<boolean>(!!args?.analyzeImages);
|
||||
const [analyzeContentQuality, setAnalyzeContentQuality] = React.useState<boolean>(!!args?.analyzeContentQuality);
|
||||
const [isRunning, setIsRunning] = React.useState(false);
|
||||
const [result, setResult] = React.useState<any>(null);
|
||||
const [error, setError] = React.useState<string | null>(null);
|
||||
const url = args?.url || getDefaultUrl();
|
||||
|
||||
const run = async () => {
|
||||
try {
|
||||
setIsRunning(true);
|
||||
setError(null);
|
||||
if (!url) throw new Error('No URL available');
|
||||
const parsedKeywords = keywords.split(',').map(k => k.trim()).filter(Boolean);
|
||||
const res = await seoApiService.analyzeOnPageSEO({
|
||||
url,
|
||||
target_keywords: parsedKeywords.length ? parsedKeywords : undefined,
|
||||
analyze_images: analyzeImages,
|
||||
analyze_content_quality: analyzeContentQuality
|
||||
});
|
||||
setResult(res);
|
||||
respond({ success: true, url, targetKeywords: parsedKeywords, analyzeImages, analyzeContentQuality, result: res });
|
||||
} catch (e: any) {
|
||||
setError(e?.message || 'Failed to analyze on-page SEO');
|
||||
} finally {
|
||||
setIsRunning(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ padding: 12 }}>
|
||||
<div style={{ marginBottom: 8, fontWeight: 600 }}>On-page SEO analysis</div>
|
||||
<div style={{ marginBottom: 8, fontSize: 12, color: '#555' }}>URL: {url || 'Not available'}</div>
|
||||
<div style={{ marginBottom: 8 }}>
|
||||
<div style={{ fontSize: 12, marginBottom: 4 }}>Target keywords (comma-separated)</div>
|
||||
<input type="text" value={keywords} onChange={(e) => setKeywords(e.target.value)} style={{ width: '100%', padding: 6, fontSize: 12 }} />
|
||||
</div>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 6, marginBottom: 12 }}>
|
||||
<label style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
||||
<input type="checkbox" checked={analyzeImages} onChange={(e) => setAnalyzeImages(e.target.checked)} />
|
||||
Analyze images
|
||||
</label>
|
||||
<label style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
||||
<input type="checkbox" checked={analyzeContentQuality} onChange={(e) => setAnalyzeContentQuality(e.target.checked)} />
|
||||
Analyze content quality
|
||||
</label>
|
||||
</div>
|
||||
<button onClick={run} disabled={isRunning} style={{ padding: '6px 10px' }}>{isRunning ? 'Analyzing…' : 'Run analysis'}</button>
|
||||
{error && <div style={{ marginTop: 10, color: '#c33', fontSize: 12 }}>{error}</div>}
|
||||
{result && (
|
||||
<div style={{ marginTop: 12, fontSize: 12 }}>
|
||||
<pre style={{ whiteSpace: 'pre-wrap' }}>{JSON.stringify(result, null, 2)}</pre>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const RegisterOnPage: React.FC = () => {
|
||||
const execute = useExecute();
|
||||
const useAction = useCopilotActionTyped();
|
||||
|
||||
useAction({
|
||||
name: 'analyzeOnPageSEO',
|
||||
description: 'Analyze on-page SEO elements and provide optimization recommendations',
|
||||
parameters: [
|
||||
{ name: 'url', type: 'string', description: 'URL to analyze (optional)', required: false },
|
||||
{ name: 'targetKeywords', type: 'string[]', description: 'Target keywords (optional)', required: false },
|
||||
{ name: 'analyzeImages', type: 'boolean', description: 'Analyze images', required: false },
|
||||
{ name: 'analyzeContentQuality', type: 'boolean', description: 'Analyze content quality', required: false }
|
||||
],
|
||||
renderAndWaitForResponse: ({ args, respond }: any) => <OnPageUI args={args} respond={respond} />,
|
||||
handler: async (args: any) => {
|
||||
const url = args?.url || getDefaultUrl();
|
||||
return await execute('analyzeOnPageSEO', { ...args, url });
|
||||
}
|
||||
});
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export default RegisterOnPage;
|
||||
@@ -0,0 +1,87 @@
|
||||
import React from 'react';
|
||||
import { useCopilotActionTyped, useExecute, getDefaultUrl } from './helpers';
|
||||
import { seoApiService } from '../../../services/seoApiService';
|
||||
|
||||
const PageSpeedUI: React.FC<{ args: any; respond: (data: any) => void }> = ({ args, respond }) => {
|
||||
const [device, setDevice] = React.useState<string>(args?.device || 'mobile');
|
||||
const [isRunning, setIsRunning] = React.useState(false);
|
||||
const [result, setResult] = React.useState<any>(null);
|
||||
const [error, setError] = React.useState<string | null>(null);
|
||||
const url = args?.url || getDefaultUrl();
|
||||
|
||||
const run = async () => {
|
||||
try {
|
||||
setIsRunning(true);
|
||||
setError(null);
|
||||
if (!url) throw new Error('No URL available');
|
||||
if (device === 'both') {
|
||||
const [mobile, desktop] = await Promise.all([
|
||||
seoApiService.analyzePageSpeed({ url, strategy: 'MOBILE' }),
|
||||
seoApiService.analyzePageSpeed({ url, strategy: 'DESKTOP' })
|
||||
]);
|
||||
setResult({ mobile, desktop });
|
||||
respond({ success: true, url, device: 'both', mobile, desktop });
|
||||
} else if (device === 'desktop') {
|
||||
const desktop = await seoApiService.analyzePageSpeed({ url, strategy: 'DESKTOP' });
|
||||
setResult({ desktop });
|
||||
respond({ success: true, url, device: 'desktop', desktop });
|
||||
} else {
|
||||
const mobile = await seoApiService.analyzePageSpeed({ url, strategy: 'MOBILE' });
|
||||
setResult({ mobile });
|
||||
respond({ success: true, url, device: 'mobile', mobile });
|
||||
}
|
||||
} catch (e: any) {
|
||||
setError(e?.message || 'Failed to run page speed analysis');
|
||||
} finally {
|
||||
setIsRunning(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ padding: 12 }}>
|
||||
<div style={{ marginBottom: 8, fontWeight: 600 }}>PageSpeed analysis</div>
|
||||
<div style={{ marginBottom: 8, fontSize: 12, color: '#555' }}>URL: {url || 'Not available'}</div>
|
||||
<div style={{ display: 'flex', gap: 8, marginBottom: 12 }}>
|
||||
{['mobile', 'desktop', 'both'].map((d) => (
|
||||
<label key={d} style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
|
||||
<input type="radio" name="device" value={d} checked={device === d} onChange={() => setDevice(d)} />
|
||||
{d}
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
<button onClick={run} disabled={isRunning} style={{ padding: '6px 10px' }}>
|
||||
{isRunning ? 'Analyzing…' : 'Run analysis'}
|
||||
</button>
|
||||
{error && <div style={{ marginTop: 10, color: '#c33', fontSize: 12 }}>{error}</div>}
|
||||
{result && (
|
||||
<div style={{ marginTop: 12, fontSize: 12 }}>
|
||||
<pre style={{ whiteSpace: 'pre-wrap' }}>{JSON.stringify(result, null, 2)}</pre>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const RegisterPageSpeed: React.FC = () => {
|
||||
const execute = useExecute();
|
||||
const useAction = useCopilotActionTyped();
|
||||
|
||||
useAction({
|
||||
name: 'analyzePageSpeed',
|
||||
description: 'Analyze website performance and page speed metrics',
|
||||
parameters: [
|
||||
{ name: 'url', type: 'string', description: 'URL to analyze (optional)', required: false },
|
||||
{ name: 'device', type: 'string', description: 'mobile | desktop | both (optional)', required: false }
|
||||
],
|
||||
renderAndWaitForResponse: ({ args, respond }: any) => <PageSpeedUI args={args} respond={respond} />,
|
||||
handler: async (args: any) => {
|
||||
const url = args?.url || getDefaultUrl();
|
||||
const device = args?.device || 'MOBILE';
|
||||
return await execute('analyzePageSpeed', { ...args, url, device });
|
||||
}
|
||||
});
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export default RegisterPageSpeed;
|
||||
@@ -0,0 +1,81 @@
|
||||
import React from 'react';
|
||||
import { useCopilotActionTyped, useExecute, getDefaultUrl } from './helpers';
|
||||
import { seoApiService } from '../../../services/seoApiService';
|
||||
|
||||
const SitemapUI: React.FC<{ args: any; respond: (data: any) => void }> = ({ args, respond }) => {
|
||||
const [analyzeContentTrends, setAnalyzeContentTrends] = React.useState<boolean>(!!args?.analyzeContentTrends);
|
||||
const [analyzePublishingPatterns, setAnalyzePublishingPatterns] = React.useState<boolean>(!!args?.analyzePublishingPatterns);
|
||||
const [isRunning, setIsRunning] = React.useState(false);
|
||||
const [result, setResult] = React.useState<any>(null);
|
||||
const [error, setError] = React.useState<string | null>(null);
|
||||
const url = args?.url || getDefaultUrl();
|
||||
|
||||
const run = async () => {
|
||||
try {
|
||||
setIsRunning(true);
|
||||
setError(null);
|
||||
if (!url) throw new Error('No URL available');
|
||||
const res = await seoApiService.analyzeSitemap({
|
||||
sitemap_url: url,
|
||||
analyze_content_trends: analyzeContentTrends,
|
||||
analyze_publishing_patterns: analyzePublishingPatterns
|
||||
});
|
||||
setResult(res);
|
||||
respond({ success: true, url, analyzeContentTrends, analyzePublishingPatterns, result: res });
|
||||
} catch (e: any) {
|
||||
setError(e?.message || 'Failed to analyze sitemap');
|
||||
} finally {
|
||||
setIsRunning(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ padding: 12 }}>
|
||||
<div style={{ marginBottom: 8, fontWeight: 600 }}>Sitemap analysis</div>
|
||||
<div style={{ marginBottom: 8, fontSize: 12, color: '#555' }}>URL: {url || 'Not available'}</div>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 6, marginBottom: 12 }}>
|
||||
<label style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
||||
<input type="checkbox" checked={analyzeContentTrends} onChange={(e) => setAnalyzeContentTrends(e.target.checked)} />
|
||||
Analyze content trends
|
||||
</label>
|
||||
<label style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
||||
<input type="checkbox" checked={analyzePublishingPatterns} onChange={(e) => setAnalyzePublishingPatterns(e.target.checked)} />
|
||||
Analyze publishing patterns
|
||||
</label>
|
||||
</div>
|
||||
<button onClick={run} disabled={isRunning} style={{ padding: '6px 10px' }}>
|
||||
{isRunning ? 'Analyzing…' : 'Run analysis'}
|
||||
</button>
|
||||
{error && <div style={{ marginTop: 10, color: '#c33', fontSize: 12 }}>{error}</div>}
|
||||
{result && (
|
||||
<div style={{ marginTop: 12, fontSize: 12 }}>
|
||||
<pre style={{ whiteSpace: 'pre-wrap' }}>{JSON.stringify(result, null, 2)}</pre>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const RegisterSitemap: React.FC = () => {
|
||||
const execute = useExecute();
|
||||
const useAction = useCopilotActionTyped();
|
||||
|
||||
useAction({
|
||||
name: 'analyzeSitemap',
|
||||
description: 'Analyze and optimize sitemap structure and content',
|
||||
parameters: [
|
||||
{ name: 'url', type: 'string', description: 'Website URL (optional)', required: false },
|
||||
{ name: 'analyzeContentTrends', type: 'boolean', description: 'Analyze content trends', required: false },
|
||||
{ name: 'analyzePublishingPatterns', type: 'boolean', description: 'Analyze publishing patterns', required: false }
|
||||
],
|
||||
renderAndWaitForResponse: ({ args, respond }: any) => <SitemapUI args={args} respond={respond} />,
|
||||
handler: async (args: any) => {
|
||||
const url = args?.url || getDefaultUrl();
|
||||
return await execute('analyzeSitemap', { ...args, url });
|
||||
}
|
||||
});
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export default RegisterSitemap;
|
||||
@@ -0,0 +1,88 @@
|
||||
import React from 'react';
|
||||
import { useCopilotActionTyped, useExecute, getDefaultUrl } from './helpers';
|
||||
import { seoApiService } from '../../../services/seoApiService';
|
||||
|
||||
const TechnicalUI: React.FC<{ args: any; respond: (data: any) => void }> = ({ args, respond }) => {
|
||||
const [scope, setScope] = React.useState<string>(args?.scope || 'full');
|
||||
const [isRunning, setIsRunning] = React.useState(false);
|
||||
const [result, setResult] = React.useState<any>(null);
|
||||
const [error, setError] = React.useState<string | null>(null);
|
||||
const url = args?.url || getDefaultUrl();
|
||||
|
||||
const run = async () => {
|
||||
try {
|
||||
setIsRunning(true);
|
||||
setError(null);
|
||||
if (!url) throw new Error('No URL available');
|
||||
const flags =
|
||||
scope === 'full'
|
||||
? { analyze_core_web_vitals: true, analyze_mobile_friendliness: true, analyze_security: true }
|
||||
: {
|
||||
analyze_core_web_vitals: scope === 'core_web_vitals',
|
||||
analyze_mobile_friendliness: scope === 'mobile_friendliness',
|
||||
analyze_security: scope === 'security'
|
||||
};
|
||||
const res = await seoApiService.analyzeTechnicalSEO({ url, ...flags });
|
||||
setResult(res);
|
||||
respond({ success: true, url, scope, result: res });
|
||||
} catch (e: any) {
|
||||
setError(e?.message || 'Failed to run technical SEO audit');
|
||||
} finally {
|
||||
setIsRunning(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ padding: 12 }}>
|
||||
<div style={{ marginBottom: 8, fontWeight: 600 }}>Technical SEO audit</div>
|
||||
<div style={{ marginBottom: 8, fontSize: 12, color: '#555' }}>URL: {url || 'Not available'}</div>
|
||||
<div style={{ display: 'flex', gap: 8, marginBottom: 12 }}>
|
||||
{['full', 'core_web_vitals', 'mobile_friendliness', 'security'].map((s) => (
|
||||
<label key={s} style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
|
||||
<input type="radio" name="scope" value={s} checked={scope === s} onChange={() => setScope(s)} />
|
||||
{s.replaceAll('_', ' ')}
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
<button onClick={run} disabled={isRunning} style={{ padding: '6px 10px' }}>{isRunning ? 'Auditing…' : 'Run audit'}</button>
|
||||
{error && <div style={{ marginTop: 10, color: '#c33', fontSize: 12 }}>{error}</div>}
|
||||
{result && (
|
||||
<div style={{ marginTop: 12, fontSize: 12 }}>
|
||||
<pre style={{ whiteSpace: 'pre-wrap' }}>{JSON.stringify(result, null, 2)}</pre>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const RegisterTechnical: React.FC = () => {
|
||||
const execute = useExecute();
|
||||
const useAction = useCopilotActionTyped();
|
||||
|
||||
useAction({
|
||||
name: 'analyzeTechnicalSEO',
|
||||
description: 'Perform technical SEO audit and provide technical recommendations',
|
||||
parameters: [
|
||||
{ name: 'url', type: 'string', description: 'URL to analyze (optional)', required: false },
|
||||
{ name: 'scope', type: 'string', description: 'full | core_web_vitals | mobile_friendliness | security', required: false }
|
||||
],
|
||||
renderAndWaitForResponse: ({ args, respond }: any) => <TechnicalUI args={args} respond={respond} />,
|
||||
handler: async (args: any) => {
|
||||
const url = args?.url || getDefaultUrl();
|
||||
const scope = args?.scope || 'full';
|
||||
const flags =
|
||||
scope === 'full'
|
||||
? { analyze_core_web_vitals: true, analyze_mobile_friendliness: true, analyze_security: true }
|
||||
: {
|
||||
analyze_core_web_vitals: scope === 'core_web_vitals',
|
||||
analyze_mobile_friendliness: scope === 'mobile_friendliness',
|
||||
analyze_security: scope === 'security'
|
||||
};
|
||||
return await execute('analyzeTechnicalSEO', { ...args, url, ...flags });
|
||||
}
|
||||
});
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export default RegisterTechnical;
|
||||
6
frontend/src/components/SEODashboard/actions/helpers.tsx
Normal file
6
frontend/src/components/SEODashboard/actions/helpers.tsx
Normal file
@@ -0,0 +1,6 @@
|
||||
import { useCopilotAction } from '@copilotkit/react-core';
|
||||
import useSEOCopilotStore from '../../../stores/seoCopilotStore';
|
||||
|
||||
export const useExecute = () => useSEOCopilotStore(s => s.executeCopilotAction);
|
||||
export const getDefaultUrl = () => useSEOCopilotStore.getState().analysisData?.url;
|
||||
export const useCopilotActionTyped = () => (useCopilotAction as any);
|
||||
41
frontend/src/components/SEODashboard/index.ts
Normal file
41
frontend/src/components/SEODashboard/index.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
// SEO Dashboard Components Index
|
||||
// Export all SEO CopilotKit components for easy importing
|
||||
|
||||
// Core CopilotKit Components
|
||||
export { default as SEOCopilotKitProvider } from './SEOCopilotKitProvider';
|
||||
export { default as SEOCopilotContext } from './SEOCopilotContext';
|
||||
export { default as SEOCopilotActions } from './SEOCopilotActions';
|
||||
export { default as SEOCopilotSuggestions } from './SEOCopilotSuggestions';
|
||||
export { default as SEOCopilotTest } from './SEOCopilotTest';
|
||||
|
||||
// Store and Services
|
||||
export { useSEOCopilotStore, useSEOCopilotAnalysis, useSEOCopilotSuggestions, useSEOCopilotDashboard } from '../../stores/seoCopilotStore';
|
||||
export { default as seoApiService } from '../../services/seoApiService';
|
||||
|
||||
// Types
|
||||
export type {
|
||||
SEOAnalysisData,
|
||||
SEOIssue,
|
||||
TrafficMetrics,
|
||||
RankingData,
|
||||
SpeedMetrics,
|
||||
KeywordData,
|
||||
UserProfile,
|
||||
PersonalizationData,
|
||||
CopilotActionParams,
|
||||
CopilotActionResponse,
|
||||
MetaDescriptionResponse,
|
||||
PageSpeedResponse,
|
||||
SitemapResponse,
|
||||
ChartConfig,
|
||||
DashboardLayout,
|
||||
SEOCopilotState,
|
||||
CopilotSuggestion,
|
||||
SEOApiService,
|
||||
SEOActionError,
|
||||
SEOCategory,
|
||||
SEOExperienceLevel,
|
||||
BusinessType,
|
||||
TimeRange,
|
||||
ChartType
|
||||
} from '../../types/seoCopilotTypes';
|
||||
@@ -4,17 +4,19 @@ import { styled } from '@mui/material/styles';
|
||||
// Shared styled components for dashboard components
|
||||
export const DashboardContainer = styled(Box)(({ theme }) => ({
|
||||
minHeight: '100vh',
|
||||
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 50%, #f093fb 100%)',
|
||||
padding: theme.spacing(4),
|
||||
background:
|
||||
'radial-gradient(1200px 600px at 10% -10%, rgba(255,255,255,0.08) 0%, transparent 60%),\
|
||||
radial-gradient(900px 500px at 110% 10%, rgba(255,255,255,0.06) 0%, transparent 60%),\
|
||||
linear-gradient(135deg, #0f1226 0%, #1b1e3b 35%, #2a2f59 70%, #3a3f7a 100%)',
|
||||
padding: theme.spacing(5, 4, 6, 4),
|
||||
position: 'relative',
|
||||
color: 'rgba(255,255,255,0.9)',
|
||||
'&::before': {
|
||||
content: '""',
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
background: 'url("data:image/svg+xml,%3Csvg width="80" height="80" viewBox="0 0 80 80" xmlns="http://www.w3.org/2000/svg"%3E%3Cg fill="none" fill-rule="evenodd"%3E%3Cg fill="%23ffffff" fill-opacity="0.03"%3E%3Ccircle cx="40" cy="40" r="3"/%3E%3C/g%3E%3C/g%3E%3C/svg%3E")',
|
||||
inset: 0,
|
||||
background:
|
||||
'url("data:image/svg+xml,%3Csvg width=\'80\' height=\'80\' viewBox=\'0 0 80 80\' xmlns=\'http://www.w3.org/2000/svg\'%3E%3Cg fill=\'none\' fill-rule=\'evenodd\'%3E%3Cg fill=\'%23ffffff\' fill-opacity=\'0.03\'%3E%3Ccircle cx=\'40\' cy=\'40\' r=\'2\'/%3E%3C/g%3E%3C/g%3E%3C/svg%3E")',
|
||||
pointerEvents: 'none',
|
||||
},
|
||||
'&::after': {
|
||||
@@ -22,40 +24,43 @@ export const DashboardContainer = styled(Box)(({ theme }) => ({
|
||||
position: 'absolute',
|
||||
top: '50%',
|
||||
left: '50%',
|
||||
width: '600px',
|
||||
height: '600px',
|
||||
background: 'radial-gradient(circle, rgba(255,255,255,0.1) 0%, transparent 70%)',
|
||||
width: '900px',
|
||||
height: '900px',
|
||||
background: 'radial-gradient(circle, rgba(255,255,255,0.08) 0%, transparent 65%)',
|
||||
transform: 'translate(-50%, -50%)',
|
||||
filter: 'blur(20px)',
|
||||
pointerEvents: 'none',
|
||||
zIndex: 0,
|
||||
},
|
||||
}));
|
||||
|
||||
export const GlassCard = styled(Card)(({ theme }) => ({
|
||||
background: 'rgba(255, 255, 255, 0.08)',
|
||||
backdropFilter: 'blur(24px)',
|
||||
border: '1px solid rgba(255, 255, 255, 0.12)',
|
||||
borderRadius: theme.spacing(3),
|
||||
boxShadow: '0 12px 40px rgba(0, 0, 0, 0.12)',
|
||||
transition: 'all 0.4s cubic-bezier(0.4, 0, 0.2, 1)',
|
||||
background: 'linear-gradient(180deg, rgba(255,255,255,0.14) 0%, rgba(255,255,255,0.08) 100%)',
|
||||
backdropFilter: 'blur(22px)',
|
||||
WebkitBackdropFilter: 'blur(22px)',
|
||||
border: '1px solid rgba(255, 255, 255, 0.16)',
|
||||
borderRadius: theme.spacing(3.5),
|
||||
boxShadow:
|
||||
'0 18px 50px rgba(0, 0, 0, 0.25), inset 0 1px 0 rgba(255,255,255,0.25), inset 0 -1px 0 rgba(0,0,0,0.1)',
|
||||
transition: 'transform 0.35s cubic-bezier(0.4, 0, 0.2, 1), box-shadow 0.35s cubic-bezier(0.4, 0, 0.2, 1), border-color 0.35s',
|
||||
position: 'relative',
|
||||
overflow: 'hidden',
|
||||
'&::before': {
|
||||
content: '""',
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: '-100%',
|
||||
left: '-120%',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
background: 'linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.08), transparent)',
|
||||
background: 'linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.10), transparent)',
|
||||
transition: 'left 0.6s ease-in-out',
|
||||
},
|
||||
'&:hover': {
|
||||
transform: 'translateY(-12px) scale(1.02)',
|
||||
boxShadow: '0 24px 60px rgba(0, 0, 0, 0.18)',
|
||||
border: '1px solid rgba(255, 255, 255, 0.2)',
|
||||
transform: 'translateY(-10px) scale(1.015)',
|
||||
boxShadow: '0 30px 80px rgba(0, 0, 0, 0.35), inset 0 1px 0 rgba(255,255,255,0.3)',
|
||||
border: '1px solid rgba(255, 255, 255, 0.22)',
|
||||
'&::before': {
|
||||
left: '100%',
|
||||
left: '120%',
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
342
frontend/src/services/seoApiService.ts
Normal file
342
frontend/src/services/seoApiService.ts
Normal file
@@ -0,0 +1,342 @@
|
||||
// SEO API Service
|
||||
// Handles all communication with the FastAPI backend SEO endpoints
|
||||
|
||||
import {
|
||||
SEOAnalysisData,
|
||||
MetaDescriptionResponse,
|
||||
PageSpeedResponse,
|
||||
SitemapResponse,
|
||||
PersonalizationData,
|
||||
DashboardLayout,
|
||||
CopilotActionResponse,
|
||||
CopilotSuggestion
|
||||
} from '../types/seoCopilotTypes';
|
||||
|
||||
const API_BASE_URL = process.env.REACT_APP_API_BASE_URL || 'http://localhost:8000';
|
||||
|
||||
class SEOApiService {
|
||||
private baseUrl: string;
|
||||
|
||||
constructor() {
|
||||
this.baseUrl = API_BASE_URL;
|
||||
}
|
||||
|
||||
// Generic API request method
|
||||
private async makeRequest<T>(
|
||||
endpoint: string,
|
||||
method: 'GET' | 'POST' | 'PUT' | 'DELETE' = 'GET',
|
||||
data?: any
|
||||
): Promise<T> {
|
||||
try {
|
||||
const url = `${this.baseUrl}${endpoint}`;
|
||||
const options: RequestInit = {
|
||||
method,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
credentials: 'include',
|
||||
};
|
||||
|
||||
if (data && method !== 'GET') {
|
||||
options.body = JSON.stringify(data);
|
||||
}
|
||||
|
||||
const response = await fetch(url, options);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`API request failed: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error(`SEO API Error (${endpoint}):`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// SEO Analysis Methods
|
||||
async analyzeSEO(url: string, options?: any): Promise<SEOAnalysisData> {
|
||||
return this.makeRequest<SEOAnalysisData>('/api/seo-dashboard/analyze-comprehensive', 'POST', {
|
||||
url,
|
||||
...options
|
||||
});
|
||||
}
|
||||
|
||||
async analyzeSEOFull(url: string, options?: any): Promise<SEOAnalysisData> {
|
||||
return this.makeRequest<SEOAnalysisData>('/api/seo-dashboard/analyze-full', 'POST', {
|
||||
url,
|
||||
...options
|
||||
});
|
||||
}
|
||||
|
||||
async getSEOHealthScore(): Promise<{ health_score: number }> {
|
||||
return this.makeRequest<{ health_score: number }>('/api/seo-dashboard/health-score');
|
||||
}
|
||||
|
||||
async getSEOMetrics(url?: string): Promise<any> {
|
||||
const endpoint = url ? `/api/seo-dashboard/metrics-detailed?url=${encodeURIComponent(url)}` : '/api/seo-dashboard/metrics';
|
||||
return this.makeRequest(endpoint);
|
||||
}
|
||||
|
||||
async getAnalysisSummary(url: string): Promise<any> {
|
||||
return this.makeRequest(`/api/seo-dashboard/analysis-summary?url=${encodeURIComponent(url)}`);
|
||||
}
|
||||
|
||||
async batchAnalyzeUrls(urls: string[]): Promise<any> {
|
||||
return this.makeRequest('/api/seo-dashboard/batch-analyze', 'POST', { urls });
|
||||
}
|
||||
|
||||
// Meta Description Generation
|
||||
async generateMetaDescriptions(params: {
|
||||
keywords: string[];
|
||||
tone?: string;
|
||||
search_intent?: string;
|
||||
language?: string;
|
||||
custom_prompt?: string;
|
||||
}): Promise<MetaDescriptionResponse> {
|
||||
return this.makeRequest<MetaDescriptionResponse>('/api/seo/meta-description', 'POST', params);
|
||||
}
|
||||
|
||||
// PageSpeed Analysis
|
||||
async analyzePageSpeed(params: {
|
||||
url: string;
|
||||
strategy?: 'DESKTOP' | 'MOBILE';
|
||||
locale?: string;
|
||||
categories?: string[];
|
||||
}): Promise<PageSpeedResponse> {
|
||||
return this.makeRequest<PageSpeedResponse>('/api/seo/pagespeed-analysis', 'POST', params);
|
||||
}
|
||||
|
||||
// Sitemap Analysis
|
||||
async analyzeSitemap(params: {
|
||||
sitemap_url: string;
|
||||
analyze_content_trends?: boolean;
|
||||
analyze_publishing_patterns?: boolean;
|
||||
}): Promise<SitemapResponse> {
|
||||
return this.makeRequest<SitemapResponse>('/api/seo/sitemap-analysis', 'POST', params);
|
||||
}
|
||||
|
||||
// Image Alt Text Generation
|
||||
async generateImageAltText(params: {
|
||||
image_url?: string;
|
||||
context?: string;
|
||||
keywords?: string[];
|
||||
}): Promise<any> {
|
||||
return this.makeRequest('/api/seo/image-alt-text', 'POST', params);
|
||||
}
|
||||
|
||||
// OpenGraph Tag Generation
|
||||
async generateOpenGraphTags(params: {
|
||||
url: string;
|
||||
title_hint?: string;
|
||||
description_hint?: string;
|
||||
platform?: string;
|
||||
}): Promise<any> {
|
||||
return this.makeRequest('/api/seo/opengraph-tags', 'POST', params);
|
||||
}
|
||||
|
||||
// On-Page SEO Analysis
|
||||
async analyzeOnPageSEO(params: {
|
||||
url: string;
|
||||
target_keywords?: string[];
|
||||
analyze_images?: boolean;
|
||||
analyze_content_quality?: boolean;
|
||||
}): Promise<any> {
|
||||
return this.makeRequest('/api/seo/on-page-analysis', 'POST', params);
|
||||
}
|
||||
|
||||
// Technical SEO Analysis
|
||||
async analyzeTechnicalSEO(params: {
|
||||
url: string;
|
||||
analyze_core_web_vitals?: boolean;
|
||||
analyze_mobile_friendliness?: boolean;
|
||||
analyze_security?: boolean;
|
||||
}): Promise<any> {
|
||||
return this.makeRequest('/api/seo/technical-seo', 'POST', params);
|
||||
}
|
||||
|
||||
// Enterprise SEO Analysis
|
||||
async analyzeEnterpriseSEO(params: {
|
||||
url: string;
|
||||
analyze_competitors?: boolean;
|
||||
analyze_market_position?: boolean;
|
||||
analyze_roi_metrics?: boolean;
|
||||
}): Promise<any> {
|
||||
return this.makeRequest('/api/seo/workflow/website-audit', 'POST', params);
|
||||
}
|
||||
|
||||
// Content Strategy Analysis
|
||||
async analyzeContentStrategy(params: {
|
||||
url: string;
|
||||
analyze_content_gaps?: boolean;
|
||||
analyze_topic_clusters?: boolean;
|
||||
analyze_content_performance?: boolean;
|
||||
}): Promise<any> {
|
||||
return this.makeRequest('/api/seo/workflow/content-analysis', 'POST', params);
|
||||
}
|
||||
|
||||
// Health Check
|
||||
async getSEOHealthCheck(): Promise<any> {
|
||||
return this.makeRequest('/api/seo/health');
|
||||
}
|
||||
|
||||
async getSEOToolsStatus(): Promise<any> {
|
||||
return this.makeRequest('/api/seo/tools/status');
|
||||
}
|
||||
|
||||
// Website Audit Workflow
|
||||
async performWebsiteAudit(url: string, options?: any): Promise<SEOAnalysisData> {
|
||||
return this.makeRequest<SEOAnalysisData>('/api/seo/workflow/website-audit', 'POST', {
|
||||
url,
|
||||
audit_type: options?.audit_type || 'comprehensive',
|
||||
include_recommendations: options?.include_recommendations ?? true
|
||||
});
|
||||
}
|
||||
|
||||
// Content Analysis Workflow
|
||||
async analyzeContentComprehensive(url: string, options?: any): Promise<SEOAnalysisData> {
|
||||
return this.makeRequest<SEOAnalysisData>('/api/seo/workflow/content-analysis', 'POST', {
|
||||
url,
|
||||
content_focus: options?.content_focus,
|
||||
seo_optimization: options?.seo_optimization ?? true
|
||||
});
|
||||
}
|
||||
|
||||
// SEO Health Check
|
||||
async checkSEOHealth(url?: string, options?: any): Promise<{ health_score: number; status: string; tools_status?: any }> {
|
||||
const endpoint = url ? '/api/seo/health' : '/api/seo/tools/status';
|
||||
const params = url ? { url } : {};
|
||||
|
||||
return this.makeRequest(endpoint, 'GET', params);
|
||||
}
|
||||
|
||||
// Personalization Data
|
||||
async getPersonalizationData(): Promise<PersonalizationData> {
|
||||
// This would typically fetch from a user profile endpoint
|
||||
// For now, return mock data
|
||||
return Promise.resolve({
|
||||
user_profile: {
|
||||
id: '1',
|
||||
name: 'SEO User',
|
||||
email: 'seo@example.com',
|
||||
experience_level: 'intermediate',
|
||||
business_type: 'ecommerce',
|
||||
target_audience: 'general',
|
||||
seo_goals: ['improve_rankings', 'increase_traffic'],
|
||||
seo_experience: 'intermediate'
|
||||
},
|
||||
business_type: 'ecommerce',
|
||||
target_audience: 'general',
|
||||
seo_goals: ['improve_rankings', 'increase_traffic'],
|
||||
seo_experience: 'intermediate'
|
||||
});
|
||||
}
|
||||
|
||||
// Dashboard Layout Update
|
||||
async updateDashboardLayout(layout: DashboardLayout): Promise<{ success: boolean; layout: DashboardLayout }> {
|
||||
// This would typically save to backend
|
||||
// For now, return success
|
||||
return Promise.resolve({
|
||||
success: true,
|
||||
layout
|
||||
});
|
||||
}
|
||||
|
||||
// SEO Suggestions
|
||||
async getSEOSuggestions(context: string): Promise<CopilotSuggestion[]> {
|
||||
// This would typically call an AI service for contextual suggestions
|
||||
// For now, return mock suggestions
|
||||
return Promise.resolve([
|
||||
{
|
||||
id: '1',
|
||||
title: 'Optimize Page Speed',
|
||||
description: 'Your page speed could be improved for better user experience',
|
||||
message: 'Consider optimizing your page speed for better user experience and SEO rankings',
|
||||
category: 'optimization',
|
||||
priority: 'high',
|
||||
action: 'analyzePageSpeed',
|
||||
icon: '⚡',
|
||||
severity: 'medium'
|
||||
}
|
||||
]);
|
||||
}
|
||||
|
||||
// CopilotKit Specific Methods
|
||||
async executeCopilotAction(action: string, params: any): Promise<CopilotActionResponse> {
|
||||
try {
|
||||
const startTime = Date.now();
|
||||
|
||||
let result: any;
|
||||
|
||||
switch (action) {
|
||||
case 'analyzeSEOComprehensive':
|
||||
result = await this.analyzeSEO(params.url, params);
|
||||
break;
|
||||
case 'generateMetaDescriptions':
|
||||
result = await this.generateMetaDescriptions(params);
|
||||
break;
|
||||
case 'analyzePageSpeed':
|
||||
result = await this.analyzePageSpeed(params);
|
||||
break;
|
||||
case 'analyzeSitemap':
|
||||
result = await this.analyzeSitemap(params);
|
||||
break;
|
||||
case 'generateImageAltText':
|
||||
result = await this.generateImageAltText(params);
|
||||
break;
|
||||
case 'generateOpenGraphTags':
|
||||
result = await this.generateOpenGraphTags(params);
|
||||
break;
|
||||
case 'analyzeOnPageSEO':
|
||||
result = await this.analyzeOnPageSEO(params);
|
||||
break;
|
||||
case 'analyzeTechnicalSEO':
|
||||
result = await this.analyzeTechnicalSEO(params);
|
||||
break;
|
||||
case 'analyzeEnterpriseSEO':
|
||||
result = await this.analyzeEnterpriseSEO(params);
|
||||
break;
|
||||
case 'analyzeContentStrategy':
|
||||
result = await this.analyzeContentStrategy(params);
|
||||
break;
|
||||
case 'performWebsiteAudit':
|
||||
result = await this.performWebsiteAudit(params.url, params);
|
||||
break;
|
||||
case 'analyzeContentComprehensive':
|
||||
result = await this.analyzeContentComprehensive(params.url, params);
|
||||
break;
|
||||
case 'checkSEOHealth':
|
||||
result = await this.checkSEOHealth(params.url, params);
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unknown action: ${action}`);
|
||||
}
|
||||
|
||||
const executionTime = Date.now() - startTime;
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: `${action} completed successfully`,
|
||||
data: result,
|
||||
execution_time: executionTime
|
||||
};
|
||||
} catch (error: any) {
|
||||
return {
|
||||
success: false,
|
||||
message: `Failed to execute ${action}: ${error.message}`,
|
||||
error: error.message,
|
||||
execution_time: 0
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Error handling utility
|
||||
private handleError(error: any, context: string): never {
|
||||
console.error(`SEO API Error (${context}):`, error);
|
||||
throw new Error(`SEO API Error: ${error.message || 'Unknown error occurred'}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Export singleton instance
|
||||
export const seoApiService = new SEOApiService();
|
||||
export default seoApiService;
|
||||
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 };
|
||||
}
|
||||
}),
|
||||
{
|
||||
|
||||
6
frontend/src/types/copilotkit-react-ui.d.ts
vendored
Normal file
6
frontend/src/types/copilotkit-react-ui.d.ts
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
declare module '@copilotkit/react-ui' {
|
||||
export const CopilotSidebar: any;
|
||||
export const CopilotChat: any;
|
||||
}
|
||||
|
||||
declare module '@copilotkit/react-ui/styles.css';
|
||||
313
frontend/src/types/seoCopilotTypes.ts
Normal file
313
frontend/src/types/seoCopilotTypes.ts
Normal file
@@ -0,0 +1,313 @@
|
||||
// SEO CopilotKit Type Definitions
|
||||
// This file contains all TypeScript interfaces and types for SEO CopilotKit integration
|
||||
|
||||
// SEO Analysis Data Types
|
||||
export interface SEOAnalysisData {
|
||||
health_score: number;
|
||||
critical_issues: SEOIssue[];
|
||||
traffic_metrics: TrafficMetrics;
|
||||
ranking_data: RankingData;
|
||||
mobile_speed: SpeedMetrics;
|
||||
keyword_data: KeywordData;
|
||||
url: string;
|
||||
last_updated: string;
|
||||
status: 'pending' | 'completed' | 'failed';
|
||||
}
|
||||
|
||||
export interface SEOIssue {
|
||||
id: string;
|
||||
title: string;
|
||||
description: string;
|
||||
severity: 'critical' | 'high' | 'medium' | 'low';
|
||||
category: 'technical' | 'content' | 'performance' | 'accessibility';
|
||||
impact: string;
|
||||
recommendation: string;
|
||||
effort: 'easy' | 'medium' | 'hard';
|
||||
priority: number;
|
||||
}
|
||||
|
||||
export interface PageTraffic {
|
||||
url: string;
|
||||
traffic: number;
|
||||
growth: number;
|
||||
}
|
||||
|
||||
export interface TrafficSource {
|
||||
source: string;
|
||||
traffic: number;
|
||||
percentage: number;
|
||||
}
|
||||
|
||||
export interface TrafficMetrics {
|
||||
organic_traffic: number;
|
||||
traffic_growth: number;
|
||||
top_pages: PageTraffic[];
|
||||
traffic_sources: TrafficSource[];
|
||||
}
|
||||
|
||||
export interface KeywordRanking {
|
||||
keyword: string;
|
||||
position: number;
|
||||
volume: number;
|
||||
difficulty: number;
|
||||
}
|
||||
|
||||
export interface PositionChange {
|
||||
keyword: string;
|
||||
old_position: number;
|
||||
new_position: number;
|
||||
change: number;
|
||||
}
|
||||
|
||||
export interface RankingData {
|
||||
average_position: number;
|
||||
ranking_keywords: KeywordRanking[];
|
||||
position_changes: PositionChange[];
|
||||
}
|
||||
|
||||
export interface CoreWebVitals {
|
||||
lcp: number;
|
||||
fid: number;
|
||||
cls: number;
|
||||
}
|
||||
|
||||
export interface SpeedMetrics {
|
||||
mobile_score: number;
|
||||
desktop_score: number;
|
||||
load_time: number;
|
||||
core_web_vitals: CoreWebVitals;
|
||||
}
|
||||
|
||||
export interface KeywordOpportunity {
|
||||
keyword: string;
|
||||
volume: number;
|
||||
difficulty: number;
|
||||
opportunity_score: number;
|
||||
current_position?: number;
|
||||
}
|
||||
|
||||
export interface KeywordData {
|
||||
total_keywords: number;
|
||||
ranking_keywords: number;
|
||||
keyword_opportunities: KeywordOpportunity[];
|
||||
}
|
||||
|
||||
// User Context Types
|
||||
export interface UserProfile {
|
||||
id: string;
|
||||
name: string;
|
||||
email: string;
|
||||
business_type: string;
|
||||
seo_experience: 'beginner' | 'intermediate' | 'advanced';
|
||||
seo_goals: string[];
|
||||
target_audience: string;
|
||||
}
|
||||
|
||||
export interface PersonalizationData {
|
||||
user_profile: UserProfile;
|
||||
business_type: string;
|
||||
target_audience: string;
|
||||
seo_goals: string[];
|
||||
seo_experience: 'beginner' | 'intermediate' | 'advanced';
|
||||
}
|
||||
|
||||
// CopilotKit Action Types
|
||||
export interface CopilotActionParams {
|
||||
url?: string;
|
||||
keywords?: string[];
|
||||
tone?: string;
|
||||
searchIntent?: string;
|
||||
strategy?: 'DESKTOP' | 'MOBILE';
|
||||
categories?: string[];
|
||||
chartType?: string;
|
||||
timeRange?: string;
|
||||
metrics?: string[];
|
||||
focusArea?: string;
|
||||
focusAreas?: string[];
|
||||
layout?: string;
|
||||
hideSections?: string[];
|
||||
concept?: string;
|
||||
complexity?: 'simple' | 'detailed' | 'technical';
|
||||
businessContext?: string;
|
||||
category?: string;
|
||||
timeframe?: string;
|
||||
scenarios?: string[];
|
||||
priority?: 'critical' | 'high' | 'medium' | 'low';
|
||||
effort?: 'easy' | 'medium' | 'hard';
|
||||
targetKeywords?: string[];
|
||||
sitemapUrl?: string;
|
||||
analyzeContentTrends?: boolean;
|
||||
analyzePublishingPatterns?: boolean;
|
||||
imageUrl?: string;
|
||||
context?: string;
|
||||
titleHint?: string;
|
||||
descriptionHint?: string;
|
||||
platform?: string;
|
||||
analyzeImages?: boolean;
|
||||
analyzeContentQuality?: boolean;
|
||||
includeMobile?: boolean;
|
||||
competitorUrls?: string[];
|
||||
marketAnalysis?: boolean;
|
||||
contentType?: string;
|
||||
targetAudience?: string;
|
||||
auditType?: string;
|
||||
includeRecommendations?: boolean;
|
||||
contentFocus?: string;
|
||||
seoOptimization?: boolean;
|
||||
includeToolsStatus?: boolean;
|
||||
depth?: string;
|
||||
}
|
||||
|
||||
export interface CopilotActionResponse {
|
||||
success: boolean;
|
||||
message: string;
|
||||
data?: any;
|
||||
error?: string;
|
||||
execution_time?: number;
|
||||
}
|
||||
|
||||
// SEO Service Response Types
|
||||
export interface MetaDescriptionResponse {
|
||||
meta_descriptions: string[];
|
||||
analysis: {
|
||||
keyword_density: number;
|
||||
length_optimal: boolean;
|
||||
seo_score: number;
|
||||
};
|
||||
}
|
||||
|
||||
export interface PageSpeedResponse {
|
||||
performance_score: number;
|
||||
accessibility_score: number;
|
||||
best_practices_score: number;
|
||||
seo_score: number;
|
||||
recommendations: string[];
|
||||
opportunities: SpeedOpportunity[];
|
||||
}
|
||||
|
||||
export interface SitemapIssue {
|
||||
url: string;
|
||||
issue: string;
|
||||
severity: 'critical' | 'high' | 'medium' | 'low';
|
||||
recommendation: string;
|
||||
}
|
||||
|
||||
export interface SpeedOpportunity {
|
||||
metric: string;
|
||||
current_value: number;
|
||||
target_value: number;
|
||||
improvement: number;
|
||||
recommendation: string;
|
||||
}
|
||||
|
||||
export interface SitemapResponse {
|
||||
total_urls: number;
|
||||
indexed_urls: number;
|
||||
issues: SitemapIssue[];
|
||||
recommendations: string[];
|
||||
}
|
||||
|
||||
// Chart and Visualization Types
|
||||
export interface ChartConfig {
|
||||
type: 'line' | 'bar' | 'pie' | 'radar';
|
||||
chartKey?: ChartType;
|
||||
data: any;
|
||||
options: any;
|
||||
timeRange?: string;
|
||||
metrics?: string[];
|
||||
}
|
||||
|
||||
export interface DashboardLayout {
|
||||
focusArea: string;
|
||||
layout: 'overview' | 'detailed' | 'focused';
|
||||
hiddenSections: string[];
|
||||
chartConfigs: ChartConfig[];
|
||||
}
|
||||
|
||||
// Store State Types
|
||||
export interface SEOCopilotState {
|
||||
// Loading states
|
||||
isLoading: boolean;
|
||||
isAnalyzing: boolean;
|
||||
isGenerating: boolean;
|
||||
|
||||
// Data states
|
||||
analysisData: SEOAnalysisData | null;
|
||||
personalizationData: PersonalizationData | null;
|
||||
|
||||
// UI states
|
||||
activeChart: string | null;
|
||||
dashboardLayout: DashboardLayout;
|
||||
suggestions: CopilotSuggestion[];
|
||||
|
||||
// Error states
|
||||
error: string | null;
|
||||
lastError: Error | null;
|
||||
|
||||
// Actions
|
||||
setLoading: (loading: boolean) => void;
|
||||
setAnalyzing: (analyzing: boolean) => void;
|
||||
setGenerating: (generating: boolean) => void;
|
||||
setAnalysisData: (data: SEOAnalysisData | null) => void;
|
||||
setPersonalizationData: (data: PersonalizationData | null) => void;
|
||||
setActiveChart: (chart: string | null) => void;
|
||||
setDashboardLayout: (layout: DashboardLayout) => void;
|
||||
setSuggestions: (suggestions: CopilotSuggestion[]) => void;
|
||||
setError: (error: string | null) => void;
|
||||
clearError: () => void;
|
||||
|
||||
// API integration methods
|
||||
loadPersonalizationData: () => Promise<void>;
|
||||
executeCopilotAction: (action: string, params: any) => Promise<any>;
|
||||
|
||||
// Chart and visualization methods
|
||||
updateChart: (chartType: ChartType, timeRange?: TimeRange, metrics?: string[]) => void;
|
||||
|
||||
// Utility methods
|
||||
generateContextualSuggestions: (analysisData: SEOAnalysisData) => CopilotSuggestion[];
|
||||
getHealthScoreColor: (score: number) => string;
|
||||
getSeverityColor: (severity: string) => string;
|
||||
getEffortColor: (effort: string) => string;
|
||||
|
||||
// Reset methods
|
||||
resetAnalysis: () => void;
|
||||
resetAll: () => void;
|
||||
}
|
||||
|
||||
// CopilotKit Suggestion Types
|
||||
export interface CopilotSuggestion {
|
||||
id: string;
|
||||
title: string;
|
||||
message: string;
|
||||
icon: string;
|
||||
category: 'analysis' | 'optimization' | 'education' | 'monitoring';
|
||||
priority: 'high' | 'medium' | 'low';
|
||||
action: string;
|
||||
parameters?: CopilotActionParams;
|
||||
}
|
||||
|
||||
// API Service Types
|
||||
export interface SEOApiService {
|
||||
analyzeSEO: (url: string, options?: any) => Promise<SEOAnalysisData>;
|
||||
generateMetaDescriptions: (params: any) => Promise<MetaDescriptionResponse>;
|
||||
analyzePageSpeed: (url: string, strategy?: string) => Promise<PageSpeedResponse>;
|
||||
analyzeSitemap: (sitemapUrl: string) => Promise<SitemapResponse>;
|
||||
getPersonalizationData: () => Promise<PersonalizationData>;
|
||||
updateDashboardLayout: (layout: DashboardLayout) => Promise<void>;
|
||||
}
|
||||
|
||||
// Error Types
|
||||
export interface SEOActionError {
|
||||
action: string;
|
||||
error: string;
|
||||
timestamp: string;
|
||||
userContext: any;
|
||||
retryable: boolean;
|
||||
}
|
||||
|
||||
// Utility Types
|
||||
export type SEOCategory = 'technical' | 'content' | 'performance' | 'accessibility' | 'mobile' | 'local';
|
||||
export type SEOExperienceLevel = 'beginner' | 'intermediate' | 'advanced';
|
||||
export type BusinessType = 'ecommerce' | 'saas' | 'blog' | 'agency' | 'local' | 'enterprise';
|
||||
export type TimeRange = '7d' | '30d' | '90d' | '1y' | 'all';
|
||||
export type ChartType = 'traffic' | 'rankings' | 'speed' | 'keywords' | 'issues' | 'performance';
|
||||
Reference in New Issue
Block a user