done the ai analysis in frntend

This commit is contained in:
Pradeep 2025-10-24 13:05:48 +05:30
parent 0b4141675e
commit 1c23876181
9 changed files with 769 additions and 46 deletions

View File

@ -19,6 +19,19 @@ const nextConfig: NextConfig = {
return config; return config;
}, },
async rewrites() {
return [
{
source: '/api/ai-analysis/analyze-repository',
destination: 'http://localhost:8000/api/ai-analysis/analyze-repository',
},
{
source: '/api/ai-analysis/health',
destination: 'http://localhost:8000/api/ai-analysis/health',
},
// Exclude reports endpoint from rewrites to use local API route
];
},
}; };
export default nextConfig; export default nextConfig;

View File

@ -0,0 +1,38 @@
import { NextRequest, NextResponse } from 'next/server'
export const runtime = 'nodejs'
export async function POST(req: NextRequest) {
try {
const body = await req.json()
// Get the backend URL from environment variables (API Gateway)
const backendUrl = process.env.BACKEND_URL || 'http://localhost:8000'
// Forward the request to the backend AI Analysis service
const response = await fetch(`${backendUrl}/api/ai-analysis/analyze-repository`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(body),
})
const data = await response.json()
if (!response.ok) {
return NextResponse.json(
{ success: false, message: data.message || 'Backend analysis failed' },
{ status: response.status }
)
}
return NextResponse.json(data)
} catch (error: any) {
console.error('AI Analysis proxy error:', error)
return NextResponse.json(
{ success: false, message: error?.message || 'AI analysis proxy failed' },
{ status: 500 }
)
}
}

View File

@ -0,0 +1,73 @@
import { NextRequest, NextResponse } from 'next/server'
export const runtime = 'nodejs'
export async function OPTIONS() {
return new NextResponse(null, {
status: 200,
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, HEAD, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Content-Disposition',
},
})
}
export async function GET(
req: NextRequest,
{ params }: { params: Promise<{ filename: string }> }
) {
try {
const { filename } = await params
// Get the backend URL from environment variables (API Gateway)
const backendUrl = process.env.BACKEND_URL || 'http://localhost:8000'
// Check if this is a download request (has download=true query param)
const url = new URL(req.url)
const isDownload = url.searchParams.get('download') === 'true'
// Forward the request to the backend AI Analysis service
const response = await fetch(`${backendUrl}/api/ai-analysis/reports/${filename}`, {
method: 'GET',
})
if (!response.ok) {
return NextResponse.json(
{ success: false, message: 'Report not found' },
{ status: 404 }
)
}
// Get the file content
const fileBuffer = await response.arrayBuffer()
// Set headers based on whether it's a download or inline view
const headers = new Headers()
headers.set('Content-Type', 'application/pdf')
headers.set('Cache-Control', 'no-cache')
headers.set('X-Content-Type-Options', 'nosniff')
headers.set('Accept-Ranges', 'bytes')
headers.set('Access-Control-Allow-Origin', '*')
headers.set('Access-Control-Allow-Methods', 'GET, HEAD, OPTIONS')
headers.set('Access-Control-Allow-Headers', 'Content-Type, Content-Disposition')
if (isDownload) {
headers.set('Content-Disposition', `attachment; filename="${filename}"`)
} else {
headers.set('Content-Disposition', `inline; filename="${filename}"`)
}
// Return the file with proper headers
return new NextResponse(fileBuffer, {
status: 200,
headers,
})
} catch (error: any) {
console.error('Report download proxy error:', error)
return NextResponse.json(
{ success: false, message: error?.message || 'Report download failed' },
{ status: 500 }
)
}
}

View File

