This commit is contained in:
Developers Digest
2025-11-19 10:15:21 -05:00
320 changed files with 38446 additions and 7311 deletions
@@ -0,0 +1,844 @@
"use client";
import { motion, AnimatePresence } from "framer-motion";
import {
Globe,
FileText,
Code,
Shield,
// Search, // Not used in current implementation
Zap,
Database,
// Lock, // Not used in current implementation
CheckCircle2,
XCircle,
Loader2,
AlertCircle,
Bot,
Sparkles,
FileCode,
Network,
Info,
Eye
} from "lucide-react";
import { useEffect, useState } from "react";
import ScoreChart from "./ScoreChart";
import RadarChart from "./RadarChart";
import MetricBars from "./MetricBars";
interface ControlPanelProps {
isAnalyzing: boolean;
showResults: boolean;
url: string;
analysisData?: any;
onReset: () => void;
}
interface CheckItem {
id: string;
label: string;
description: string;
icon: any;
status: 'pending' | 'checking' | 'pass' | 'fail' | 'warning';
score?: number;
details?: string;
recommendation?: string;
actionItems?: string[];
tooltip?: string;
}
export default function ControlPanel({
isAnalyzing,
showResults,
url,
analysisData,
onReset,
}: ControlPanelProps) {
const [showAIAnalysis, setShowAIAnalysis] = useState(false); // Reserved for AI analysis feature
const [aiInsights, setAiInsights] = useState<CheckItem[]>([]);
const [isAnalyzingAI, setIsAnalyzingAI] = useState(false);
const [combinedChecks, setCombinedChecks] = useState<CheckItem[]>([]);
const [checks, setChecks] = useState<CheckItem[]>([
{
id: 'heading-structure',
label: 'Heading Hierarchy',
description: 'H1-H6 structure',
icon: FileText,
status: 'pending',
},
{
id: 'readability',
label: 'Readability',
description: 'Content clarity',
icon: Globe,
status: 'pending',
},
{
id: 'meta-tags',
label: 'Metadata Quality',
description: 'Title, desc, author',
icon: FileCode,
status: 'pending',
},
{
id: 'semantic-html',
label: 'Semantic HTML',
description: 'Proper HTML5 tags',
icon: Code,
status: 'pending',
},
{
id: 'accessibility',
label: 'Accessibility',
description: 'Alt text & ARIA',
icon: Eye,
status: 'pending',
},
{
id: 'llms-txt',
label: 'LLMs.txt',
description: 'AI permissions',
icon: Bot,
status: 'pending',
},
{
id: 'robots-txt',
label: 'Robots.txt',
description: 'Crawler rules',
icon: Shield,
status: 'pending',
},
{
id: 'sitemap',
label: 'Sitemap',
description: 'Site structure',
icon: Network,
status: 'pending',
},
]);
const [overallScore, setOverallScore] = useState(0);
const [currentCheckIndex, setCurrentCheckIndex] = useState(-1);
const [selectedCheck, setSelectedCheck] = useState<string | null>(null);
const [hoveredCheck, setHoveredCheck] = useState<string | null>(null);
const [enhancedScore, setEnhancedScore] = useState(0);
const [viewMode, setViewMode] = useState<'grid' | 'chart' | 'bars'>('grid');
useEffect(() => {
if (analysisData && analysisData.checks && showResults) {
// Use real data from API
const mappedChecks = analysisData.checks.map((check: any) => ({
...check,
icon: checks.find(c => c.id === check.id)?.icon || FileText,
description: check.details || checks.find(c => c.id === check.id)?.description,
}));
setChecks(mappedChecks);
setCombinedChecks(mappedChecks); // Initialize with basic checks
setOverallScore(analysisData.overallScore || 0);
setCurrentCheckIndex(-1);
// If AI analysis should auto-start, handle the promise
if (analysisData.autoStartAI && analysisData.aiAnalysisPromise) {
console.log('Auto-starting AI analysis with promise');
setIsAnalyzingAI(true);
setShowAIAnalysis(true);
// Add placeholder AI tiles immediately with actual titles
const placeholderAIChecks = [
{
id: 'ai-loading-0',
label: 'Content Quality for AI',
description: 'Analyzing content signal ratio...',
icon: Sparkles,
status: 'checking' as const,
score: 0,
isAI: true,
isLoading: true
},
{
id: 'ai-loading-1',
label: 'Information Architecture',
description: 'Evaluating page structure...',
icon: Bot,
status: 'checking' as const,
score: 0,
isAI: true,
isLoading: true
},
{
id: 'ai-loading-2',
label: 'Crawlability Patterns',
description: 'Checking JavaScript usage...',
icon: Database,
status: 'checking' as const,
score: 0,
isAI: true,
isLoading: true
},
{
id: 'ai-loading-3',
label: 'AI Training Value',
description: 'Assessing training potential...',
icon: Network,
status: 'checking' as const,
score: 0,
isAI: true,
isLoading: true
},
{
id: 'ai-loading-4',
label: 'Knowledge Extraction',
description: 'Analyzing entity definitions...',
icon: FileCode,
status: 'checking' as const,
score: 0,
isAI: true,
isLoading: true
},
{
id: 'ai-loading-5',
label: 'Template Quality',
description: 'Reviewing semantic structure...',
icon: Shield,
status: 'checking' as const,
score: 0,
isAI: true,
isLoading: true
},
{
id: 'ai-loading-6',
label: 'Content Depth',
description: 'Measuring content richness...',
icon: Zap,
status: 'checking' as const,
score: 0,
isAI: true,
isLoading: true
},
{
id: 'ai-loading-7',
label: 'Machine Readability',
description: 'Testing extraction reliability...',
icon: Globe,
status: 'checking' as const,
score: 0,
isAI: true,
isLoading: true
}
];
// Add loading AI tiles with staggered animation
placeholderAIChecks.forEach((check, idx) => {
setTimeout(() => {
setCombinedChecks(prev => [...prev, check]);
}, 100 * (idx + 1));
});
// Handle the AI analysis promise
analysisData.aiAnalysisPromise
.then(async (aiResponse: any) => {
if (aiResponse) {
const data = await aiResponse.json();
if (data.success && data.insights) {
// Convert AI insights to CheckItem format
const aiChecks: CheckItem[] = data.insights.map((insight: any, idx: number) => ({
...insight,
icon: [Sparkles, Bot, Database, Network, FileCode, Shield, Zap, Globe][idx % 8],
description: insight.details?.substring(0, 60) + '...' || 'AI Analysis',
isAI: true,
}));
setAiInsights(aiChecks);
// Replace loading tiles with real AI tiles
setCombinedChecks(prev => {
// Remove loading tiles
const withoutLoading = prev.filter(c => !(c as any).isLoading);
// Add real AI tiles
return [...withoutLoading, ...aiChecks];
});
// Calculate enhanced score
if (data.insights.length > 0) {
const aiScores = data.insights.map((i: any) => i.score || 0);
const avgAiScore = aiScores.reduce((a: number, b: number) => a + b, 0) / aiScores.length;
const combinedScore = Math.round((overallScore * 0.6) + (avgAiScore * 0.4));
setEnhancedScore(combinedScore);
}
}
}
})
.catch((error: any) => {
console.error('AI analysis error:', error);
// Remove loading tiles on error
setCombinedChecks(prev => prev.filter(c => !(c as any).isLoading));
})
.finally(() => {
setIsAnalyzingAI(false);
});
}
} else if (isAnalyzing) {
// Reset all checks when starting analysis
const resetChecks = checks.map(check => ({ ...check, status: 'pending' as const }));
setChecks(resetChecks);
setCombinedChecks(resetChecks); // Reset combined checks too
setCurrentCheckIndex(0);
setOverallScore(0);
// Visual animation while waiting for real results
const checkInterval = setInterval(() => {
setCurrentCheckIndex(prev => {
if (prev >= checks.length - 1) {
clearInterval(checkInterval);
return prev;
}
return prev + 1;
});
}, 200);
return () => clearInterval(checkInterval);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isAnalyzing, showResults, analysisData]);
useEffect(() => {
if (currentCheckIndex >= 0 && currentCheckIndex < checks.length && isAnalyzing) {
// Mark current as checking during animation
setChecks(prev => prev.map((check, index) => {
if (index === currentCheckIndex) {
return { ...check, status: 'checking' };
}
if (index < currentCheckIndex) {
return { ...check, status: 'checking' };
}
return check;
}));
// Update combinedChecks to show the animation
setCombinedChecks(prev => prev.map((check, index) => {
if (index === currentCheckIndex) {
return { ...check, status: 'checking' };
}
if (index < currentCheckIndex) {
return { ...check, status: 'checking' };
}
return check;
}));
}
}, [currentCheckIndex, checks.length, isAnalyzing]);
const getStatusIcon = (status: CheckItem['status']) => {
switch (status) {
case 'checking':
return <Loader2 className="w-16 h-16 text-heat-100 animate-spin" />;
case 'pass':
return <CheckCircle2 className="w-16 h-16 text-accent-black" />;
case 'fail':
return <XCircle className="w-16 h-16 text-heat-200" />;
case 'warning':
return <AlertCircle className="w-16 h-16 text-heat-100" />;
default:
return <div className="w-16 h-16 rounded-full border border-black-alpha-8" />;
}
};
// Utility function available but not used in current render
const getScoreColor = (score: number) => {
if (score >= 80) return "text-accent-black";
if (score >= 60) return "text-accent-black";
return "text-accent-black";
};
return (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
transition={{ duration: 0.5 }}
className="w-full max-w-[1200px] mx-auto"
>
{/* Header */}
<motion.div
className="text-center mb-48 pt-24 md:pt-0"
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.2 }}
>
<h2 className="text-title-h2 text-accent-black mb-12">AI Readiness Analysis</h2>
<p className="text-body-large text-black-alpha-64">Single-page snapshot of {url}</p>
{showResults && (
<>
{/* View Mode Toggle - Moved above score */}
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.3 }}
className="mt-24 mb-20 flex justify-center gap-4"
>
<button
onClick={() => setViewMode('grid')}
className={`px-16 py-8 rounded-8 text-label-medium font-medium transition-all ${
viewMode === 'grid'
? 'bg-accent-black text-white shadow-md'
: 'bg-black-alpha-4 text-black-alpha-64 hover:bg-black-alpha-8'
}`}
>
Grid View
</button>
<button
onClick={() => setViewMode('chart')}
className={`px-16 py-8 rounded-8 text-label-medium font-medium transition-all ${
viewMode === 'chart'
? 'bg-accent-black text-white shadow-md'
: 'bg-black-alpha-4 text-black-alpha-64 hover:bg-black-alpha-8'
}`}
>
Radar Chart
</button>
<button
onClick={() => setViewMode('bars')}
className={`px-16 py-8 rounded-8 text-label-medium font-medium transition-all ${
viewMode === 'bars'
? 'bg-accent-black text-white shadow-md'
: 'bg-black-alpha-4 text-black-alpha-64 hover:bg-black-alpha-8'
}`}
>
Bar Chart
</button>
</motion.div>
<motion.div
initial={{ scale: 0 }}
animate={{ scale: 1 }}
transition={{ type: "spring", delay: 0.5 }}
className="flex justify-center"
>
<ScoreChart
score={enhancedScore > 0 ? enhancedScore : overallScore}
enhanced={enhancedScore > 0}
size={180}
/>
</motion.div>
</>
)}
</motion.div>
{/* Conditional rendering based on view mode */}
{viewMode === 'grid' && (
<div className="grid grid-cols-2 lg:grid-cols-4 gap-12 mb-40 px-40 relative">
{combinedChecks.map((check, index) => {
const isActive = index === currentCheckIndex;
return (
<motion.div
key={check.id}
initial={(check as any).isAI ? { opacity: 1, scale: 1 } : { opacity: 0, scale: 0.9 }}
animate={{
opacity: 1,
scale: isActive ? 1.05 : 1,
}}
transition={{
delay: (check as any).isAI ? 0 : index * 0.1,
scale: { type: "spring", stiffness: 300 }
}}
className={`
relative p-16 rounded-8 transition-all bg-accent-white border
${(check as any).isAI ? 'border-heat-100 border-opacity-40 bg-gradient-to-br from-accent-white to-heat-4' : 'border-black-alpha-8'}
${isActive ? 'border-heat-100 shadow-lg' : ''}
${check.status !== 'pending' && check.status !== 'checking' ? 'cursor-pointer hover:shadow-md' : ''}
${(check as any).isLoading ? 'animate-pulse' : ''}
`}
onClick={() => {
if (check.status !== 'pending' && check.status !== 'checking') {
setSelectedCheck(selectedCheck === check.id ? null : check.id);
}
}}
onMouseEnter={() => setHoveredCheck(check.id)}
onMouseLeave={() => setHoveredCheck(null)}
>
<div className="relative">
<div className="flex items-start justify-end mb-12">
{getStatusIcon(check.status)}
</div>
<h3 className="text-label-large mb-4 text-accent-black font-medium flex items-center gap-6">
{check.label}
{check.tooltip && !aiInsights.some(ai => ai.id === check.id) && (
<div className="relative inline-block">
<Info className="w-14 h-14 text-black-alpha-32 hover:text-black-alpha-64 transition-colors" />
<AnimatePresence>
{hoveredCheck === check.id && (
<motion.div
initial={{ opacity: 0, y: 5 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: 5 }}
className="absolute bottom-full left-1/2 -translate-x-1/2 mb-8 w-200 p-8 bg-accent-black text-white text-body-x-small rounded-6 shadow-lg z-50 pointer-events-none"
>
{check.tooltip}
<div className="absolute top-full left-1/2 -translate-x-1/2 -mt-1 w-0 h-0 border-l-4 border-r-4 border-t-4 border-transparent border-t-accent-black" />
</motion.div>
)}
</AnimatePresence>
</div>
)}
</h3>
<p className="text-body-small text-black-alpha-64">
{check.description}
</p>
{check.status !== 'pending' && check.status !== 'checking' && (
<>
<motion.div
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
className="mt-8"
>
<div className="h-2 bg-black-alpha-4 rounded-full overflow-hidden">
<motion.div
className={`
h-full rounded-full
${check.status === 'pass' ? 'bg-accent-black' : ''}
${check.status === 'warning' ? 'bg-heat-100' : ''}
${check.status === 'fail' ? 'bg-heat-200' : ''}
`}
initial={{ width: 0 }}
animate={{ width: `${check.score}%` }}
transition={{ duration: 0.5 }}
/>
</div>
</motion.div>
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.5 }}
className="text-label-x-small text-black-alpha-32 mt-4 text-center"
>
Click for details
</motion.div>
</>
)}
</div>
{/* Expanded Details */}
<AnimatePresence>
{selectedCheck === check.id && check.details && (
<motion.div
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -10 }}
transition={{ duration: 0.2 }}
className="mt-12 pt-12 border-t border-black-alpha-8"
>
<div className="space-y-6">
<div>
<div className="text-label-small text-black-alpha-48 mb-2">Status</div>
<div className="text-body-small text-accent-black">{check.details}</div>
</div>
<div>
<div className="text-label-small text-black-alpha-48 mb-2">Recommendation</div>
<div className="text-body-small text-black-alpha-64">{check.recommendation}</div>
{check.actionItems && check.actionItems.length > 0 && (
<ul className="mt-4 space-y-2">
{check.actionItems.map((item: string, i: number) => (
<li key={i} className="flex items-start gap-6 text-body-small text-black-alpha-64">
<span className="text-heat-100 mt-1"></span>
<span>{item}</span>
</li>
))}
</ul>
)}
</div>
</div>
</motion.div>
)}
</AnimatePresence>
</motion.div>
);
})}
</div>
)}
{/* Radar Chart View */}
{viewMode === 'chart' && showResults && (
<div>
<motion.div
className="flex justify-center gap-40 mb-40"
initial={{ opacity: 0, scale: 0.9 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ duration: 0.3 }}
>
{/* Basic Analysis Chart */}
<div className="flex flex-col items-center">
<h3 className="text-label-large text-accent-black mb-16 font-medium">Basic Analysis</h3>
<RadarChart
data={checks
.filter(check => check.status !== 'pending' && check.status !== 'checking')
.slice(0, 8)
.map(check => ({
label: check.label.length > 12 ? check.label.substring(0, 12) + '...' : check.label,
score: check.score || 0
}))}
size={350}
/>
<div className="mt-16 text-center">
<div className="text-title-h3 text-accent-black">{overallScore}%</div>
<div className="text-label-small text-black-alpha-48">Overall Score</div>
</div>
</div>
{/* VS Indicator */}
{aiInsights.length > 0 && (
<motion.div
className="flex items-center"
initial={{ opacity: 0, scale: 0 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ delay: 0.2, type: "spring" }}
>
<div className="text-label-large text-black-alpha-32 font-medium">VS</div>
</motion.div>
)}
{/* AI Analysis Chart - Only show if AI insights exist */}
{aiInsights.length > 0 && (
<motion.div
className="flex flex-col items-center"
initial={{ opacity: 0, x: 20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: 0.3 }}
>
<h3 className="text-label-large text-heat-100 mb-16 font-medium">AI Enhanced Analysis</h3>
<RadarChart
data={aiInsights
.filter(check => check.status !== 'pending' && check.status !== 'checking')
.slice(0, 8)
.map(check => ({
label: check.label.length > 12 ? check.label.substring(0, 12) + '...' : check.label,
score: check.score || 0
}))}
size={350}
/>
<div className="mt-16 text-center">
<div className="text-title-h3 text-heat-100">
{Math.round(aiInsights.reduce((sum, check) => sum + (check.score || 0), 0) / aiInsights.length)}%
</div>
<div className="text-label-small text-heat-100 opacity-60">AI Score</div>
</div>
</motion.div>
)}
</motion.div>
{/* Comparison Summary */}
{aiInsights.length > 0 && (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.5 }}
className="text-center mb-20"
>
<div className="inline-flex items-center gap-8 px-16 py-8 bg-heat-4 rounded-8">
<span className="text-label-medium text-accent-black">
AI analysis found {aiInsights.filter(i => i.score && i.score < 50).length} additional areas for improvement
</span>
</div>
</motion.div>
)}
</div>
)}
{/* Bar Chart View */}
{viewMode === 'bars' && showResults && (
<motion.div
className="px-40 mb-40"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3 }}
>
<MetricBars
metrics={combinedChecks
.filter(check => check.status !== 'pending' && check.status !== 'checking')
.map(check => ({
label: check.label,
score: check.score || 0,
status: check.status as 'pass' | 'warning' | 'fail',
category: (check as any).isAI ? 'ai' :
['robots-txt', 'sitemap', 'llms-txt'].includes(check.id) ? 'domain' : 'page',
details: check.details,
recommendation: check.recommendation,
actionItems: check.actionItems
}))}
/>
</motion.div>
)}
{/* Action Buttons */}
{showResults && (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.8 }}
className="flex gap-12 justify-center"
>
<button
onClick={onReset}
className="px-20 py-10 bg-accent-white border border-black-alpha-8 hover:bg-black-alpha-4 rounded-8 text-label-medium transition-all"
>
Analyze Another Site
</button>
{true && (
<button
onClick={async () => {
setIsAnalyzingAI(true);
setShowAIAnalysis(true);
// Add placeholder AI tiles immediately with actual titles
const placeholderAIChecks = [
{
id: 'ai-loading-0',
label: 'Content Quality for AI',
description: 'Analyzing content signal ratio...',
icon: Sparkles,
status: 'checking' as const,
score: 0,
isAI: true,
isLoading: true
},
{
id: 'ai-loading-1',
label: 'Information Architecture',
description: 'Evaluating page structure...',
icon: Bot,
status: 'checking' as const,
score: 0,
isAI: true,
isLoading: true
},
{
id: 'ai-loading-2',
label: 'Crawlability Patterns',
description: 'Checking JavaScript usage...',
icon: Database,
status: 'checking' as const,
score: 0,
isAI: true,
isLoading: true
},
{
id: 'ai-loading-3',
label: 'AI Training Value',
description: 'Assessing training potential...',
icon: Network,
status: 'checking' as const,
score: 0,
isAI: true,
isLoading: true
},
{
id: 'ai-loading-4',
label: 'Knowledge Extraction',
description: 'Analyzing entity definitions...',
icon: FileCode,
status: 'checking' as const,
score: 0,
isAI: true,
isLoading: true
},
{
id: 'ai-loading-5',
label: 'Template Quality',
description: 'Reviewing semantic structure...',
icon: Shield,
status: 'checking' as const,
score: 0,
isAI: true,
isLoading: true
},
{
id: 'ai-loading-6',
label: 'Content Depth',
description: 'Measuring content richness...',
icon: Zap,
status: 'checking' as const,
score: 0,
isAI: true,
isLoading: true
},
{
id: 'ai-loading-7',
label: 'Machine Readability',
description: 'Testing extraction reliability...',
icon: Globe,
status: 'checking' as const,
score: 0,
isAI: true,
isLoading: true
}
];
// Add loading AI tiles with staggered animation immediately
placeholderAIChecks.forEach((check, idx) => {
setTimeout(() => {
setCombinedChecks(prev => [...prev, check]);
}, 100 * (idx + 1));
});
try {
const response = await fetch('/api/ai-analysis', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
url,
htmlContent: analysisData?.htmlContent || '',
currentChecks: checks
})
});
const data = await response.json();
if (data.success && data.insights) {
// Convert AI insights to CheckItem format with AI flag
const aiChecks: CheckItem[] = data.insights.map((insight: any, idx: number) => ({
...insight,
icon: [Sparkles, Bot, Database, Network, FileCode, Shield, Zap, Globe][idx % 8],
description: insight.details?.substring(0, 60) + '...' || 'AI Analysis',
isAI: true, // Mark as AI-generated
}));
setAiInsights(aiChecks);
// Replace loading tiles with real AI tiles
setCombinedChecks(prev => {
// Remove loading tiles
const withoutLoading = prev.filter(c => !(c as any).isLoading);
// Add real AI tiles
return [...withoutLoading, ...aiChecks];
});
// Calculate enhanced score
if (data.insights.length > 0) {
const aiScores = data.insights.map((i: any) => i.score || 0);
const avgAiScore = aiScores.reduce((a: number, b: number) => a + b, 0) / aiScores.length;
const combinedScore = Math.round((overallScore * 0.6) + (avgAiScore * 0.4));
setEnhancedScore(combinedScore);
}
}
} catch (error) {
console.error('AI analysis error:', error);
// Remove loading tiles on error
setCombinedChecks(prev => prev.filter(c => !(c as any).isLoading));
} finally {
setIsAnalyzingAI(false);
}
}}
disabled={isAnalyzingAI}
className="px-20 py-10 bg-accent-black hover:bg-black-alpha-80 text-white rounded-8 text-label-medium transition-all disabled:opacity-50"
>
{isAnalyzingAI ? 'Analyzing...' : 'Analyze with AI'}
</button>
)}
</motion.div>
)}
</motion.div>
);
}
@@ -0,0 +1,342 @@
"use client";
import { motion, AnimatePresence } from "framer-motion";
import { Check, X, FileText, Globe, Code, Sparkles, AlertCircle } from "lucide-react";
// import { Zap, Shield } from "lucide-react"; // Reserved for future features
import { useEffect, useState } from "react";
interface InlineResultsProps {
isAnalyzing: boolean;
showResults: boolean;
analysisStep: number;
url: string;
onReset: () => void;
}
const analysisSteps = [
"Fetching website content...",
"Checking for LLMs.txt...",
"Analyzing HTML structure...",
"Calculating AI readiness...",
];
// Placeholder data for the results
const mockResults = {
score: 78,
grade: "B+",
llmsTxt: true,
robotsTxt: true,
structuredData: true,
semanticHTML: false,
metaTags: true,
accessibility: true,
};
export default function InlineResults({
isAnalyzing,
showResults,
analysisStep,
url: _url, // URL prop available but not used in current implementation
onReset,
}: InlineResultsProps) {
const [displayScore, setDisplayScore] = useState(0);
useEffect(() => {
if (showResults) {
// Animate score counting up
const target = mockResults.score;
const duration = 1500;
const increment = target / (duration / 16);
let current = 0;
const timer = setInterval(() => {
current += increment;
if (current >= target) {
setDisplayScore(target);
clearInterval(timer);
} else {
setDisplayScore(Math.floor(current));
}
}, 16);
return () => clearInterval(timer);
}
}, [showResults]);
const getScoreColor = (score: number) => {
if (score >= 80) return "#22c55e";
if (score >= 60) return "#eab308";
return "#ef4444";
};
return (
<AnimatePresence mode="wait">
{/* Analyzing State */}
{isAnalyzing && (
<motion.div
key="analyzing"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
transition={{ duration: 0.3 }}
className="space-y-20"
>
{/* Progress Bar */}
<div className="relative">
<div className="h-2 bg-black-alpha-4 rounded-full overflow-hidden">
<motion.div
className="h-full bg-gradient-to-r from-heat-100 to-heat-200"
initial={{ width: "0%" }}
animate={{ width: `${((analysisStep + 1) / 4) * 100}%` }}
transition={{ duration: 0.5, ease: "easeOut" }}
/>
</div>
{/* Glowing dot at the end of progress */}
<motion.div
className="absolute top-1/2 -translate-y-1/2 w-3 h-3 bg-heat-100 rounded-full"
style={{
left: `${((analysisStep + 1) / 4) * 100}%`,
boxShadow: "0 0 20px rgba(255, 77, 0, 0.8)",
}}
animate={{
scale: [1, 1.5, 1],
opacity: [1, 0.8, 1],
}}
transition={{
duration: 0.8,
repeat: Infinity,
}}
/>
</div>
{/* Status Text */}
<motion.div
key={analysisStep}
initial={{ opacity: 0, x: -10 }}
animate={{ opacity: 1, x: 0 }}
className="flex items-center gap-8 text-body-medium text-black-alpha-64"
>
<motion.div
animate={{ rotate: 360 }}
transition={{ duration: 1, repeat: Infinity, ease: "linear" }}
>
<Sparkles className="w-16 h-16 text-heat-100" />
</motion.div>
{analysisSteps[analysisStep]}
</motion.div>
{/* ASCII Animation */}
<motion.div
className="font-mono text-xs text-black-alpha-16 overflow-hidden h-32 relative"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.2 }}
>
<motion.div
animate={{ y: [0, -10, 0] }}
transition={{ duration: 2, repeat: Infinity }}
className="absolute inset-0 flex items-center justify-center"
>
{'< analyzing />'}
</motion.div>
</motion.div>
</motion.div>
)}
{/* Results State */}
{showResults && (
<motion.div
key="results"
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.95 }}
transition={{ duration: 0.4, ease: "easeOut" }}
className="space-y-24"
>
{/* Score Display */}
<div className="text-center">
<motion.div
initial={{ scale: 0 }}
animate={{ scale: 1 }}
transition={{
type: "spring",
stiffness: 200,
delay: 0.2
}}
className="relative inline-block"
>
{/* Background glow */}
<motion.div
className="absolute inset-0 rounded-full blur-xl"
style={{ background: getScoreColor(mockResults.score) }}
animate={{
opacity: [0.3, 0.6, 0.3],
scale: [0.8, 1.2, 0.8],
}}
transition={{
duration: 2,
repeat: Infinity,
}}
/>
{/* Score circle */}
<div
className="relative w-120 h-120 rounded-full flex flex-col items-center justify-center"
style={{
background: `conic-gradient(from 0deg, ${getScoreColor(mockResults.score)} ${displayScore * 3.6}deg, #f0f0f0 ${displayScore * 3.6}deg)`,
padding: "4px",
}}
>
<div className="w-full h-full bg-white rounded-full flex flex-col items-center justify-center">
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.5 }}
className="text-4xl font-bold"
style={{ color: getScoreColor(mockResults.score) }}
>
{displayScore}
</motion.div>
<motion.div
initial={{ opacity: 0, y: 5 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.7 }}
className="text-label-medium text-black-alpha-48"
>
AI Ready
</motion.div>
</div>
</div>
</motion.div>
</div>
{/* Quick Checks Grid */}
<motion.div
className="grid grid-cols-3 gap-12"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.8 }}
>
{[
{
label: "LLMs.txt",
value: mockResults.llmsTxt,
icon: FileText,
description: "AI instructions",
detail: mockResults.llmsTxt ? "Found" : "Missing"
},
{
label: "Structured Data",
value: mockResults.structuredData,
icon: Code,
description: "Schema markup",
detail: mockResults.structuredData ? "Detected" : "Not found"
},
{
label: "Semantic HTML",
value: mockResults.semanticHTML,
icon: Globe,
description: "HTML5 tags",
detail: mockResults.semanticHTML ? "Good" : "Needs work"
},
].map((item, index) => (
<motion.div
key={item.label}
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.9 + index * 0.1 }}
className={`
relative p-16 rounded-12 transition-all hover:shadow-md cursor-pointer
${item.value
? 'bg-gradient-to-br from-green-50 to-green-100/50 border-green-200'
: 'bg-gradient-to-br from-red-50 to-red-100/50 border-red-200'}
border
`}
>
{/* Status indicator */}
<div className="absolute top-12 right-12">
<motion.div
initial={{ scale: 0 }}
animate={{ scale: 1 }}
transition={{ delay: 1 + index * 0.1, type: "spring" }}
className={`
w-24 h-24 rounded-full flex items-center justify-center
${item.value ? 'bg-green-500' : 'bg-red-500'}
`}
>
{item.value ? (
<Check className="w-14 h-14 text-white" strokeWidth={3} />
) : (
<X className="w-14 h-14 text-white" strokeWidth={3} />
)}
</motion.div>
</div>
{/* Icon */}
<div className="mb-12">
<item.icon className={`
w-24 h-24
${item.value ? 'text-green-600' : 'text-red-600'}
`} />
</div>
{/* Content */}
<div className="space-y-4">
<div className="text-label-medium text-accent-black">
{item.label}
</div>
<div className="text-body-small text-black-alpha-48">
{item.description}
</div>
<div className={`
text-label-small font-semibold
${item.value ? 'text-green-600' : 'text-red-600'}
`}>
{item.detail}
</div>
</div>
</motion.div>
))}
</motion.div>
{/* Quick Tip */}
<motion.div
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 1.2 }}
className="p-12 bg-heat-4 rounded-8 border border-heat-100 flex items-start gap-8"
>
<AlertCircle className="w-16 h-16 text-heat-100 mt-2" />
<div className="flex-1">
<div className="text-label-medium text-accent-black mb-4">Quick Tip</div>
<div className="text-body-small text-black-alpha-64">
{mockResults.semanticHTML
? "Your site has good semantic HTML structure for AI understanding."
: "Add semantic HTML5 elements to improve AI comprehension of your content."}
</div>
</div>
</motion.div>
{/* Action Buttons */}
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 1.4 }}
className="flex gap-8"
>
<button
onClick={onReset}
className="flex-1 px-16 py-10 bg-black-alpha-4 hover:bg-black-alpha-6 rounded-8 text-label-medium transition-all"
>
Try Another
</button>
<button className="flex-1 px-16 py-10 bg-heat-100 hover:bg-heat-200 text-white rounded-8 text-label-medium transition-all shadow-lg hover:shadow-xl">
View Details
</button>
</motion.div>
</motion.div>
)}
</AnimatePresence>
);
}
@@ -0,0 +1,189 @@
"use client";
import { motion, AnimatePresence } from "framer-motion";
import { useState } from "react";
import { ChevronDown } from "lucide-react";
interface MetricBarsProps {
metrics: {
label: string;
score: number;
status: 'pass' | 'warning' | 'fail';
category?: 'page' | 'domain' | 'ai';
details?: string;
recommendation?: string;
actionItems?: string[];
}[];
}
export default function MetricBars({ metrics }: MetricBarsProps) {
const [expandedItems, setExpandedItems] = useState<Set<string>>(new Set());
const getBarColor = (score: number) => {
// Use brand orange colors with opacity for gradient effect
if (score >= 80) return 'bg-heat-100';
if (score >= 60) return 'bg-heat-90';
if (score >= 40) return 'bg-heat-40 opacity-80';
return 'bg-heat-20';
};
const getBulletColor = (_score: number) => {
// Always use heat-100 for all bullets for consistency
return 'bg-heat-100';
};
const toggleExpanded = (label: string) => {
const newExpanded = new Set(expandedItems);
if (newExpanded.has(label)) {
newExpanded.delete(label);
} else {
newExpanded.add(label);
}
setExpandedItems(newExpanded);
};
// Sort metrics by score descending
const sortedMetrics = [...metrics].sort((a, b) => b.score - a.score);
return (
<div className="space-y-8 max-w-[800px] mx-auto">
{sortedMetrics.map((metric, index) => {
const isExpanded = expandedItems.has(metric.label);
return (
<motion.div
key={metric.label}
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: index * 0.05, duration: 0.3 }}
className="space-y-0"
>
<div
className={`grid grid-cols-12 gap-4 items-center p-8 -m-8 rounded-8 cursor-pointer transition-all hover:bg-black-alpha-2 ${
isExpanded ? 'bg-black-alpha-4' : ''
}`}
onClick={() => toggleExpanded(metric.label)}
>
{/* Bullet and Label - fixed width */}
<div className="col-span-4 flex items-center gap-8">
<div className={`w-6 h-6 rounded-full ${getBulletColor(metric.score)}`} />
<span className="text-label-medium text-accent-black truncate">{metric.label}</span>
<motion.div
animate={{ rotate: isExpanded ? 180 : 0 }}
transition={{ duration: 0.2 }}
className="ml-auto"
>
<ChevronDown className="w-16 h-16 text-black-alpha-32" />
</motion.div>
</div>
{/* Bar container - flexible width */}
<div className="col-span-7 relative">
<div className="relative h-8 bg-black-alpha-8 rounded-full overflow-hidden">
{/* Animated bar */}
<motion.div
className={`absolute inset-y-0 left-0 ${getBarColor(metric.score)} rounded-full`}
initial={{ width: 0 }}
animate={{ width: `${Math.max(metric.score, 2)}%` }}
transition={{
delay: 0.2 + index * 0.05,
duration: 0.8,
ease: "easeOut"
}}
>
{/* Subtle inner glow */}
<div className="absolute inset-0 bg-gradient-to-t from-transparent to-white opacity-10 rounded-full" />
</motion.div>
{/* Score indicator lines at key thresholds */}
{[40, 60, 80].map(threshold => (
<div
key={threshold}
className="absolute top-0 bottom-0 w-px bg-black-alpha-8 opacity-30"
style={{ left: `${threshold}%` }}
/>
))}
</div>
</div>
{/* Score value - fixed width */}
<div className="col-span-1 text-right">
<span className="text-label-medium font-medium text-heat-100">
{metric.score}%
</span>
</div>
</div>
{/* Expanded Details */}
<AnimatePresence>
{isExpanded && metric.details && (
<motion.div
initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: "auto" }}
exit={{ opacity: 0, height: 0 }}
transition={{ duration: 0.3 }}
className="overflow-hidden"
>
<div className="pl-54 pr-12 py-12 space-y-8">
<div>
<div className="text-label-small text-black-alpha-48 mb-4">Status</div>
<div className="text-body-small text-accent-black">{metric.details}</div>
</div>
{metric.recommendation && (
<div>
<div className="text-label-small text-black-alpha-48 mb-4">Recommendation</div>
<div className="text-body-small text-black-alpha-64">{metric.recommendation}</div>
</div>
)}
{metric.actionItems && metric.actionItems.length > 0 && (
<div>
<div className="text-label-small text-black-alpha-48 mb-4">Action Items</div>
<ul className="space-y-4">
{metric.actionItems.map((item: string, i: number) => (
<li key={i} className="flex items-start gap-6 text-body-small text-black-alpha-64">
<span className="text-heat-100 mt-1"></span>
<span>{item}</span>
</li>
))}
</ul>
</div>
)}
</div>
</motion.div>
)}
</AnimatePresence>
</motion.div>
);
})}
{/* Summary stats */}
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.5 }}
className="mt-20 pt-12 border-t border-black-alpha-8"
>
<div className="grid grid-cols-3 gap-16 text-center">
<div>
<div className="text-title-h3 text-heat-150">
{metrics.filter(m => m.status === 'pass').length}
</div>
<div className="text-label-small text-black-alpha-48">Passing</div>
</div>
<div>
<div className="text-title-h3 text-heat-100">
{metrics.filter(m => m.status === 'warning').length}
</div>
<div className="text-label-small text-black-alpha-48">Warning</div>
</div>
<div>
<div className="text-title-h3 text-heat-50">
{metrics.filter(m => m.status === 'fail').length}
</div>
<div className="text-label-small text-black-alpha-48">Failing</div>
</div>
</div>
</motion.div>
</div>
);
}
@@ -0,0 +1,220 @@
"use client";
import { motion } from "framer-motion";
import { useEffect, useState } from "react";
interface RadarChartProps {
data: {
label: string;
score: number;
maxScore?: number;
}[];
size?: number;
}
export default function RadarChart({ data, size = 300 }: RadarChartProps) {
const [isAnimated, setIsAnimated] = useState(false);
const center = size / 2;
const radius = (size / 2) - 60; // Increased padding for labels
const angleStep = (Math.PI * 2) / data.length;
useEffect(() => {
setIsAnimated(true);
}, []);
// Calculate points for the polygon
const getPoint = (value: number, index: number) => {
const angle = index * angleStep - Math.PI / 2;
const r = (value / 100) * radius;
return {
x: center + r * Math.cos(angle),
y: center + r * Math.sin(angle)
};
};
// Create polygon points string
const polygonPoints = data
.map((item, i) => {
const point = getPoint(isAnimated ? item.score : 0, i);
return `${point.x},${point.y}`;
})
.join(' ');
// Grid levels
const gridLevels = [20, 40, 60, 80, 100];
return (
<div className="relative">
<svg width={size} height={size} className="overflow-visible">
<defs>
<linearGradient id="radar-gradient" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stopColor="#FF4A00" stopOpacity="0.8" />
<stop offset="100%" stopColor="#FF8533" stopOpacity="0.3" />
</linearGradient>
<filter id="radar-glow">
<feGaussianBlur stdDeviation="2" result="coloredBlur"/>
<feMerge>
<feMergeNode in="coloredBlur"/>
<feMergeNode in="SourceGraphic"/>
</feMerge>
</filter>
</defs>
{/* Grid circles */}
{gridLevels.map((level) => (
<circle
key={level}
cx={center}
cy={center}
r={(level / 100) * radius}
fill="none"
stroke="rgba(0,0,0,0.05)"
strokeWidth="1"
/>
))}
{/* Axis lines */}
{data.map((_, i) => {
const angle = i * angleStep - Math.PI / 2;
const x2 = center + radius * Math.cos(angle);
const y2 = center + radius * Math.sin(angle);
return (
<line
key={i}
x1={center}
y1={center}
x2={x2}
y2={y2}
stroke="rgba(0,0,0,0.05)"
strokeWidth="1"
/>
);
})}
{/* Data polygon */}
<motion.polygon
points={polygonPoints}
fill="url(#radar-gradient)"
stroke="#FF4A00"
strokeWidth="2"
initial={{ opacity: 0, scale: 0.8 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ duration: 0.8, ease: "easeOut" }}
filter="url(#radar-glow)"
/>
{/* Data points */}
{data.map((item, i) => {
const point = getPoint(item.score, i);
return (
<motion.circle
key={i}
cx={point.x}
cy={point.y}
r="4"
fill="#FF4A00"
stroke="white"
strokeWidth="2"
initial={{ scale: 0 }}
animate={{ scale: isAnimated ? 1 : 0 }}
transition={{ delay: 0.8 + i * 0.1, duration: 0.3 }}
/>
);
})}
{/* Labels */}
{data.map((item, i) => {
const angle = i * angleStep - Math.PI / 2;
const labelRadius = radius + 40; // Increased label distance
const x = center + labelRadius * Math.cos(angle);
const y = center + labelRadius * Math.sin(angle);
// Better text anchor logic based on quadrant
let textAnchor = "middle";
let dy = 0;
// Left side
if (x < center - 20) {
textAnchor = "end";
}
// Right side
else if (x > center + 20) {
textAnchor = "start";
}
// Top
if (y < center - 20) {
dy = -5;
}
// Bottom
else if (y > center + 20) {
dy = 5;
}
return (
<motion.g key={i}>
{/* Background for better readability */}
<motion.rect
x={x - (textAnchor === "middle" ? 30 : textAnchor === "end" ? 60 : 0)}
y={y - 10}
width={60}
height={20}
fill="white"
fillOpacity={0.9}
rx={4}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.9 + i * 0.05 }}
/>
<motion.text
x={x}
y={y + dy}
textAnchor={textAnchor as any}
dominantBaseline="middle"
className="text-xs fill-black-alpha-80 font-medium"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 1 + i * 0.05 }}
style={{ pointerEvents: 'none' }}
>
{item.label}
</motion.text>
{/* Score value */}
<motion.text
x={x}
y={y + dy + 12}
textAnchor={textAnchor as any}
dominantBaseline="middle"
className="text-[10px] fill-heat-100 font-bold"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 1.1 + i * 0.05 }}
style={{ pointerEvents: 'none' }}
>
{item.score}%
</motion.text>
</motion.g>
);
})}
</svg>
{/* Legend */}
<div className="mt-16 flex justify-center">
<div className="inline-flex flex-row gap-16 text-xs text-black-alpha-48 bg-white px-16 py-8 rounded-6 shadow-sm">
<div className="flex items-center gap-8">
<div className="w-12 h-12 rounded-full bg-heat-200" />
<span className="whitespace-nowrap">80-100%</span>
</div>
<div className="flex items-center gap-8">
<div className="w-12 h-12 rounded-full bg-heat-100" />
<span className="whitespace-nowrap">60-79%</span>
</div>
<div className="flex items-center gap-8">
<div className="w-12 h-12 rounded-full bg-heat-50" />
<span className="whitespace-nowrap">&lt;60%</span>
</div>
</div>
</div>
</div>
);
}
@@ -0,0 +1,104 @@
"use client";
import { motion } from "framer-motion";
import { useEffect, useState } from "react";
interface ScoreChartProps {
score: number;
enhanced?: boolean;
size?: number;
}
export default function ScoreChart({ score, enhanced = false, size = 200 }: ScoreChartProps) {
const [animatedScore, setAnimatedScore] = useState(0);
const radius = size / 2 - 20;
const circumference = 2 * Math.PI * radius;
useEffect(() => {
const timer = setTimeout(() => {
setAnimatedScore(score);
}, 300);
return () => clearTimeout(timer);
}, [score]);
// Calculate stroke dash offset for the progress
const strokeDashoffset = circumference - (animatedScore / 100) * circumference;
// Determine color based on score
const getColor = () => {
if (score >= 80) return "#FF4A00"; // heat-200 - Excellent
if (score >= 60) return "#FF6500"; // heat-150 - Good
if (score >= 40) return "#FF8533"; // heat-100 - Warning
return "#FFA566"; // heat-50 - Poor
};
const getGradientId = enhanced ? "enhanced-gradient" : "normal-gradient";
return (
<div className="relative inline-flex items-center justify-center">
<svg width={size} height={size} className="transform -rotate-90">
<defs>
<linearGradient id={getGradientId} x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stopColor={getColor()} stopOpacity="1" />
<stop offset="100%" stopColor={enhanced ? "#FF8533" : getColor()} stopOpacity="0.6" />
</linearGradient>
<filter id="glow">
<feGaussianBlur stdDeviation="3" result="coloredBlur"/>
<feMerge>
<feMergeNode in="coloredBlur"/>
<feMergeNode in="SourceGraphic"/>
</feMerge>
</filter>
</defs>
{/* Background circle */}
<circle
cx={size / 2}
cy={size / 2}
r={radius}
fill="none"
stroke="rgba(0,0,0,0.05)"
strokeWidth="12"
/>
{/* Progress circle */}
<motion.circle
cx={size / 2}
cy={size / 2}
r={radius}
fill="none"
stroke={`url(#${getGradientId})`}
strokeWidth="12"
strokeLinecap="round"
strokeDasharray={circumference}
initial={{ strokeDashoffset: circumference }}
animate={{ strokeDashoffset }}
transition={{ duration: 1.5, ease: "easeOut" }}
filter="url(#glow)"
/>
</svg>
{/* Center content */}
<div className="absolute inset-0 flex flex-col items-center justify-center">
<motion.div
className="text-4xl font-bold text-heat-150"
initial={{ opacity: 0, scale: 0.5 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ delay: 0.5, duration: 0.5 }}
>
{animatedScore}%
</motion.div>
{enhanced && (
<motion.div
className="text-xs text-heat-100 mt-1"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.8 }}
>
AI Enhanced
</motion.div>
)}
</div>
</div>
);
}