From 1c23876181f0d02e876154300e042601e598d734 Mon Sep 17 00:00:00 2001 From: Pradeep Date: Fri, 24 Oct 2025 13:05:48 +0530 Subject: [PATCH] done the ai analysis in frntend --- next.config.ts | 13 + .../ai-analysis/analyze-repository/route.ts | 38 +++ .../ai-analysis/reports/[filename]/route.ts | 73 +++++ src/app/github/repos/page.tsx | 305 +++++++++++++++--- src/app/vcs/repos/page.tsx | 5 +- src/components/apis/authApiClients.tsx | 2 +- src/components/repository-analysis.tsx | 300 +++++++++++++++++ src/hooks/useAIAnalysis.ts | 71 ++++ src/lib/api/github.ts | 8 +- 9 files changed, 769 insertions(+), 46 deletions(-) create mode 100644 src/app/api/ai-analysis/analyze-repository/route.ts create mode 100644 src/app/api/ai-analysis/reports/[filename]/route.ts create mode 100644 src/components/repository-analysis.tsx create mode 100644 src/hooks/useAIAnalysis.ts diff --git a/next.config.ts b/next.config.ts index 8e1ffc7..867f1b2 100644 --- a/next.config.ts +++ b/next.config.ts @@ -19,6 +19,19 @@ const nextConfig: NextConfig = { 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; diff --git a/src/app/api/ai-analysis/analyze-repository/route.ts b/src/app/api/ai-analysis/analyze-repository/route.ts new file mode 100644 index 0000000..146cce8 --- /dev/null +++ b/src/app/api/ai-analysis/analyze-repository/route.ts @@ -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 } + ) + } +} diff --git a/src/app/api/ai-analysis/reports/[filename]/route.ts b/src/app/api/ai-analysis/reports/[filename]/route.ts new file mode 100644 index 0000000..a6dbd00 --- /dev/null +++ b/src/app/api/ai-analysis/reports/[filename]/route.ts @@ -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 } + ) + } +} diff --git a/src/app/github/repos/page.tsx b/src/app/github/repos/page.tsx index 86514be..8b77f03 100644 --- a/src/app/github/repos/page.tsx +++ b/src/app/github/repos/page.tsx @@ -20,11 +20,14 @@ import { GitCompare, Brain, GitCommit, - Server + Server, + FileText } from 'lucide-react'; import { getUserRepositories, type GitHubRepoSummary } from '@/lib/api/github'; import { authApiClient } from '@/components/apis/authApiClients'; import Link from 'next/link'; +import RepositoryAnalysis from '@/components/repository-analysis'; +import { useAuth } from '@/contexts/auth-context'; const GitHubReposPage: React.FC = () => { const [repositories, setRepositories] = useState([]); @@ -34,6 +37,9 @@ const GitHubReposPage: React.FC = () => { const [providerFilter, setProviderFilter] = useState<'all' | 'github' | 'gitlab' | 'bitbucket' | 'gitea'>('all'); const [aiAnalysisLoading, setAiAnalysisLoading] = useState(null); const [aiAnalysisError, setAiAnalysisError] = useState(null); + const [selectedRepoId, setSelectedRepoId] = useState(null); + const [analysisResults, setAnalysisResults] = useState<{[key: string]: any}>({}); + const { user } = useAuth(); // Load repositories useEffect(() => { @@ -79,34 +85,137 @@ const GitHubReposPage: React.FC = () => { setAiAnalysisLoading(repositoryId); setAiAnalysisError(null); - const response = await authApiClient.get(`/api/ai/repository/${repositoryId}/ai-stream`); - const data = response.data; - - console.log('AI Analysis Result:', data); - - // Log user ID from response - if (data.user_id) { - console.log('🔍 [USER-ID] Received user ID in response:', data.user_id); + // Get user ID from auth context + const userId = user?.id; + if (!userId) { + throw new Error('User not authenticated'); } - // You can add a success notification or modal here - alert('AI Analysis completed successfully!'); + console.log('🚀 Starting AI Analysis for repository:', repositoryId); + 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); - console.error('AI Analysis Error:', err); + alert(`AI Analysis failed: ${errorMessage}`); } finally { 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 => { // Use correct field names from the API response const repoName = repo.repository_name || repo.name || ''; - const fullName = repo.metadata?.full_name || repo.full_name || ''; - const description = repo.metadata?.description || repo.description || ''; - const language = repo.metadata?.language || repo.language || ''; + const fullName = repo.full_name || ''; + const description = repo.description || ''; + const language = repo.language || ''; const matchesSearch = fullName.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.metadata?.full_name || repo.full_name || 'Unknown Owner'} + {repo.full_name || 'Unknown Owner'}

- - {repo.metadata?.visibility || repo.visibility || 'unknown'} + + {repo.visibility || 'unknown'} @@ -352,30 +461,134 @@ const GitHubReposPage: React.FC = () => { {/* AI Analysis Button */}
- + ) : ( + + > + + AI Analysis Unavailable + + )}
+ + {/* Analysis Results Display */} + {repo.id && analysisResults[repo.id] && ( +
+
+

Analysis Complete

+ + {new Date(analysisResults[repo.id!].timestamp).toLocaleString()} + +
+ + {/* Analysis Statistics */} +
+
+ Files: + {analysisResults[repo.id!].stats?.total_files || 0} +
+
+ Lines: + {analysisResults[repo.id!].stats?.total_lines || 0} +
+
+ Quality: + {Math.round((analysisResults[repo.id!].stats?.code_quality_score || 0) * 100) / 100}/10 +
+
+ Issues: + {analysisResults[repo.id!].stats?.total_issues || 0} +
+
+ + {/* Languages */} + {analysisResults[repo.id!].stats?.languages && ( +
+ Languages: + + {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) + } + +
+ )} + + {/* PDF Actions */} +
+ + +
+
+ )} ))} @@ -401,6 +614,14 @@ const GitHubReposPage: React.FC = () => { )} + + {/* Repository Analysis */} + {selectedRepoId && user?.id && ( +
+

Analyzing Repository {selectedRepoId}

+ +
+ )} ); }; diff --git a/src/app/vcs/repos/page.tsx b/src/app/vcs/repos/page.tsx index 18afbd5..e04062c 100644 --- a/src/app/vcs/repos/page.tsx +++ b/src/app/vcs/repos/page.tsx @@ -35,6 +35,7 @@ interface VcsRepoSummary { language?: string; visibility: 'public' | 'private'; provider: 'github' | 'gitlab' | 'bitbucket' | 'gitea'; + provider_name?: 'github' | 'gitlab' | 'bitbucket' | 'gitea'; // Backend field html_url: string; clone_url: string; default_branch: string; @@ -182,10 +183,10 @@ const VcsReposPage: React.FC = () => { }); 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) => ({ ...repo, - provider: provider + provider: repo.provider_name || provider // Use backend provider_name, fallback to route provider })); allRepos.push(...reposWithProvider); diff --git a/src/components/apis/authApiClients.tsx b/src/components/apis/authApiClients.tsx index 7fba47f..3c0b214 100644 --- a/src/components/apis/authApiClients.tsx +++ b/src/components/apis/authApiClients.tsx @@ -87,7 +87,7 @@ export const logout = async () => { export const authApiClient = axios.create({ baseURL: BACKEND_URL, withCredentials: true, - timeout: 10000, // 10 second timeout + timeout: 600000, // 10 minute timeout for AI analysis }); // Add auth token to requests diff --git a/src/components/repository-analysis.tsx b/src/components/repository-analysis.tsx new file mode 100644 index 0000000..2fdd09b --- /dev/null +++ b/src/components/repository-analysis.tsx @@ -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 { + 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 { + 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(null); + const [error, setError] = useState(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 ( +
+
+ +

AI Repository Analysis

+
+ + {/* Analysis Button */} +
+ +
+ + {/* Progress Bar */} + {isAnalyzing && ( +
+
+ Analysis Progress + {Math.round(progress)}% +
+
+
+
+

+ Analyzing files and generating comprehensive report... +

+
+ )} + + {/* Error Message */} + {error && ( +
+ + {error} +
+ )} + + {/* Analysis Results */} + {analysisResult && ( +
+ {/* Success Message */} +
+ + Analysis completed successfully! +
+ + {/* Stats Grid */} +
+
+
+ + Total Files +
+

{analysisResult.stats?.total_files}

+
+ +
+
+ + Total Lines +
+

{analysisResult.stats?.total_lines?.toLocaleString()}

+
+ +
+
+ + Quality Score +
+

{Math.round((analysisResult.stats?.code_quality_score || 0) * 100) / 100}/10

+
+ +
+
+ + Total Issues +
+

{analysisResult.stats?.total_issues}

+
+
+ + {/* Quality Breakdown */} +
+

Quality Breakdown

+
+
+
{analysisResult.stats?.high_quality_files}
+
High Quality Files
+
+
+
{analysisResult.stats?.medium_quality_files}
+
Medium Quality Files
+
+
+
{analysisResult.stats?.low_quality_files}
+
Low Quality Files
+
+
+
+ + {/* Languages */} + {analysisResult.stats?.languages && Object.keys(analysisResult.stats.languages).length > 0 && ( +
+

Languages Detected

+
+ {Object.entries(analysisResult.stats.languages).map(([lang, count]) => ( + + {lang} ({count}) + + ))} +
+
+ )} + + {/* Report Actions */} +
+ + + +
+ + {/* PDF Preview */} + {showPdfPreview && analysisResult.report_path && ( +
+

Report Preview

+
+