@ -20,11 +20,14 @@ import {
GitCompare, GitCompare,
Brain, Brain,
GitCommit, GitCommit,
Server Server,
FileText
} from 'lucide-react'; } from 'lucide-react';
import { getUserRepositories, type GitHubRepoSummary } from '@/lib/api/github'; import { getUserRepositories, type GitHubRepoSummary } from '@/lib/api/github';
import { authApiClient } from '@/components/apis/authApiClients'; import { authApiClient } from '@/components/apis/authApiClients';
import Link from 'next/link'; import Link from 'next/link';
import RepositoryAnalysis from '@/components/repository-analysis';
import { useAuth } from '@/contexts/auth-context';
const GitHubReposPage: React.FC = () => { const GitHubReposPage: React.FC = () => {
const [repositories, setRepositories] = useState<GitHubRepoSummary[]>([]); const [repositories, setRepositories] = useState<GitHubRepoSummary[]>([]);
@ -34,6 +37,9 @@ const GitHubReposPage: React.FC = () => {
const [providerFilter, setProviderFilter] = useState<'all' | 'github' | 'gitlab' | 'bitbucket' | 'gitea'>('all'); const [providerFilter, setProviderFilter] = useState<'all' | 'github' | 'gitlab' | 'bitbucket' | 'gitea'>('all');
const [aiAnalysisLoading, setAiAnalysisLoading] = useState<string | null>(null); const [aiAnalysisLoading, setAiAnalysisLoading] = useState<string | null>(null);
const [aiAnalysisError, setAiAnalysisError] = useState<string | null>(null); const [aiAnalysisError, setAiAnalysisError] = useState<string | null>(null);
const [selectedRepoId, setSelectedRepoId] = useState<string | null>(null);
const [analysisResults, setAnalysisResults] = useState<{[key: string]: any}>({});
const { user } = useAuth();
// Load repositories // Load repositories
useEffect(() => { useEffect(() => {
@ -79,34 +85,137 @@ const GitHubReposPage: React.FC = () => {
setAiAnalysisLoading(repositoryId); setAiAnalysisLoading(repositoryId);
setAiAnalysisError(null); setAiAnalysisError(null);
const response = await authApiClient.get(`/api/ai/repository/${repositoryId}/ai-stream`); // Get user ID from auth context
const data = response.data; const userId = user?.id;
if (!userId) {
console.log('AI Analysis Result:', data); throw new Error('User not authenticated');
// Log user ID from response
if (data.user_id) {
console.log('🔍 [USER-ID] Received user ID in response:', data.user_id);
} }
// You can add a success notification or modal here console.log('🚀 Starting AI Analysis for repository:', repositoryId);
alert('AI Analysis completed successfully!'); console.log('🔍 User ID:', userId);
// Call the new AI Analysis Service endpoint
const response = await authApiClient.post('/api/ai-analysis/analyze-repository', {
repository_id: repositoryId,
user_id: userId,
output_format: 'pdf',
max_files: 0, // 0 = unlimited files
analysis_type: 'full' // Use full AI analysis for detailed results
});
const data = response.data;
console.log('✅ AI Analysis Result:', data);
if (data.success) {
// Store analysis results in state
setAnalysisResults(prev => ({
...prev,
[repositoryId]: {
analysis_id: data.analysis_id,
report_path: data.report_path,
stats: data.stats,
timestamp: new Date().toISOString()
}
}));
// Show success message
alert(`AI Analysis completed successfully!\n\nAnalysis ID: ${data.analysis_id}\nTotal Files: ${data.stats?.total_files || 'N/A'}\nCode Quality: ${data.stats?.code_quality_score || 'N/A'}/10`);
} else {
throw new Error(data.message || 'Analysis failed');
}
} catch (err: any) {
console.error('❌ AI Analysis Error:', err);
let errorMessage = 'AI Analysis failed';
// Handle specific error types
if (err.code === 'ECONNABORTED' || err.message?.includes('timeout')) {
errorMessage = 'AI Analysis is taking longer than expected. Please wait a moment and try again.';
} else if (err.response?.status === 400) {
errorMessage = err.response.data?.message || 'Invalid request. Please check your repository.';
} else if (err.response?.status === 500) {
errorMessage = 'Server error. Please try again later.';
} else if (err.response?.status === 401) {
errorMessage = 'Authentication required. Please sign in again.';
} else if (err.response?.data?.message) {
errorMessage = err.response.data.message;
} else if (err.message) {
errorMessage = err.message;
}
} catch (err) {
const errorMessage = err instanceof Error ? err.message : 'AI Analysis failed';
setAiAnalysisError(errorMessage); setAiAnalysisError(errorMessage);
console.error('AI Analysis Error:', err); alert(`AI Analysis failed: ${errorMessage}`);
} finally { } finally {
setAiAnalysisLoading(null); setAiAnalysisLoading(null);
} }
}; };
const downloadAnalysisReport = async (repositoryId: string) => {
const analysis = analysisResults[repositoryId];
if (!analysis?.report_path) {
alert('No analysis report available for this repository');
return;
}
try {
// Extract filename from report_path
const filename = analysis.report_path.split('/').pop() || 'analysis_report.pdf';
// Show loading state
const downloadButton = document.querySelector(`[data-repo-id="${repositoryId}"] .download-button`);
if (downloadButton) {
downloadButton.textContent = 'Downloading...';
(downloadButton as HTMLButtonElement).disabled = true;
}
// Download the PDF via frontend API route with download parameter
const response = await fetch(`/api/ai-analysis/reports/${filename}?download=true`);
if (!response.ok) {
if (response.status === 404) {
throw new Error('Analysis report not found. Please run the analysis again.');
} else if (response.status === 500) {
throw new Error('Server error. Please try again later.');
} else {
throw new Error(`Failed to download report (${response.status})`);
}
}
const blob = await response.blob();
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
window.URL.revokeObjectURL(url);
document.body.removeChild(a);
// Reset button state
if (downloadButton) {
downloadButton.textContent = 'Download PDF Report';
(downloadButton as HTMLButtonElement).disabled = false;
}
} catch (error: any) {
console.error('Download failed:', error);
alert(`Failed to download analysis report: ${error.message}`);
// Reset button state
const downloadButton = document.querySelector(`[data-repo-id="${repositoryId}"] .download-button`);
if (downloadButton) {
downloadButton.textContent = 'Download PDF Report';
(downloadButton as HTMLButtonElement).disabled = false;
}
}
};
const filteredRepositories = repositories.filter(repo => { const filteredRepositories = repositories.filter(repo => {
// Use correct field names from the API response // Use correct field names from the API response
const repoName = repo.repository_name || repo.name || ''; const repoName = repo.repository_name || repo.name || '';
const fullName = repo.metadata?.full_name || repo.full_name || ''; const fullName = repo.full_name || '';
const description = repo.metadata?.description || repo.description || ''; const description = repo.description || '';
const language = repo.metadata?.language || repo.language || ''; const language = repo.language || '';
const matchesSearch = fullName.toLowerCase().includes(searchQuery.toLowerCase()) || const matchesSearch = fullName.toLowerCase().includes(searchQuery.toLowerCase()) ||
repoName.toLowerCase().includes(searchQuery.toLowerCase()) || repoName.toLowerCase().includes(searchQuery.toLowerCase()) ||
@ -293,12 +402,12 @@ const GitHubReposPage: React.FC = () => {
{repo.repository_name || repo.name || 'Unknown Repository'} {repo.repository_name || repo.name || 'Unknown Repository'}
</CardTitle> </CardTitle>
<p className="text-sm text-muted-foreground"> <p className="text-sm text-muted-foreground">
{repo.metadata?.full_name || repo.full_name || 'Unknown Owner'} {repo.full_name || 'Unknown Owner'}
</p> </p>
</div> </div>
</div> </div>
<Badge variant={repo.metadata?.visibility === 'public' || repo.visibility === 'public' ? 'default' : 'secondary'}> <Badge variant={repo.visibility === 'public' ? 'default' : 'secondary'}>
{repo.metadata?.visibility || repo.visibility || 'unknown'} {repo.visibility || 'unknown'}
</Badge> </Badge>
</div> </div>
</CardHeader> </CardHeader>
@ -352,6 +461,17 @@ const GitHubReposPage: React.FC = () => {
{/* AI Analysis Button */} {/* AI Analysis Button */}
<div className="pt-2"> <div className="pt-2">
{/* Debug: Log repository data */}
{(() => {
console.log('🔍 Repository check:', {
name: repo.repository_name,
storage_status: repo.storage_status,
total_files_count: repo.total_files_count,
condition: repo.storage_status === 'completed' && (repo.total_files_count || 0) > 0
});
return null;
})()}
{repo.storage_status === 'completed' && (repo.total_files_count || 0) > 0 ? (
<Button <Button
size="sm" size="sm"
variant="secondary" variant="secondary"
@ -375,7 +495,100 @@ const GitHubReposPage: React.FC = () => {
</> </>
)} )}
</Button> </Button>
) : (
<Button
size="sm"
variant="outline"
className="w-full opacity-50 cursor-not-allowed"
disabled
title={
repo.storage_status !== 'completed'
? 'Repository not fully synced'
: 'Repository is empty or has no files'
}
>
<Brain className="h-4 w-4 mr-2" />
AI Analysis Unavailable
</Button>
)}
</div> </div>
{/* Analysis Results Display */}
{repo.id && analysisResults[repo.id] && (
<div className="mt-4 p-3 bg-gray-800 border border-gray-700 rounded-lg">
<div className="flex items-center justify-between mb-2">
<h4 className="text-sm font-medium text-blue-400">Analysis Complete</h4>
<span className="text-xs text-blue-300">
{new Date(analysisResults[repo.id!].timestamp).toLocaleString()}
</span>
</div>
{/* Analysis Statistics */}
<div className="grid grid-cols-2 gap-3 mb-3 text-xs">
<div className="flex justify-between items-center">
<span className="text-gray-400">Files:</span>
<span className="font-medium text-white">{analysisResults[repo.id!].stats?.total_files || 0}</span>
</div>
<div className="flex justify-between items-center">
<span className="text-gray-400">Lines:</span>
<span className="font-medium text-white">{analysisResults[repo.id!].stats?.total_lines || 0}</span>
</div>
<div className="flex justify-between items-center">
<span className="text-gray-400">Quality:</span>
<span className="font-medium text-white">{Math.round((analysisResults[repo.id!].stats?.code_quality_score || 0) * 100) / 100}/10</span>
</div>
<div className="flex justify-between items-center">
<span className="text-gray-400">Issues:</span>
<span className="font-medium text-white">{analysisResults[repo.id!].stats?.total_issues || 0}</span>
</div>
</div>
{/* Languages */}
{analysisResults[repo.id!].stats?.languages && (
<div className="mb-3">
<span className="text-xs text-gray-400">Languages: </span>
<span className="text-xs font-medium text-white">
{typeof analysisResults[repo.id!].stats.languages === 'object'
? Object.entries(analysisResults[repo.id!].stats.languages)
.map(([lang, count]) => `${lang} (${count})`)
.join(', ')
: Array.isArray(analysisResults[repo.id!].stats.languages)
? analysisResults[repo.id!].stats.languages.join(', ')
: String(analysisResults[repo.id!].stats.languages)
}
</span>
</div>
)}
{/* PDF Actions */}
<div className="flex gap-2">
<Button
size="sm"
variant="outline"
className="flex-1 text-blue-400 border-blue-500 hover:bg-blue-500 hover:text-white download-button"
data-repo-id={repo.id}
onClick={() => downloadAnalysisReport(String(repo.id))}
>
<FileText className="h-4 w-4 mr-2" />
Download PDF
</Button>
<Button
size="sm"
variant="outline"
className="flex-1 text-green-400 border-green-500 hover:bg-green-500 hover:text-white"
onClick={() => {
const filename = analysisResults[repo.id!].report_path?.split('/').pop();
if (filename) {
window.open(`/api/ai-analysis/reports/${filename}`, '_blank');
}
}}
>
<Eye className="h-4 w-4 mr-2" />
View PDF
</Button>
</div>
</div>
)}
</CardContent> </CardContent>
</Card> </Card>
))} ))}
@ -401,6 +614,14 @@ const GitHubReposPage: React.FC = () => {
</CardContent> </CardContent>
</Card> </Card>
)} )}
{/* Repository Analysis */}
{selectedRepoId && user?.id && (
<div className="mt-8 border-t pt-4">
<h3>Analyzing Repository {selectedRepoId}</h3>
<RepositoryAnalysis repositoryId={selectedRepoId} userId={user.id} />
</div>
)}
</div> </div>
); );
}; };

