ALwrity SEO CopilotKit Implementation Plan

This commit is contained in:
ajaysi
2025-08-30 20:07:55 +05:30
parent 0f16b855e1
commit 1e0a13e204
8 changed files with 381 additions and 94 deletions

View File

@@ -7,6 +7,7 @@ import Wizard from './components/OnboardingWizard/Wizard';
import MainDashboard from './components/MainDashboard/MainDashboard';
import SEODashboard from './components/SEODashboard/SEODashboard';
import ContentPlanningDashboard from './components/ContentPlanningDashboard/ContentPlanningDashboard';
import FacebookWriter from './components/FacebookWriter/FacebookWriter';
import { apiClient } from './api/client';
@@ -180,7 +181,9 @@ const App: React.FC = () => {
<Route path="/onboarding" element={<Wizard />} />
<Route path="/dashboard" element={<MainDashboard />} />
<Route path="/seo" element={<SEODashboard />} />
<Route path="/seo-dashboard" element={<SEODashboard />} />
<Route path="/content-planning" element={<ContentPlanningDashboard />} />
<Route path="/facebook-writer" element={<FacebookWriter />} />
</Routes>
</ConditionalCopilotKit>
</Router>

View File

@@ -622,8 +622,8 @@ const ContentStrategyBuilder: React.FC = () => {
suggestions={suggestions}
observabilityHooks={{
onChatExpanded: () => console.log("Strategy assistant opened"),
onMessageSent: (message) => console.log("Strategy message sent", { message }),
onFeedbackGiven: (messageId, type) => console.log("Strategy feedback", { messageId, type })
onMessageSent: (message: any) => console.log("Strategy message sent", { message }),
onFeedbackGiven: (messageId: string, type: string) => console.log("Strategy feedback", { messageId, type })
}}
>
<Box sx={{ p: 3 }}>

View File

@@ -23,8 +23,7 @@ import {
Card,
CardContent,
CardHeader,
Avatar,
Badge
Avatar
} from '@mui/material';
import {
CheckCircle as HealthyIcon,
@@ -98,6 +97,7 @@ const SystemStatusIndicator: React.FC<SystemStatusIndicatorProps> = ({ className
const [error, setError] = useState<string | null>(null);
const [dashboardOpen, setDashboardOpen] = useState(false);
const [chartData, setChartData] = useState<any[]>([]);
const [cachePerf, setCachePerf] = useState<{ hits: number; misses: number; hit_rate: number } | null>(null);
const fetchStatus = async () => {
setLoading(true);
@@ -137,6 +137,9 @@ const SystemStatusIndicator: React.FC<SystemStatusIndicatorProps> = ({ className
const result = await response.json();
if (result.status === 'success') {
setDetailedStats(result.data);
if (result.data?.cache_performance) {
setCachePerf(result.data.cache_performance);
}
// Generate chart data
const chartData = result.data.top_endpoints.slice(0, 5).map((endpoint: any, index: number) => ({
@@ -156,10 +159,16 @@ const SystemStatusIndicator: React.FC<SystemStatusIndicatorProps> = ({ className
useEffect(() => {
fetchStatus();
// Prime cache performance occasionally even when dashboard is closed
fetchDetailedStats();
// Refresh every 30 seconds
const interval = setInterval(fetchStatus, 30000);
return () => clearInterval(interval);
const cacheInterval = setInterval(fetchDetailedStats, 60000);
return () => {
clearInterval(interval);
clearInterval(cacheInterval);
};
}, []);
useEffect(() => {
@@ -239,56 +248,46 @@ const SystemStatusIndicator: React.FC<SystemStatusIndicatorProps> = ({ className
);
}
const total = statusData?.recent_requests ?? 0;
const failed = statusData?.recent_errors ?? 0;
const passed = Math.max(0, total - failed);
return (
<>
<Box className={className} sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<Tooltip title={tooltipContent} arrow placement="bottom">
<Box
sx={{ display: 'flex', alignItems: 'center', cursor: 'pointer' }}
onClick={handleDashboardClick}
>
<motion.div
whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.9 }}
>
{statusData ? getStatusIcon(statusData.status) : <UnknownIcon />}
</motion.div>
</Box>
</Tooltip>
<Chip
label={statusData?.status || 'Unknown'}
size="small"
color={getStatusColor(statusData?.status || 'unknown')}
<Tooltip title={tooltipContent} arrow placement="bottom">
<Button
variant="outlined"
sx={{ height: 24, fontSize: '0.75rem' }}
/>
<IconButton
size="small"
onClick={fetchStatus}
disabled={loading}
sx={{ p: 0.5 }}
>
<RefreshIcon sx={{ fontSize: 16 }} />
</IconButton>
{/* Debug button to test dashboard opening */}
<IconButton
size="small"
onClick={handleDashboardClick}
sx={{ p: 0.5, color: 'primary.main' }}
title="Open Dashboard (Debug)"
sx={{
display: 'flex',
alignItems: 'center',
gap: 1,
px: 1,
py: 0.5,
minHeight: 34,
borderRadius: 2,
background: 'linear-gradient(180deg, rgba(255,255,255,0.18), rgba(255,255,255,0.1))',
borderColor: 'rgba(255,255,255,0.35)',
color: 'white',
boxShadow: '0 10px 28px rgba(0,0,0,0.25), inset 0 1px 0 rgba(255,255,255,0.2)'
}}
>
<AnalyticsIcon sx={{ fontSize: 16 }} />
</IconButton>
{error && (
<Alert severity="error" sx={{ fontSize: '0.75rem', py: 0 }}>
{error}
</Alert>
)}
</Box>
{statusData ? getStatusIcon(statusData.status) : <UnknownIcon sx={{ color: 'grey.200' }} />}
<Chip
label={`System • ${(statusData?.status || 'unknown').toUpperCase()}`}
size="small"
color={getStatusColor(statusData?.status || 'unknown')}
sx={{ height: 22, fontSize: '0.70rem' }}
/>
<IconButton
size="small"
onClick={(e) => { e.stopPropagation(); fetchStatus(); fetchDetailedStats(); }}
sx={{ ml: 0.5, color: 'rgba(255,255,255,0.9)' }}
>
<RefreshIcon sx={{ fontSize: 16 }} />
</IconButton>
</Button>
</Tooltip>
{/* Enhanced Monitoring Dashboard */}
<Dialog

View File

@@ -0,0 +1,118 @@
import React from 'react';
import { Box, Container, Typography, TextField, Paper } from '@mui/material';
import { CopilotSidebar } from '@copilotkit/react-ui';
import { useCopilotReadable, useCopilotAction } from '@copilotkit/react-core';
import '@copilotkit/react-ui/styles.css';
const useCopilotActionTyped = useCopilotAction as any;
const FacebookWriter: React.FC = () => {
const [postDraft, setPostDraft] = React.useState<string>('');
const [notes, setNotes] = React.useState<string>('');
// Share current draft and notes with Copilot
useCopilotReadable({
description: 'Current Facebook post draft text the user is editing',
value: postDraft,
categories: ['social', 'facebook', 'draft']
});
useCopilotReadable({
description: 'User notes/context for the next Facebook post',
value: notes,
categories: ['social', 'facebook', 'context']
});
// Allow Copilot to update the draft directly (predictive state-like edit)
useCopilotActionTyped({
name: 'updateFacebookPostDraft',
description: 'Replace the Facebook post draft with provided content',
parameters: [
{ name: 'content', type: 'string', description: 'The full post content to set', required: true }
],
handler: async ({ content }: { content: string }) => {
setPostDraft(content);
return { success: true, message: 'Draft updated' };
}
});
// Let Copilot append text to the draft (collaborative editing)
useCopilotActionTyped({
name: 'appendToFacebookPostDraft',
description: 'Append text to the current Facebook post draft',
parameters: [
{ name: 'content', type: 'string', description: 'The text to append', required: true }
],
handler: async ({ content }: { content: string }) => {
setPostDraft(prev => (prev ? `${prev}\n\n${content}` : content));
return { success: true, message: 'Text appended' };
}
});
const suggestions = [
{ title: '🎉 Launch teaser', message: 'Write a short Facebook post announcing our new feature launch' },
{ title: '💡 Benefit-first', message: 'Draft a Facebook post highlighting a key user benefit with a CTA' },
{ title: '🔁 Variations', message: 'Generate 3 alternative versions of this post to A/B test' },
{ title: '🏷️ Hashtags', message: 'Suggest 5 relevant hashtags for this post' }
];
return (
<CopilotSidebar
className="alwrity-copilot-sidebar"
labels={{
title: 'ALwrity • Facebook Writer',
initial: 'Tell me what you want to post. I can draft, refine, and generate variants. I can also update the draft directly for you.'
}}
suggestions={suggestions}
>
<Box sx={{ py: 4 }}>
<Container maxWidth="md">
<Typography variant="h4" sx={{ color: 'white', fontWeight: 800, mb: 2 }}>
Facebook Writer (Preview)
</Typography>
<Typography variant="body1" sx={{ color: 'rgba(255,255,255,0.85)', mb: 3 }}>
Collaborate with the Copilot to craft your post. The assistant can update the draft directly.
</Typography>
<Paper sx={{ p: 2, background: 'rgba(255,255,255,0.1)', border: '1px solid rgba(255,255,255,0.3)' }}>
<Typography variant="subtitle2" sx={{ color: 'rgba(255,255,255,0.9)', mb: 1 }}>
Context/Notes (optional)
</Typography>
<TextField
fullWidth
multiline
minRows={2}
value={notes}
onChange={e => setNotes(e.target.value)}
placeholder="Audience, campaign, tone, key points..."
sx={{
mb: 2,
'& .MuiInputBase-root': { color: 'white' },
'& .MuiOutlinedInput-notchedOutline': { borderColor: 'rgba(255,255,255,0.3)' }
}}
/>
<Typography variant="subtitle2" sx={{ color: 'rgba(255,255,255,0.9)', mb: 1 }}>
Post Draft
</Typography>
<TextField
fullWidth
multiline
minRows={6}
value={postDraft}
onChange={e => setPostDraft(e.target.value)}
placeholder="Your Facebook post will appear here. Ask the Copilot to draft or update it."
sx={{
'& .MuiInputBase-root': { color: 'white' },
'& .MuiOutlinedInput-notchedOutline': { borderColor: 'rgba(255,255,255,0.3)' }
}}
/>
</Paper>
</Container>
</Box>
</CopilotSidebar>
);
};
export default FacebookWriter;

View File

@@ -9,9 +9,11 @@ import {
useMediaQuery
} from '@mui/material';
import { motion, AnimatePresence } from 'framer-motion';
import { useNavigate } from 'react-router-dom';
// Shared components
import DashboardHeader from '../shared/DashboardHeader';
import SystemStatusIndicator from '../ContentPlanningDashboard/components/SystemStatusIndicator';
import SearchFilter from '../shared/SearchFilter';
import ToolCard from '../shared/ToolCard';
import CategoryHeader from '../shared/CategoryHeader';
@@ -33,6 +35,7 @@ import { toolCategories } from '../../data/toolCategories';
const MainDashboard: React.FC = () => {
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
const navigate = useNavigate();
// Zustand store hooks
const {
@@ -56,19 +59,10 @@ const MainDashboard: React.FC = () => {
const handleToolClick = (tool: Tool) => {
console.log('Navigating to tool:', tool.path);
// Handle SEO Dashboard navigation
if (tool.path === '/seo-dashboard') {
window.location.href = '/seo-dashboard';
if (tool.path) {
navigate(tool.path);
return;
}
// Handle Content Planning Dashboard navigation
if (tool.path === '/content-planning') {
window.location.href = '/content-planning';
return;
}
showSnackbar(`Launching ${tool.name}...`, 'info');
};
@@ -128,18 +122,8 @@ const MainDashboard: React.FC = () => {
<DashboardHeader
title="🚀 Alwrity Content Hub"
subtitle="Your AI-powered content creation suite"
statusChips={[
{
label: 'Active',
color: '#4CAF50',
icon: <span></span>,
},
{
label: 'Premium',
color: '#FFD700',
icon: <span></span>,
},
]}
statusChips={[]}
rightContent={<SystemStatusIndicator />}
/>
{/* Search and Filter */}

View File

@@ -29,6 +29,22 @@ const SEOCopilotKitProvider: React.FC<SEOCopilotKitProviderProps> = ({
// Get the CopilotKit API key from environment variables
const publicApiKey = process.env.REACT_APP_COPILOTKIT_API_KEY;
// Derive a friendly site/brand name from the URL for personalization
const domainRootName = useMemo(() => {
const url = analysisData?.url;
if (!url) return '';
try {
const withProto = url.startsWith('http') ? url : `https://${url}`;
const host = new URL(withProto).hostname;
const parts = host.split('.').filter(Boolean);
const root = parts.length >= 2 ? parts[parts.length - 2] : parts[0] || '';
if (!root) return '';
return root.charAt(0).toUpperCase() + root.slice(1);
} catch {
return '';
}
}, [analysisData?.url]);
// Suggestions model: progressive disclosure
const topLevelGroups = useMemo(() => ([
{ title: 'Content analysis', message: 'Content analysis' },
@@ -115,9 +131,12 @@ const SEOCopilotKitProvider: React.FC<SEOCopilotKitProviderProps> = ({
return (
<CopilotKit publicApiKey={publicApiKey}>
<CopilotSidebar
className="alwrity-copilot-sidebar"
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?",
title: domainRootName ? `ALwrity SEO • ${domainRootName}` : "ALwrity SEO Assistant",
initial: domainRootName
? `Hi! 👋 I'm your SEO assistant for ${domainRootName}. I can analyze your site, generate meta descriptions, check page speed, and more. What would you like to work on today?`
: "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) => {
@@ -125,6 +144,7 @@ const SEOCopilotKitProvider: React.FC<SEOCopilotKitProviderProps> = ({
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}
When greeting the user, personalize messages${domainRootName ? ` for ${domainRootName}` : ''} and keep a professional, friendly tone.
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();
@@ -202,6 +222,164 @@ Focus on actionable recommendations and use the registered tools.
height: 100%;
}
/* ALwrity glassomorphic styling for Copilot sidebar */
.alwrity-copilot-sidebar {
--alwrity-bg: linear-gradient(180deg, rgba(255,255,255,0.16), rgba(255,255,255,0.08));
--alwrity-border: rgba(255,255,255,0.22);
--alwrity-shadow: 0 18px 50px rgba(0,0,0,0.35);
--alwrity-accent: #667eea;
--alwrity-accent2: #764ba2;
--alwrity-text: rgba(255,255,255,0.92);
--alwrity-subtext: rgba(255,255,255,0.7);
}
.alwrity-copilot-sidebar * {
font-family: Inter, system-ui, -apple-system, Segoe UI, Roboto, Ubuntu, 'Helvetica Neue', Arial, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol';
}
.alwrity-copilot-sidebar .copilot-sidebar-container,
.alwrity-copilot-sidebar .copilotkit-sidebar,
.alwrity-copilot-sidebar .copilotkit-chat-container {
background: var(--alwrity-bg) !important;
backdrop-filter: blur(22px) !important;
-webkit-backdrop-filter: blur(22px) !important;
border: 1px solid var(--alwrity-border) !important;
box-shadow: var(--alwrity-shadow) !important;
color: var(--alwrity-text) !important;
}
.alwrity-copilot-sidebar .copilotkit-title,
.alwrity-copilot-sidebar .copilot-title {
color: var(--alwrity-text) !important;
font-weight: 700 !important;
letter-spacing: 0.2px !important;
}
.alwrity-copilot-sidebar .copilotkit-sidebar,
.alwrity-copilot-sidebar .copilot-sidebar-container {
z-index: 1200 !important;
}
.alwrity-copilot-sidebar .copilotkit-subtitle,
.alwrity-copilot-sidebar .copilot-subtitle,
.alwrity-copilot-sidebar .copilotkit-initial-message {
color: var(--alwrity-subtext) !important;
}
/* Suggestions: border, glow, depth, enterprise look */
.alwrity-copilot-sidebar .copilot-suggestion,
.alwrity-copilot-sidebar .copilotkit-suggestion,
.alwrity-copilot-sidebar .copilotkit-suggestions button,
.alwrity-copilot-sidebar .copilot-suggestions button {
position: relative;
background: linear-gradient(180deg, rgba(255,255,255,0.16), rgba(255,255,255,0.08)) !important;
border: 1.5px solid rgba(255,255,255,0.32) !important;
color: var(--alwrity-text) !important;
box-shadow: 0 10px 28px rgba(0,0,0,0.25), inset 0 1px 0 rgba(255,255,255,0.2) !important;
transition: transform 0.25s ease, box-shadow 0.25s ease, border-color 0.25s ease;
border-radius: 12px !important;
overflow: hidden;
backdrop-filter: blur(16px);
-webkit-backdrop-filter: blur(16px);
}
.alwrity-copilot-sidebar .copilot-suggestion::before,
.alwrity-copilot-sidebar .copilotkit-suggestion::before,
.alwrity-copilot-sidebar .copilotkit-suggestions button::before,
.alwrity-copilot-sidebar .copilot-suggestions button::before {
content: "";
position: absolute;
top: 0;
left: -120%;
width: 120%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.18), transparent);
transition: left 0.6s ease;
}
.alwrity-copilot-sidebar .copilot-suggestion:hover::before,
.alwrity-copilot-sidebar .copilotkit-suggestion:hover::before,
.alwrity-copilot-sidebar .copilotkit-suggestions button:hover::before,
.alwrity-copilot-sidebar .copilot-suggestions button:hover::before {
left: 120%;
}
.alwrity-copilot-sidebar .copilot-suggestion:hover,
.alwrity-copilot-sidebar .copilotkit-suggestion:hover,
.alwrity-copilot-sidebar .copilotkit-suggestions button:hover,
.alwrity-copilot-sidebar .copilot-suggestions button:hover {
transform: translateY(-4px) scale(1.015);
box-shadow: 0 18px 44px rgba(0,0,0,0.35), 0 0 0 1px rgba(255,255,255,0.18) inset !important;
border-color: rgba(255,255,255,0.45) !important;
}
.alwrity-copilot-sidebar .copilot-suggestion:active,
.alwrity-copilot-sidebar .copilotkit-suggestion:active,
.alwrity-copilot-sidebar .copilotkit-suggestions button:active,
.alwrity-copilot-sidebar .copilot-suggestions button:active {
transform: translateY(-1px) scale(0.995);
box-shadow: 0 8px 20px rgba(0,0,0,0.3) !important;
}
.alwrity-copilot-sidebar .copilot-suggestion:focus-visible,
.alwrity-copilot-sidebar .copilotkit-suggestion:focus-visible,
.alwrity-copilot-sidebar .copilotkit-suggestions button:focus-visible,
.alwrity-copilot-sidebar .copilot-suggestions button:focus-visible {
outline: none !important;
box-shadow: 0 0 0 2px rgba(255,255,255,0.45), 0 0 0 4px rgba(102,126,234,0.35) !important;
}
.alwrity-copilot-sidebar .copilot-suggestion .icon,
.alwrity-copilot-sidebar .copilotkit-suggestion .icon,
.alwrity-copilot-sidebar .copilotkit-suggestions button .icon {
filter: drop-shadow(0 2px 6px rgba(0,0,0,0.25));
}
.alwrity-copilot-sidebar .copilotkit-suggestions,
.alwrity-copilot-sidebar .copilot-suggestions {
display: grid;
grid-template-columns: 1fr;
gap: 10px;
}
@media (min-width: 420px) {
.alwrity-copilot-sidebar .copilotkit-suggestions,
.alwrity-copilot-sidebar .copilot-suggestions {
grid-template-columns: 1fr 1fr;
gap: 12px;
}
}
/* Reduce motion for users who prefer it */
@media (prefers-reduced-motion: reduce) {
.alwrity-copilot-sidebar .copilot-suggestion,
.alwrity-copilot-sidebar .copilotkit-suggestion,
.alwrity-copilot-sidebar .copilotkit-suggestions button,
.alwrity-copilot-sidebar .copilot-suggestions button {
transition: none !important;
}
.alwrity-copilot-sidebar .copilot-suggestion::before,
.alwrity-copilot-sidebar .copilotkit-suggestion::before,
.alwrity-copilot-sidebar .copilotkit-suggestions button::before,
.alwrity-copilot-sidebar .copilot-suggestions button::before {
display: none !important;
}
}
/* Scrollbar styling (webkit) */
.alwrity-copilot-sidebar ::-webkit-scrollbar {
width: 10px;
height: 10px;
}
.alwrity-copilot-sidebar ::-webkit-scrollbar-thumb {
background: rgba(255,255,255,0.25);
border: 2px solid rgba(0,0,0,0.1);
border-radius: 10px;
}
.alwrity-copilot-sidebar ::-webkit-scrollbar-track {
background: rgba(255,255,255,0.08);
border-radius: 10px;
}
.alwrity-copilot-sidebar .copilot-primary,
.alwrity-copilot-sidebar .copilotkit-primary-button,
.alwrity-copilot-sidebar button[type="submit"] {
background: linear-gradient(90deg, var(--alwrity-accent), var(--alwrity-accent2)) !important;
border: 1px solid rgba(255,255,255,0.35) !important;
color: white !important;
}
.alwrity-copilot-sidebar .copilot-input,
.alwrity-copilot-sidebar .copilotkit-input {
background: rgba(255,255,255,0.14) !important;
color: var(--alwrity-text) !important;
border: 1px solid rgba(255,255,255,0.22) !important;
}
.seo-copilotkit-loading {
position: fixed;
top: 0;

View File

@@ -6,7 +6,8 @@ import { DashboardHeaderProps } from './types';
const DashboardHeader: React.FC<DashboardHeaderProps> = ({
title,
subtitle,
statusChips = []
statusChips = [],
rightContent
}) => {
return (
<ShimmerHeader sx={{ mb: 5 }}>
@@ -33,23 +34,26 @@ const DashboardHeader: React.FC<DashboardHeaderProps> = ({
{subtitle}
</Typography>
</Box>
{statusChips.length > 0 && (
<Box sx={{ display: 'flex', gap: 1.5 }}>
{statusChips.map((chip, index) => (
<Chip
key={index}
icon={chip.icon}
label={chip.label}
sx={{
background: `${chip.color}20`,
border: `1px solid ${chip.color}40`,
color: chip.color,
fontWeight: 700,
}}
/>
))}
</Box>
)}
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5 }}>
{statusChips.length > 0 && (
<Box sx={{ display: 'flex', gap: 1.5 }}>
{statusChips.map((chip, index) => (
<Chip
key={index}
icon={chip.icon}
label={chip.label}
sx={{
background: `${chip.color}20`,
border: `1px solid ${chip.color}40`,
color: chip.color,
fontWeight: 700,
}}
/>
))}
</Box>
)}
{rightContent}
</Box>
</Box>
</ShimmerHeader>
);

View File

@@ -83,6 +83,7 @@ export interface DashboardHeaderProps {
color: string;
icon: React.ReactElement;
}>;
rightContent?: React.ReactNode;
}
export interface LoadingSkeletonProps {