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:
ajaysi
2025-08-30 16:12:41 +05:30
parent d9833f30a6
commit f5f3c09ecc
39 changed files with 10606 additions and 1606 deletions

View 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;

View 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;

View 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;

View 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;

View 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;

View File

@@ -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>
);
};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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);

View 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';

View File

@@ -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%',
},
},
}));