View File

@ -35,6 +35,7 @@ interface VcsRepoSummary {
language?: string; language?: string;
visibility: 'public' | 'private'; visibility: 'public' | 'private';
provider: 'github' | 'gitlab' | 'bitbucket' | 'gitea'; provider: 'github' | 'gitlab' | 'bitbucket' | 'gitea';
provider_name?: 'github' | 'gitlab' | 'bitbucket' | 'gitea'; // Backend field
html_url: string; html_url: string;
clone_url: string; clone_url: string;
default_branch: string; default_branch: string;
@ -182,10 +183,10 @@ const VcsReposPage: React.FC = () => {
}); });
const repos = response.data?.data || []; const repos = response.data?.data || [];
// Add provider info to each repo // Use the provider_name from backend, fallback to route provider if not available
const reposWithProvider = repos.map((repo: any) => ({ const reposWithProvider = repos.map((repo: any) => ({
...repo, ...repo,
provider: provider provider: repo.provider_name || provider // Use backend provider_name, fallback to route provider
})); }));
allRepos.push(...reposWithProvider); allRepos.push(...reposWithProvider);

View File

@ -87,7 +87,7 @@ export const logout = async () => {
export const authApiClient = axios.create({ export const authApiClient = axios.create({
baseURL: BACKEND_URL, baseURL: BACKEND_URL,
withCredentials: true, withCredentials: true,
timeout: 10000, // 10 second timeout timeout: 600000, // 10 minute timeout for AI analysis
}); });
// Add auth token to requests // Add auth token to requests

View File

@ -0,0 +1,300 @@
"use client"
import React, { useState, useEffect } from 'react';
import { Brain, Download, FileText, CheckCircle, AlertCircle, Clock, BarChart3 } from 'lucide-react';
interface AnalysisResponse {
success: boolean;
message: string;
analysis_id?: string;
report_path?: string;
stats?: {
repository_id: string;
total_files: number;
total_lines: number;
languages: string[];
code_quality_score: number;
high_quality_files: number;
medium_quality_files: number;
low_quality_files: number;
total_issues: number;
};
}
class AIAnalysisService {
private baseUrl = '/api/ai-analysis'; // Via gateway
async analyzeRepository(request: {
repository_id: string;
user_id: string;
output_format: string;
max_files: number;
analysis_type: string;
}): Promise<AnalysisResponse> {
const response = await fetch(`${this.baseUrl}/analyze-repository`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(request)
});
if (!response.ok) {
throw new Error(`Analysis failed: ${response.statusText}`);
}
return response.json();
}
async downloadReport(filename: string): Promise<Blob> {
const response = await fetch(`${this.baseUrl}/reports/${filename}`);
if (!response.ok) {
throw new Error(`Failed to download report: ${response.statusText}`);
}
return response.blob();
}
}
const RepositoryAnalysis: React.FC<{ repositoryId: string; userId: string }> = ({
repositoryId,
userId
}) => {
const [isAnalyzing, setIsAnalyzing] = useState(false);
const [analysisResult, setAnalysisResult] = useState<AnalysisResponse | null>(null);
const [error, setError] = useState<string | null>(null);
const [progress, setProgress] = useState(0);
const [showPdfPreview, setShowPdfPreview] = useState(false);
const aiAnalysisService = new AIAnalysisService();
const handleAnalyze = async () => {
setIsAnalyzing(true);
setError(null);
setProgress(0);
// Simulate progress updates
const progressInterval = setInterval(() => {
setProgress(prev => {
if (prev >= 90) return prev;
return prev + Math.random() * 10;
});
}, 1000);
try {
const result = await aiAnalysisService.analyzeRepository({
repository_id: repositoryId,
user_id: userId,
output_format: 'pdf',
max_files: 0, // 0 = unlimited files
analysis_type: 'full' // Use full AI analysis for detailed results
});
setProgress(100);
setAnalysisResult(result);
} catch (err) {
setError(err instanceof Error ? err.message : 'Analysis failed');
} finally {
clearInterval(progressInterval);
setIsAnalyzing(false);
}
};
const handleDownloadReport = async () => {
if (!analysisResult?.report_path) return;
try {
const filename = analysisResult.report_path.split('/').pop();
const blob = await aiAnalysisService.downloadReport(filename!);
// Create download link
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename!;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
window.URL.revokeObjectURL(url);
} catch (err) {
setError('Failed to download report');
}
};
return (
<div className="repository-analysis bg-white rounded-lg shadow-lg p-6">
<div className="flex items-center gap-3 mb-6">
<Brain className="h-8 w-8 text-blue-600" />
<h2 className="text-2xl font-bold text-gray-800">AI Repository Analysis</h2>
</div>
{/* Analysis Button */}
<div className="mb-6">
<button
onClick={handleAnalyze}
disabled={isAnalyzing}
className={`flex items-center gap-2 px-6 py-3 rounded-lg font-semibold transition-all duration-200 ${
isAnalyzing
? 'bg-blue-400 cursor-not-allowed'
: 'bg-blue-600 hover:bg-blue-700 hover:shadow-lg'
} text-white`}
>
{isAnalyzing ? (
<>
<Clock className="h-5 w-5 animate-spin" />
Analyzing Repository...
</>
) : (
<>
<Brain className="h-5 w-5" />
Start AI Analysis
</>
)}
</button>
</div>
{/* Progress Bar */}
{isAnalyzing && (
<div className="mb-6">
<div className="flex justify-between text-sm text-gray-600 mb-2">
<span>Analysis Progress</span>
<span>{Math.round(progress)}%</span>
</div>
<div className="w-full bg-gray-200 rounded-full h-2">
<div
className="bg-blue-600 h-2 rounded-full transition-all duration-300"
style={{ width: `${progress}%` }}
/>
</div>
<p className="text-sm text-gray-500 mt-2">
Analyzing files and generating comprehensive report...
</p>
</div>
)}
{/* Error Message */}
{error && (
<div className="mb-6 p-4 bg-red-900/20 border border-red-500/30 rounded-lg flex items-center gap-3">
<AlertCircle className="h-5 w-5 text-red-400" />
<span className="text-red-300">{error}</span>
</div>
)}
{/* Analysis Results */}
{analysisResult && (
<div className="space-y-6">
{/* Success Message */}
<div className="p-4 bg-green-900/20 border border-green-500/30 rounded-lg flex items-center gap-3">
<CheckCircle className="h-5 w-5 text-green-400" />
<span className="text-green-300 font-semibold">Analysis completed successfully!</span>
</div>
{/* Stats Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
<div className="bg-blue-900/20 border border-blue-500/30 p-4 rounded-lg">
<div className="flex items-center gap-2 mb-2">
<FileText className="h-5 w-5 text-blue-400" />
<span className="font-semibold text-blue-300">Total Files</span>
</div>
<p className="text-2xl font-bold text-blue-200">{analysisResult.stats?.total_files}</p>
</div>
<div className="bg-green-900/20 border border-green-500/30 p-4 rounded-lg">
<div className="flex items-center gap-2 mb-2">
<BarChart3 className="h-5 w-5 text-green-400" />
<span className="font-semibold text-green-300">Total Lines</span>
</div>
<p className="text-2xl font-bold text-green-200">{analysisResult.stats?.total_lines?.toLocaleString()}</p>
</div>
<div className="bg-purple-900/20 border border-purple-500/30 p-4 rounded-lg">
<div className="flex items-center gap-2 mb-2">
<Brain className="h-5 w-5 text-purple-400" />
<span className="font-semibold text-purple-300">Quality Score</span>
</div>
<p className="text-2xl font-bold text-purple-200">{Math.round((analysisResult.stats?.code_quality_score || 0) * 100) / 100}/10</p>
</div>
<div className="bg-orange-900/20 border border-orange-500/30 p-4 rounded-lg">
<div className="flex items-center gap-2 mb-2">
<AlertCircle className="h-5 w-5 text-orange-400" />
<span className="font-semibold text-orange-300">Total Issues</span>
</div>
<p className="text-2xl font-bold text-orange-200">{analysisResult.stats?.total_issues}</p>
</div>
</div>
{/* Quality Breakdown */}
<div className="bg-gray-800 border border-gray-700 p-6 rounded-lg">
<h3 className="text-lg font-semibold text-white mb-4">Quality Breakdown</h3>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div className="text-center p-4 bg-green-900/20 border border-green-500/30 rounded-lg">
<div className="text-2xl font-bold text-green-300">{analysisResult.stats?.high_quality_files}</div>
<div className="text-sm text-green-400">High Quality Files</div>
</div>
<div className="text-center p-4 bg-yellow-900/20 border border-yellow-500/30 rounded-lg">
<div className="text-2xl font-bold text-yellow-300">{analysisResult.stats?.medium_quality_files}</div>
<div className="text-sm text-yellow-400">Medium Quality Files</div>
</div>
<div className="text-center p-4 bg-red-900/20 border border-red-500/30 rounded-lg">
<div className="text-2xl font-bold text-red-300">{analysisResult.stats?.low_quality_files}</div>
<div className="text-sm text-red-400">Low Quality Files</div>
</div>
</div>
</div>
{/* Languages */}
{analysisResult.stats?.languages && Object.keys(analysisResult.stats.languages).length > 0 && (
<div className="bg-gray-800 border border-gray-700 p-6 rounded-lg">
<h3 className="text-lg font-semibold text-white mb-4">Languages Detected</h3>
<div className="flex flex-wrap gap-2">
{Object.entries(analysisResult.stats.languages).map(([lang, count]) => (
<span key={lang} className="px-3 py-1 bg-blue-900/30 border border-blue-500/30 text-blue-300 rounded-full text-sm">
{lang} ({count})
</span>
))}
</div>
</div>
)}
{/* Report Actions */}
<div className="flex flex-col sm:flex-row gap-4">
<button
onClick={handleDownloadReport}
className="flex items-center gap-2 px-6 py-3 bg-green-600 hover:bg-green-700 text-white rounded-lg font-semibold transition-colors"
>
<Download className="h-5 w-5" />
Download PDF Report
</button>
<button
onClick={() => setShowPdfPreview(!showPdfPreview)}
className="flex items-center gap-2 px-6 py-3 bg-gray-600 hover:bg-gray-700 text-white rounded-lg font-semibold transition-colors"
>
<FileText className="h-5 w-5" />
{showPdfPreview ? 'Hide' : 'Preview'} Report
</button>
</div>
{/* PDF Preview */}
{showPdfPreview && analysisResult.report_path && (
<div className="mt-6">
<h3 className="text-lg font-semibold text-gray-800 mb-4">Report Preview</h3>
<div className="border rounded-lg p-4 bg-gray-50">
<iframe
src={`/api/ai-analysis/reports/${analysisResult.report_path.split('/').pop()}`}
width="100%"
height="600"
className="border-0 rounded"
title="PDF Report Preview"
/>
</div>
</div>
)}
</div>
)}
</div>
);
};
export default RepositoryAnalysis;

