done the ai analysis in frntend
This commit is contained in:
parent
0b4141675e
commit
1c23876181
@ -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;
|
||||||
|
|||||||
38
src/app/api/ai-analysis/analyze-repository/route.ts
Normal file
38
src/app/api/ai-analysis/analyze-repository/route.ts
Normal 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 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
73
src/app/api/ai-analysis/reports/[filename]/route.ts
Normal file
73
src/app/api/ai-analysis/reports/[filename]/route.ts
Normal 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 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
300
src/components/repository-analysis.tsx
Normal file
300
src/components/repository-analysis.tsx
Normal 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;
|
||||||
|
|
||||||
71
src/hooks/useAIAnalysis.ts
Normal file
71
src/hooks/useAIAnalysis.ts
Normal 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 };
|
||||||
|
};
|
||||||
@ -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 {}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user