330 lines
13 KiB
TypeScript
330 lines
13 KiB
TypeScript
"use client"
|
|
|
|
import { useEffect, useState, Suspense } from "react"
|
|
import { useRouter, useSearchParams } from "next/navigation"
|
|
import Link from "next/link"
|
|
import { Button } from "@/components/ui/button"
|
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
|
import { Badge } from "@/components/ui/badge"
|
|
import { Progress } from "@/components/ui/progress"
|
|
import {
|
|
ArrowLeft,
|
|
Brain,
|
|
Code,
|
|
FileText,
|
|
GitBranch,
|
|
Star,
|
|
Shield,
|
|
TrendingUp,
|
|
AlertTriangle,
|
|
CheckCircle,
|
|
Clock,
|
|
Zap,
|
|
Target,
|
|
BarChart3,
|
|
Layers,
|
|
Cpu,
|
|
Search,
|
|
File,
|
|
Folder,
|
|
Eye,
|
|
AlertCircle
|
|
} from "lucide-react"
|
|
|
|
interface FileAnalysis {
|
|
id: string
|
|
name: string
|
|
path: string
|
|
type: 'file' | 'folder'
|
|
status: 'scanning' | 'analyzing' | 'completed' | 'pending'
|
|
size?: string
|
|
language?: string
|
|
issues?: number
|
|
score?: number
|
|
details?: string
|
|
}
|
|
|
|
// Component that uses useSearchParams - needs to be wrapped in Suspense
|
|
function AIAnalysisContent() {
|
|
const router = useRouter()
|
|
const searchParams = useSearchParams()
|
|
const repoId = searchParams.get('repoId')
|
|
const repoName = searchParams.get('repoName') || 'Repository'
|
|
|
|
const [analysisProgress, setAnalysisProgress] = useState(0)
|
|
const [isAnalyzing, setIsAnalyzing] = useState(true)
|
|
const [currentFile, setCurrentFile] = useState('Initializing analysis...')
|
|
|
|
const [files, setFiles] = useState<FileAnalysis[]>([
|
|
{ id: '1', name: 'package.json', path: '/package.json', type: 'file', status: 'pending', language: 'JSON', size: '2.1 KB' },
|
|
{ id: '2', name: 'src', path: '/src', type: 'folder', status: 'pending' },
|
|
{ id: '3', name: 'App.js', path: '/src/App.js', type: 'file', status: 'pending', language: 'JavaScript', size: '5.2 KB' },
|
|
{ id: '4', name: 'components', path: '/src/components', type: 'folder', status: 'pending' },
|
|
{ id: '5', name: 'Header.jsx', path: '/src/components/Header.jsx', type: 'file', status: 'pending', language: 'JavaScript', size: '3.8 KB' },
|
|
{ id: '6', name: 'Footer.jsx', path: '/src/components/Footer.jsx', type: 'file', status: 'pending', language: 'JavaScript', size: '2.5 KB' },
|
|
{ id: '7', name: 'utils', path: '/src/utils', type: 'folder', status: 'pending' },
|
|
{ id: '8', name: 'helpers.js', path: '/src/utils/helpers.js', type: 'file', status: 'pending', language: 'JavaScript', size: '4.1 KB' },
|
|
{ id: '9', name: 'README.md', path: '/README.md', type: 'file', status: 'pending', language: 'Markdown', size: '1.8 KB' },
|
|
{ id: '10', name: 'styles', path: '/styles', type: 'folder', status: 'pending' },
|
|
{ id: '11', name: 'main.css', path: '/styles/main.css', type: 'file', status: 'pending', language: 'CSS', size: '6.3 KB' },
|
|
{ id: '12', name: 'tests', path: '/tests', type: 'folder', status: 'pending' },
|
|
{ id: '13', name: 'App.test.js', path: '/tests/App.test.js', type: 'file', status: 'pending', language: 'JavaScript', size: '2.9 KB' }
|
|
])
|
|
|
|
useEffect(() => {
|
|
if (!repoId) {
|
|
router.push('/github/repos')
|
|
return
|
|
}
|
|
|
|
let fileIndex = 0
|
|
let progress = 0
|
|
|
|
const interval = setInterval(() => {
|
|
if (fileIndex < files.length) {
|
|
const currentFileData = files[fileIndex]
|
|
|
|
// Update current file being analyzed
|
|
setCurrentFile(`Analyzing ${currentFileData.name}...`)
|
|
|
|
// Update file status to scanning
|
|
setFiles(prev => prev.map((file, index) =>
|
|
index === fileIndex ? { ...file, status: 'scanning' as const } : file
|
|
))
|
|
|
|
// After a short delay, mark as analyzing
|
|
setTimeout(() => {
|
|
setFiles(prev => prev.map((file, index) =>
|
|
index === fileIndex ? { ...file, status: 'analyzing' as const } : file
|
|
))
|
|
}, 500)
|
|
|
|
// After another delay, mark as completed with mock data
|
|
setTimeout(() => {
|
|
setFiles(prev => prev.map((file, index) =>
|
|
index === fileIndex ? {
|
|
...file,
|
|
status: 'completed' as const,
|
|
score: Math.floor(Math.random() * 30) + 70, // 70-100
|
|
issues: Math.floor(Math.random() * 5),
|
|
details: file.type === 'file' ? 'Analysis completed successfully' : 'Directory scanned'
|
|
} : file
|
|
))
|
|
}, 1000)
|
|
|
|
progress = Math.min(100, ((fileIndex + 1) / files.length) * 100)
|
|
setAnalysisProgress(progress)
|
|
fileIndex++
|
|
} else {
|
|
// Complete analysis
|
|
setIsAnalyzing(false)
|
|
setCurrentFile('Analysis completed!')
|
|
setAnalysisProgress(100)
|
|
clearInterval(interval)
|
|
}
|
|
}, 1500)
|
|
|
|
return () => clearInterval(interval)
|
|
}, [repoId, router, files.length])
|
|
|
|
const getStatusIcon = (status: FileAnalysis['status'], type: FileAnalysis['type']) => {
|
|
switch (status) {
|
|
case 'completed':
|
|
return <CheckCircle className="h-4 w-4 text-green-400" />
|
|
case 'analyzing':
|
|
return <div className="h-4 w-4 border-2 border-blue-400 border-t-transparent rounded-full animate-spin" />
|
|
case 'scanning':
|
|
return <Search className="h-4 w-4 text-yellow-400 animate-pulse" />
|
|
case 'pending':
|
|
return <Clock className="h-4 w-4 text-white/40" />
|
|
}
|
|
}
|
|
|
|
const getFileIcon = (type: FileAnalysis['type'], language?: string) => {
|
|
if (type === 'folder') {
|
|
return <Folder className="h-4 w-4 text-blue-400" />
|
|
}
|
|
|
|
switch (language) {
|
|
case 'JavaScript':
|
|
return <Code className="h-4 w-4 text-yellow-400" />
|
|
case 'CSS':
|
|
return <FileText className="h-4 w-4 text-blue-400" />
|
|
case 'Markdown':
|
|
return <FileText className="h-4 w-4 text-gray-400" />
|
|
case 'JSON':
|
|
return <FileText className="h-4 w-4 text-orange-400" />
|
|
default:
|
|
return <File className="h-4 w-4 text-white/60" />
|
|
}
|
|
}
|
|
|
|
const getScoreColor = (score: number) => {
|
|
if (score >= 90) return 'text-green-400'
|
|
if (score >= 80) return 'text-yellow-400'
|
|
if (score >= 70) return 'text-orange-400'
|
|
return 'text-red-400'
|
|
}
|
|
|
|
return (
|
|
<div className="mx-auto max-w-7xl px-4 py-8 space-y-8">
|
|
{/* Header */}
|
|
<div className="flex items-center gap-4">
|
|
<Link href="/github/repos">
|
|
<Button variant="ghost" className="text-white/80 hover:text-white">
|
|
<ArrowLeft className="mr-2 h-4 w-4" /> Back to Repositories
|
|
</Button>
|
|
</Link>
|
|
<div>
|
|
<h1 className="text-3xl md:text-4xl font-bold text-white flex items-center gap-3">
|
|
<Brain className="h-8 w-8 text-orange-400" />
|
|
AI Code Analysis
|
|
</h1>
|
|
<p className="text-white/60 mt-1">Analyzing: <span className="text-orange-400 font-medium">{repoName}</span></p>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Analysis Progress */}
|
|
{isAnalyzing && (
|
|
<Card className="bg-white/5 border-white/10">
|
|
<CardContent className="p-6">
|
|
<div className="space-y-4">
|
|
<div className="flex items-center justify-between">
|
|
<h3 className="text-lg font-semibold text-white">Analysis Progress</h3>
|
|
<span className="text-sm text-white/60">{Math.round(analysisProgress)}%</span>
|
|
</div>
|
|
<Progress value={analysisProgress} className="h-2" />
|
|
<p className="text-white/80 text-sm flex items-center gap-2">
|
|
<Eye className="h-4 w-4 text-blue-400" />
|
|
{currentFile}
|
|
</p>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
)}
|
|
|
|
{/* File Analysis List */}
|
|
<Card className="bg-white/5 border-white/10">
|
|
<CardHeader>
|
|
<CardTitle className="text-white flex items-center gap-2">
|
|
<FileText className="h-6 w-6 text-orange-400" />
|
|
File Analysis Results
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent className="p-0">
|
|
<div className="divide-y divide-white/10">
|
|
{files.map((file) => (
|
|
<div key={file.id} className="p-4 hover:bg-white/5 transition-colors">
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex items-center gap-3 min-w-0 flex-1">
|
|
{getFileIcon(file.type, file.language)}
|
|
<div className="min-w-0 flex-1">
|
|
<div className="flex items-center gap-2">
|
|
<span className="font-medium text-white truncate">{file.name}</span>
|
|
{file.language && (
|
|
<Badge variant="outline" className="text-xs border-white/20 text-white/60">
|
|
{file.language}
|
|
</Badge>
|
|
)}
|
|
{file.size && (
|
|
<span className="text-xs text-white/40">{file.size}</span>
|
|
)}
|
|
</div>
|
|
<p className="text-sm text-white/60 truncate">{file.path}</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex items-center gap-3">
|
|
{file.status === 'completed' && file.score && (
|
|
<div className="flex items-center gap-2">
|
|
{file.issues && file.issues > 0 && (
|
|
<div className="flex items-center gap-1 text-red-400">
|
|
<AlertCircle className="h-4 w-4" />
|
|
<span className="text-sm">{file.issues}</span>
|
|
</div>
|
|
)}
|
|
<Badge className={`${getScoreColor(file.score)} bg-transparent border`}>
|
|
{file.score}/100
|
|
</Badge>
|
|
</div>
|
|
)}
|
|
{getStatusIcon(file.status, file.type)}
|
|
</div>
|
|
</div>
|
|
|
|
{file.status === 'completed' && file.details && (
|
|
<div className="mt-2 text-sm text-white/70">
|
|
{file.details}
|
|
</div>
|
|
)}
|
|
</div>
|
|
))}
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* Summary */}
|
|
{!isAnalyzing && (
|
|
<Card className="bg-white/5 border-white/10">
|
|
<CardHeader>
|
|
<CardTitle className="text-white flex items-center gap-2">
|
|
<BarChart3 className="h-6 w-6 text-orange-400" />
|
|
Analysis Summary
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="grid grid-cols-1 md:grid-cols-4 gap-6">
|
|
<div className="text-center">
|
|
<div className="text-2xl font-bold text-green-400">
|
|
{files.filter(f => f.status === 'completed').length}
|
|
</div>
|
|
<div className="text-white/60 text-sm">Files Analyzed</div>
|
|
</div>
|
|
<div className="text-center">
|
|
<div className="text-2xl font-bold text-yellow-400">
|
|
{files.filter(f => f.language).length}
|
|
</div>
|
|
<div className="text-white/60 text-sm">Languages Found</div>
|
|
</div>
|
|
<div className="text-center">
|
|
<div className="text-2xl font-bold text-red-400">
|
|
{files.reduce((sum, f) => sum + (f.issues || 0), 0)}
|
|
</div>
|
|
<div className="text-white/60 text-sm">Issues Found</div>
|
|
</div>
|
|
<div className="text-center">
|
|
<div className="text-2xl font-bold text-blue-400">
|
|
{Math.round(files.filter(f => f.score).reduce((sum, f) => sum + (f.score || 0), 0) / files.filter(f => f.score).length) || 0}
|
|
</div>
|
|
<div className="text-white/60 text-sm">Avg Score</div>
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
)}
|
|
|
|
{/* Action Buttons */}
|
|
{!isAnalyzing && (
|
|
<div className="flex justify-center gap-4">
|
|
<Button className="bg-orange-600 hover:bg-orange-700 text-white">
|
|
<Layers className="mr-2 h-4 w-4" />
|
|
Generate Detailed Report
|
|
</Button>
|
|
<Button variant="outline" className="border-white/20 text-white hover:bg-white/10">
|
|
<Cpu className="mr-2 h-4 w-4" />
|
|
Export Analysis
|
|
</Button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export default function AIAnalysisPage() {
|
|
return (
|
|
<Suspense fallback={<div className="mx-auto max-w-7xl px-4 py-8 flex items-center justify-center min-h-screen"><div className="text-white">Loading analysis...</div></div>}>
|
|
<AIAnalysisContent />
|
|
</Suspense>
|
|
)
|
|
}
|