View File

@ -0,0 +1,71 @@
import { useState } from 'react';
interface AnalysisProgress {
status: 'analyzing' | 'generating_report' | 'complete';
files_processed: number;
total_files: number;
percentage: number;
}
interface AnalysisResult {
success: boolean;
analysis_id: string;
report_path: string;
stats: {
total_files: number;
total_lines: number;
code_quality_score: number;
// other stats
};
}
export const useAIAnalysis = () => {
const [isAnalyzing, setIsAnalyzing] = useState(false);
const [progress, setProgress] = useState<AnalysisProgress | null>(null);
const [result, setResult] = useState<AnalysisResult | null>(null);
const [error, setError] = useState<string | null>(null);
const startAnalysis = async (
repositoryId: string,
userId: string,
options: { output_format?: string; max_files?: number } = {}
) => {
try {
setIsAnalyzing(true);
// Start analysis via API Gateway
const response = await fetch(
'/api/ai-analysis/analyze-repository', // Assuming relative to frontend
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
repository_id: repositoryId,
user_id: userId,
output_format: options.output_format || 'pdf',
max_files: options.max_files || 0, // 0 = unlimited files
analysis_type: 'full' // Use full AI analysis for detailed results
})
}
);
if (!response.ok) {
throw new Error('Analysis failed to start');
}
const data = await response.json();
if (data.success) {
setResult(data);
}
} catch (err: unknown) {
setError(err instanceof Error ? err.message : 'Unknown error');
} finally {
setIsAnalyzing(false);
}
};
return { isAnalyzing, progress, result, error, startAnalysis };
};

