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%',
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
Reference in New Issue
Block a user