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