View File

@ -242,6 +242,9 @@ export interface GitHubRepoSummary {
updated_at?: string updated_at?: string
html_url?: string html_url?: string
provider_name?: string provider_name?: string
storage_status?: string
total_files_count?: number
repository_name?: string
} }
// Tries backend gateway route first. If backend does not yet provide it, returns an empty list gracefully. // Tries backend gateway route first. If backend does not yet provide it, returns an empty list gracefully.
@ -352,7 +355,10 @@ export async function getUserRepositories(clearCache = false): Promise<GitHubRep
language: md?.language || null, language: md?.language || null,
updated_at: md?.updated_at || r?.updated_at, updated_at: md?.updated_at || r?.updated_at,
html_url: md?.html_url || (full ? `https://github.com/${full}` : undefined), html_url: md?.html_url || (full ? `https://github.com/${full}` : undefined),
provider_name: r?.provider_name, // Add the provider_name field! provider_name: r?.provider_name,
storage_status: r?.storage_status, // Include storage_status from backend
total_files_count: r?.total_files_count, // Include total_files_count from backend
repository_name: r?.repository_name, // Include repository_name for display
} as GitHubRepoSummary } as GitHubRepoSummary
}) })
try { if (typeof window !== 'undefined') sessionStorage.setItem(`user_repos_cache_${userId || 'anon'}`, JSON.stringify(normalized)) } catch {} try { if (typeof window !== 'undefined') sessionStorage.setItem(`user_repos_cache_${userId || 'anon'}`, JSON.stringify(normalized)) } catch {}