implemented webshocket for ai analysis
This commit is contained in:
parent
1c23876181
commit
9adb24799b
@ -5,6 +5,7 @@ import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/dialog';
|
||||
import {
|
||||
Github,
|
||||
Gitlab,
|
||||
@ -28,6 +29,7 @@ import { authApiClient } from '@/components/apis/authApiClients';
|
||||
import Link from 'next/link';
|
||||
import RepositoryAnalysis from '@/components/repository-analysis';
|
||||
import { useAuth } from '@/contexts/auth-context';
|
||||
import AIAnalysisProgressTracker from '@/components/ai/AIAnalysisProgressTracker';
|
||||
|
||||
const GitHubReposPage: React.FC = () => {
|
||||
const [repositories, setRepositories] = useState<GitHubRepoSummary[]>([]);
|
||||
@ -40,6 +42,12 @@ const GitHubReposPage: React.FC = () => {
|
||||
const [selectedRepoId, setSelectedRepoId] = useState<string | null>(null);
|
||||
const [analysisResults, setAnalysisResults] = useState<{[key: string]: any}>({});
|
||||
const { user } = useAuth();
|
||||
|
||||
// Progress tracking dialog state
|
||||
const [showProgressDialog, setShowProgressDialog] = useState(false);
|
||||
const [currentAnalysisId, setCurrentAnalysisId] = useState<string | null>(null);
|
||||
const [currentRepoName, setCurrentRepoName] = useState<string>('');
|
||||
const [currentRepoId, setCurrentRepoId] = useState<string | null>(null);
|
||||
|
||||
// Load repositories
|
||||
useEffect(() => {
|
||||
@ -80,34 +88,178 @@ const GitHubReposPage: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleAiAnalysis = async (repositoryId: string) => {
|
||||
const handleAiAnalysis = async (repositoryId: string, repoName: string) => {
|
||||
try {
|
||||
console.log('🚀 [DEBUG] Starting AI Analysis for repository:', repositoryId);
|
||||
console.log('🚀 [DEBUG] Repository name:', repoName);
|
||||
console.log('🚀 [DEBUG] User:', user);
|
||||
|
||||
setAiAnalysisLoading(repositoryId);
|
||||
setAiAnalysisError(null);
|
||||
|
||||
// Get user ID from auth context
|
||||
const userId = user?.id;
|
||||
let userId = user?.id;
|
||||
|
||||
// Fallback: try to get user ID from localStorage if context is not loaded
|
||||
if (!userId) {
|
||||
throw new Error('User not authenticated');
|
||||
try {
|
||||
const storedUser = localStorage.getItem('codenuk_user');
|
||||
if (storedUser) {
|
||||
const userData = JSON.parse(storedUser);
|
||||
userId = userData?.id;
|
||||
console.log('🔄 [DEBUG] Using user ID from localStorage:', userId);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ [DEBUG] Failed to parse user data from localStorage:', error);
|
||||
}
|
||||
}
|
||||
|
||||
if (!userId) {
|
||||
console.error('❌ [DEBUG] User not authenticated');
|
||||
console.error('❌ [DEBUG] User object:', user);
|
||||
console.error('❌ [DEBUG] User from localStorage:', localStorage.getItem('codenuk_user'));
|
||||
throw new Error('User not authenticated. Please sign in again.');
|
||||
}
|
||||
|
||||
// Check if user has valid authentication token
|
||||
const token = localStorage.getItem('accessToken');
|
||||
if (!token) {
|
||||
console.error('❌ [DEBUG] No authentication token found');
|
||||
throw new Error('Authentication token not found. Please sign in again.');
|
||||
}
|
||||
|
||||
console.log('🚀 Starting AI Analysis for repository:', repositoryId);
|
||||
console.log('🔍 User ID:', userId);
|
||||
console.log('🔍 Auth Token exists:', !!token);
|
||||
console.log('🔍 Token length:', token?.length || 0);
|
||||
console.log('🔍 User data from localStorage:', localStorage.getItem('codenuk_user'));
|
||||
console.log('🔍 Access token from localStorage:', localStorage.getItem('accessToken'));
|
||||
|
||||
// Test the getAccessToken function directly
|
||||
const { getAccessToken } = await import('@/components/apis/authApiClients');
|
||||
const testToken = getAccessToken();
|
||||
console.log('🔍 Test token from getAccessToken:', !!testToken, testToken?.length || 0);
|
||||
|
||||
// Call the new AI Analysis Service endpoint first
|
||||
console.log('📡 [DEBUG] Making API request to:', '/api/ai-analysis/analyze-repository');
|
||||
console.log('📡 [DEBUG] Request payload:', {
|
||||
repository_id: repositoryId,
|
||||
user_id: userId,
|
||||
output_format: 'pdf',
|
||||
max_files: 0,
|
||||
analysis_type: 'full'
|
||||
});
|
||||
|
||||
// Add request interceptor to log the actual request being sent
|
||||
const requestInterceptor = authApiClient.interceptors.request.use(
|
||||
(config) => {
|
||||
console.log('📡 [DEBUG] Request interceptor called');
|
||||
console.log('📡 [DEBUG] Actual request config:', {
|
||||
url: config.url,
|
||||
method: config.method,
|
||||
headers: config.headers,
|
||||
data: config.data,
|
||||
baseURL: config.baseURL,
|
||||
timeout: config.timeout
|
||||
});
|
||||
|
||||
// Manually add headers if they're missing (fallback)
|
||||
if (!config.headers.Authorization && token) {
|
||||
console.log('🔧 [DEBUG] Manually adding Authorization header');
|
||||
config.headers.Authorization = `Bearer ${token}`;
|
||||
}
|
||||
if (!config.headers['x-user-id'] && userId) {
|
||||
console.log('🔧 [DEBUG] Manually adding x-user-id header');
|
||||
config.headers['x-user-id'] = userId;
|
||||
}
|
||||
|
||||
// Check if headers are properly set after manual addition
|
||||
if (!config.headers.Authorization) {
|
||||
console.error('❌ [DEBUG] No Authorization header found in request');
|
||||
console.error('❌ [DEBUG] Available headers:', Object.keys(config.headers || {}));
|
||||
}
|
||||
if (!config.headers['x-user-id']) {
|
||||
console.error('❌ [DEBUG] No x-user-id header found in request');
|
||||
console.error('❌ [DEBUG] Available headers:', Object.keys(config.headers || {}));
|
||||
}
|
||||
|
||||
return config;
|
||||
},
|
||||
(error) => {
|
||||
console.error('📡 [DEBUG] Request interceptor error:', error);
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
// Add response interceptor to log the response
|
||||
const responseInterceptor = authApiClient.interceptors.response.use(
|
||||
(response) => {
|
||||
console.log('📡 [DEBUG] Response received:', {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
headers: response.headers,
|
||||
data: response.data
|
||||
});
|
||||
return response;
|
||||
},
|
||||
(error) => {
|
||||
console.error('📡 [DEBUG] Response error:', {
|
||||
message: error.message,
|
||||
code: error.code,
|
||||
status: error.response?.status,
|
||||
statusText: error.response?.statusText,
|
||||
data: error.response?.data,
|
||||
config: error.config
|
||||
});
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
console.log('📡 [DEBUG] About to make API request...');
|
||||
|
||||
// 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
|
||||
}, {
|
||||
timeout: 600000, // 10 minute timeout for AI analysis
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'x-user-id': userId
|
||||
}
|
||||
});
|
||||
|
||||
console.log('📡 [DEBUG] API request completed successfully');
|
||||
|
||||
// Remove the interceptors after the request
|
||||
authApiClient.interceptors.request.eject(requestInterceptor);
|
||||
authApiClient.interceptors.response.eject(responseInterceptor);
|
||||
|
||||
console.log('📡 [DEBUG] API Response:', response);
|
||||
|
||||
const data = response.data;
|
||||
|
||||
console.log('✅ AI Analysis Result:', data);
|
||||
console.log('🔍 [DEBUG] Setting showProgressDialog to true');
|
||||
console.log('🔍 [DEBUG] Analysis ID:', data.analysis_id);
|
||||
|
||||
if (data.success) {
|
||||
// Set analysis ID for progress tracking BEFORE opening dialog
|
||||
setCurrentAnalysisId(data.analysis_id);
|
||||
setCurrentRepoId(repositoryId); // Save repo ID for completion callback
|
||||
|
||||
// Show progress dialog with analysis ID
|
||||
setCurrentRepoName(repoName);
|
||||
setShowProgressDialog(true);
|
||||
|
||||
// Clear loading state since modal is now open
|
||||
setAiAnalysisLoading(null);
|
||||
|
||||
console.log('✅ [DEBUG] Modal should now be visible');
|
||||
|
||||
// Store analysis results in state
|
||||
setAnalysisResults(prev => ({
|
||||
...prev,
|
||||
@ -118,15 +270,40 @@ const GitHubReposPage: React.FC = () => {
|
||||
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 {
|
||||
console.log('❌ [DEBUG] Analysis failed:', data.message);
|
||||
throw new Error(data.message || 'Analysis failed');
|
||||
}
|
||||
|
||||
} catch (err: any) {
|
||||
console.error('❌ AI Analysis Error:', err);
|
||||
console.error('❌ Error details:', {
|
||||
message: err.message,
|
||||
response: err.response?.data,
|
||||
status: err.response?.status,
|
||||
config: err.config,
|
||||
code: err.code,
|
||||
stack: err.stack
|
||||
});
|
||||
|
||||
// Additional debugging for network issues
|
||||
if (err.code === 'ERR_NETWORK' || err.message?.includes('Network Error')) {
|
||||
console.error('🌐 Network Error - Check if backend services are running');
|
||||
console.error('🌐 Expected AI Analysis Service URL: http://localhost:8022');
|
||||
console.error('🌐 Expected API Gateway URL: http://localhost:8000');
|
||||
}
|
||||
|
||||
// Check for authentication issues
|
||||
if (err.response?.status === 401) {
|
||||
console.error('🔐 Authentication Error - User may need to sign in again');
|
||||
console.error('🔐 Current user:', user);
|
||||
console.error('🔐 Auth token exists:', !!localStorage.getItem('accessToken'));
|
||||
}
|
||||
|
||||
// Check for CORS issues
|
||||
if (err.message?.includes('CORS') || err.message?.includes('cross-origin')) {
|
||||
console.error('🌐 CORS Error - Check API Gateway CORS configuration');
|
||||
}
|
||||
|
||||
let errorMessage = 'AI Analysis failed';
|
||||
|
||||
@ -146,11 +323,42 @@ const GitHubReposPage: React.FC = () => {
|
||||
}
|
||||
|
||||
setAiAnalysisError(errorMessage);
|
||||
setShowProgressDialog(false);
|
||||
alert(`AI Analysis failed: ${errorMessage}`);
|
||||
} finally {
|
||||
setAiAnalysisLoading(null);
|
||||
}
|
||||
};
|
||||
|
||||
const handleAnalysisComplete = (completionData: any) => {
|
||||
console.log('✅ Analysis completed, full data:', completionData);
|
||||
|
||||
// Use the tracked repository ID
|
||||
if (currentRepoId) {
|
||||
// Save complete analysis results to state
|
||||
setAnalysisResults(prev => ({
|
||||
...prev,
|
||||
[currentRepoId]: {
|
||||
analysis_id: completionData.analysis_id || currentAnalysisId || '',
|
||||
report_path: completionData.report_path || '',
|
||||
stats: completionData.stats || {},
|
||||
timestamp: new Date().toISOString()
|
||||
}
|
||||
}));
|
||||
console.log('📊 Saved analysis results for repo:', currentRepoId);
|
||||
console.log('📄 Report path:', completionData.report_path);
|
||||
console.log('📈 Stats:', completionData.stats);
|
||||
} else {
|
||||
console.error('❌ No repository ID tracked for this analysis');
|
||||
}
|
||||
|
||||
// Keep dialog open so user can see completion status
|
||||
};
|
||||
|
||||
const handleAnalysisError = (error: string) => {
|
||||
console.error('❌ Analysis error:', error);
|
||||
setAiAnalysisError(error);
|
||||
};
|
||||
|
||||
const downloadAnalysisReport = async (repositoryId: string) => {
|
||||
const analysis = analysisResults[repositoryId];
|
||||
@ -477,8 +685,13 @@ const GitHubReposPage: React.FC = () => {
|
||||
variant="secondary"
|
||||
className="w-full"
|
||||
onClick={() => {
|
||||
console.log('🔘 [DEBUG] AI Analysis button clicked for repo:', repo);
|
||||
console.log('🔘 [DEBUG] Repository ID:', repo.id);
|
||||
console.log('🔘 [DEBUG] Repository name:', repo.repository_name || repo.name);
|
||||
if (repo.id) {
|
||||
handleAiAnalysis(String(repo.id));
|
||||
handleAiAnalysis(String(repo.id), repo.repository_name || repo.name || 'Repository');
|
||||
} else {
|
||||
console.error('❌ [DEBUG] No repository ID available');
|
||||
}
|
||||
}}
|
||||
disabled={aiAnalysisLoading === repo.id}
|
||||
@ -622,6 +835,39 @@ const GitHubReposPage: React.FC = () => {
|
||||
<RepositoryAnalysis repositoryId={selectedRepoId} userId={user.id} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* AI Analysis Progress Dialog */}
|
||||
<Dialog open={showProgressDialog} onOpenChange={setShowProgressDialog}>
|
||||
<DialogContent className="max-w-4xl max-h-[90vh] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle>AI Repository Analysis in Progress</DialogTitle>
|
||||
<DialogDescription>
|
||||
Analyzing <span className="font-semibold text-foreground">{currentRepoName}</span> with AI-powered insights
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="mt-4">
|
||||
{currentAnalysisId ? (
|
||||
<AIAnalysisProgressTracker
|
||||
analysisId={currentAnalysisId}
|
||||
onComplete={handleAnalysisComplete}
|
||||
onError={handleAnalysisError}
|
||||
/>
|
||||
) : (
|
||||
<div className="flex items-center justify-center p-8">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary mr-3"></div>
|
||||
<span className="text-muted-foreground">Initializing analysis...</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end gap-2 mt-4 pt-4 border-t">
|
||||
<Button variant="outline" onClick={() => setShowProgressDialog(false)}>
|
||||
Close
|
||||
</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
111
src/app/sse-test/page.tsx
Normal file
111
src/app/sse-test/page.tsx
Normal file
@ -0,0 +1,111 @@
|
||||
'use client';
|
||||
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Button } from '@/components/ui/button';
|
||||
|
||||
export default function SSETestPage() {
|
||||
const [isConnected, setIsConnected] = useState(false);
|
||||
const [events, setEvents] = useState<string[]>([]);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [analysisId, setAnalysisId] = useState('test_analysis_id');
|
||||
|
||||
const connectSSE = () => {
|
||||
const apiGatewayUrl = process.env.NEXT_PUBLIC_API_GATEWAY_URL || 'http://localhost:8000';
|
||||
const eventSourceUrl = `${apiGatewayUrl}/api/ai-analysis/progress/${analysisId}`;
|
||||
|
||||
console.log('🔌 Connecting to SSE:', eventSourceUrl);
|
||||
|
||||
const eventSource = new EventSource(eventSourceUrl);
|
||||
|
||||
eventSource.onopen = () => {
|
||||
console.log('✅ SSE connection established');
|
||||
setIsConnected(true);
|
||||
setError(null);
|
||||
setEvents(prev => [...prev, 'Connection established']);
|
||||
};
|
||||
|
||||
eventSource.onmessage = (event) => {
|
||||
console.log('📥 SSE Event:', event.data);
|
||||
setEvents(prev => [...prev, `Message: ${event.data}`]);
|
||||
};
|
||||
|
||||
eventSource.onerror = (err) => {
|
||||
console.error('❌ SSE connection error:', err);
|
||||
setIsConnected(false);
|
||||
setError('Connection error occurred');
|
||||
setEvents(prev => [...prev, 'Connection error']);
|
||||
};
|
||||
|
||||
return eventSource;
|
||||
};
|
||||
|
||||
const [eventSource, setEventSource] = useState<EventSource | null>(null);
|
||||
|
||||
const handleConnect = () => {
|
||||
if (eventSource) {
|
||||
eventSource.close();
|
||||
}
|
||||
const newEventSource = connectSSE();
|
||||
setEventSource(newEventSource);
|
||||
};
|
||||
|
||||
const handleDisconnect = () => {
|
||||
if (eventSource) {
|
||||
eventSource.close();
|
||||
setEventSource(null);
|
||||
setIsConnected(false);
|
||||
setEvents(prev => [...prev, 'Connection closed']);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (eventSource) {
|
||||
eventSource.close();
|
||||
}
|
||||
};
|
||||
}, [eventSource]);
|
||||
|
||||
return (
|
||||
<div className="container mx-auto p-6">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>SSE Connection Test</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className={`h-3 w-3 rounded-full ${isConnected ? 'bg-green-500' : 'bg-red-500'}`} />
|
||||
<span>Status: {isConnected ? 'Connected' : 'Disconnected'}</span>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<Button onClick={handleConnect} disabled={isConnected}>
|
||||
Connect
|
||||
</Button>
|
||||
<Button onClick={handleDisconnect} disabled={!isConnected}>
|
||||
Disconnect
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="text-red-600 p-2 bg-red-50 rounded">
|
||||
Error: {error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="space-y-2">
|
||||
<h3 className="font-semibold">Events:</h3>
|
||||
<div className="max-h-60 overflow-y-auto bg-gray-50 p-2 rounded">
|
||||
{events.map((event, index) => (
|
||||
<div key={index} className="text-sm font-mono">
|
||||
{event}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
511
src/components/ai/AIAnalysisProgressTracker.tsx
Normal file
511
src/components/ai/AIAnalysisProgressTracker.tsx
Normal file
@ -0,0 +1,511 @@
|
||||
'use client';
|
||||
|
||||
import React, { useEffect, useState, useCallback, useRef } from 'react';
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Progress } from '@/components/ui/progress';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||
import { CheckCircle2, Circle, XCircle, Loader2, FileCode, FolderTree, FileCheck } from 'lucide-react';
|
||||
|
||||
interface ProgressEvent {
|
||||
analysis_id: string;
|
||||
event: string;
|
||||
data: {
|
||||
message: string;
|
||||
file_path?: string;
|
||||
current?: number;
|
||||
total?: number;
|
||||
percent?: number;
|
||||
quality_score?: number;
|
||||
issues_count?: number;
|
||||
total_files?: number;
|
||||
error?: string;
|
||||
// Batch event properties
|
||||
batch?: number;
|
||||
total_batches?: number;
|
||||
files_processed?: number;
|
||||
// Smart Batching properties
|
||||
files?: string[];
|
||||
batch_size?: number;
|
||||
processing_mode?: string;
|
||||
// Analysis completion properties
|
||||
report_path?: string;
|
||||
analysis_id?: string;
|
||||
stats?: any;
|
||||
timestamp?: string;
|
||||
};
|
||||
timestamp: string;
|
||||
}
|
||||
|
||||
interface FileProgress {
|
||||
path: string;
|
||||
status: 'pending' | 'analyzing' | 'completed' | 'error';
|
||||
qualityScore?: number;
|
||||
issuesCount?: number;
|
||||
}
|
||||
|
||||
interface AIAnalysisProgressTrackerProps {
|
||||
analysisId: string;
|
||||
onComplete?: (completionData: any) => void;
|
||||
onError?: (error: string) => void;
|
||||
}
|
||||
|
||||
export const AIAnalysisProgressTracker: React.FC<AIAnalysisProgressTrackerProps> = ({
|
||||
analysisId,
|
||||
onComplete,
|
||||
onError,
|
||||
}) => {
|
||||
const [events, setEvents] = useState<ProgressEvent[]>([]);
|
||||
const [files, setFiles] = useState<Map<string, FileProgress>>(new Map());
|
||||
const [currentFile, setCurrentFile] = useState<string>('');
|
||||
const [progress, setProgress] = useState<number>(0);
|
||||
const [totalFiles, setTotalFiles] = useState<number>(0);
|
||||
const [currentFileNum, setCurrentFileNum] = useState<number>(0);
|
||||
const [phase, setPhase] = useState<string>('Starting...');
|
||||
const [isConnected, setIsConnected] = useState<boolean>(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const currentFileRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const handleProgressEvent = useCallback((event: ProgressEvent) => {
|
||||
setEvents((prev) => [...prev, event]);
|
||||
|
||||
switch (event.event) {
|
||||
case 'analysis_started':
|
||||
setPhase('Analysis Started');
|
||||
setProgress(0);
|
||||
break;
|
||||
|
||||
case 'files_discovered':
|
||||
setTotalFiles(event.data.total_files || 0);
|
||||
setPhase(`Found ${event.data.total_files} files`);
|
||||
// Initialize file map
|
||||
setFiles(new Map());
|
||||
break;
|
||||
|
||||
case 'file_analysis_started':
|
||||
if (event.data.file_path) {
|
||||
setCurrentFile(event.data.file_path);
|
||||
setCurrentFileNum(event.data.current || 0);
|
||||
setProgress(event.data.percent || 0);
|
||||
setPhase(`Analyzing file ${event.data.current}/${event.data.total}`);
|
||||
|
||||
setFiles((prev) => {
|
||||
const newMap = new Map(prev);
|
||||
newMap.set(event.data.file_path!, {
|
||||
path: event.data.file_path!,
|
||||
status: 'analyzing',
|
||||
});
|
||||
return newMap;
|
||||
});
|
||||
|
||||
// Auto-scroll to current file
|
||||
setTimeout(() => {
|
||||
currentFileRef.current?.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
||||
}, 100);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'file_analysis_completed':
|
||||
if (event.data.file_path) {
|
||||
setFiles((prev) => {
|
||||
const newMap = new Map(prev);
|
||||
newMap.set(event.data.file_path!, {
|
||||
path: event.data.file_path!,
|
||||
status: 'completed',
|
||||
qualityScore: event.data.quality_score,
|
||||
issuesCount: event.data.issues_count,
|
||||
});
|
||||
return newMap;
|
||||
});
|
||||
}
|
||||
break;
|
||||
|
||||
case 'file_analysis_error':
|
||||
if (event.data.file_path) {
|
||||
setFiles((prev) => {
|
||||
const newMap = new Map(prev);
|
||||
newMap.set(event.data.file_path!, {
|
||||
path: event.data.file_path!,
|
||||
status: 'error',
|
||||
});
|
||||
return newMap;
|
||||
});
|
||||
}
|
||||
break;
|
||||
|
||||
case 'smart_batch_started':
|
||||
// Smart Batching: Multiple files in single API call
|
||||
if (event.data.files && Array.isArray(event.data.files)) {
|
||||
setPhase(`Smart Batch: Processing ${event.data.files.length} files in single API call`);
|
||||
// Mark all files in batch as analyzing
|
||||
event.data.files.forEach((filePath: string) => {
|
||||
setFiles((prev) => {
|
||||
const newMap = new Map(prev);
|
||||
newMap.set(filePath, {
|
||||
path: filePath,
|
||||
status: 'analyzing',
|
||||
});
|
||||
return newMap;
|
||||
});
|
||||
});
|
||||
}
|
||||
break;
|
||||
|
||||
case 'smart_batch_completed':
|
||||
// Smart Batching: Batch completed
|
||||
setProgress(event.data.percent || 70);
|
||||
setCurrentFileNum(event.data.files_processed || 0);
|
||||
if (event.data.processing_mode === 'smart_batching') {
|
||||
setPhase(`Smart Batch ${event.data.batch || 0}/${event.data.total_batches || 0} completed (${event.data.files_processed || 0}/${event.data.total_files || 0} files)`);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'batch_completed':
|
||||
// Update progress but don't override phase if we're showing individual files
|
||||
setProgress(event.data.percent || 70);
|
||||
setCurrentFileNum(event.data.files_processed || 0);
|
||||
// Only update phase if no current file is being shown
|
||||
if (!currentFile) {
|
||||
setPhase(`Completed batch ${event.data.batch || 0}/${event.data.total_batches || 0} (${event.data.files_processed || 0}/${event.data.total_files || 0} files)`);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'repository_analysis_started':
|
||||
setPhase('Repository-level Analysis');
|
||||
setProgress(event.data.percent || 70);
|
||||
setCurrentFile('');
|
||||
break;
|
||||
|
||||
case 'report_generation_started':
|
||||
setPhase('Generating PDF Report');
|
||||
setProgress(event.data.percent || 85);
|
||||
break;
|
||||
|
||||
case 'analysis_completed':
|
||||
setPhase('Analysis Completed');
|
||||
setProgress(100);
|
||||
if (onComplete) {
|
||||
// Pass the full completion data, not just report_path
|
||||
onComplete(event.data as any);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'analysis_error':
|
||||
setPhase('Analysis Failed');
|
||||
setError(event.data.error || event.data.message);
|
||||
if (onError) {
|
||||
onError(event.data.error || event.data.message);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}, [onComplete, onError]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!analysisId) {
|
||||
console.log('⚠️ No analysis ID provided');
|
||||
return;
|
||||
}
|
||||
|
||||
const apiGatewayUrl = process.env.NEXT_PUBLIC_API_GATEWAY_URL || 'http://localhost:8000';
|
||||
const eventSourceUrl = `${apiGatewayUrl}/api/ai-analysis/progress/${analysisId}`;
|
||||
|
||||
console.log('🔌 Connecting to SSE:', eventSourceUrl);
|
||||
console.log('🔌 Analysis ID:', analysisId);
|
||||
|
||||
// Reset state when starting new connection
|
||||
setEvents([]);
|
||||
setFiles(new Map());
|
||||
setError(null);
|
||||
setIsConnected(false);
|
||||
|
||||
// Create EventSource with error handling
|
||||
// Note: EventSource doesn't support custom headers, but the API Gateway
|
||||
// is configured to allow unauthenticated access for AI analysis
|
||||
const eventSource = new EventSource(eventSourceUrl);
|
||||
|
||||
eventSource.onopen = () => {
|
||||
console.log('✅ SSE connection established');
|
||||
setIsConnected(true);
|
||||
setError(null); // Clear any previous errors
|
||||
};
|
||||
|
||||
eventSource.onmessage = (event) => {
|
||||
try {
|
||||
console.log('📥 Raw SSE Event:', event.data);
|
||||
|
||||
// Handle keepalive pings
|
||||
if (event.data.startsWith(': ')) {
|
||||
console.log('💓 SSE Keepalive ping received');
|
||||
return;
|
||||
}
|
||||
|
||||
// Mark that we've received events
|
||||
hasReceivedEvents = true;
|
||||
|
||||
const data = JSON.parse(event.data) as ProgressEvent;
|
||||
console.log('📥 Parsed SSE Event:', data.event, data.data);
|
||||
handleProgressEvent(data);
|
||||
|
||||
// Add to events list for debugging
|
||||
setEvents(prev => [...prev, event.data]);
|
||||
|
||||
// Reset connection state on successful message
|
||||
if (!isConnected) {
|
||||
console.log('🔄 SSE connection restored');
|
||||
setIsConnected(true);
|
||||
setError(null);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('❌ Error parsing SSE event:', err);
|
||||
console.error('❌ Raw event data:', event.data);
|
||||
}
|
||||
};
|
||||
|
||||
eventSource.onerror = (err) => {
|
||||
console.log('🔄 SSE connection state changed:', eventSource.readyState);
|
||||
|
||||
// Only set disconnected if the connection is actually closed
|
||||
if (eventSource.readyState === EventSource.CLOSED) {
|
||||
setIsConnected(false);
|
||||
// Don't show error if progress is 100% (analysis completed successfully)
|
||||
if (progress < 100) {
|
||||
setError('Connection closed. Analysis may have completed or failed.');
|
||||
console.log('❌ SSE connection closed before completion');
|
||||
} else {
|
||||
console.log('✅ SSE connection closed after successful completion');
|
||||
}
|
||||
} else if (eventSource.readyState === EventSource.CONNECTING) {
|
||||
console.log('🔄 SSE reconnecting...');
|
||||
}
|
||||
};
|
||||
|
||||
// Add timeout to handle cases where analysis completes very quickly
|
||||
// Use a ref to track if we've received any events to avoid race conditions
|
||||
let hasReceivedEvents = false;
|
||||
|
||||
const timeoutId = setTimeout(() => {
|
||||
console.log(`⏰ Timeout check: isConnected=${isConnected}, hasReceivedEvents=${hasReceivedEvents}`);
|
||||
if (!isConnected && !hasReceivedEvents) {
|
||||
console.log('⏰ SSE connection timeout - no events received and not connected');
|
||||
setError('Analysis may have completed already. Please check the results.');
|
||||
} else if (isConnected && !hasReceivedEvents) {
|
||||
console.log('⏰ SSE connection timeout - connected but no events received');
|
||||
setError('Analysis is taking longer than expected. Please wait...');
|
||||
} else if (isConnected && hasReceivedEvents) {
|
||||
console.log('⏰ SSE connection timeout - connected and events received, analysis may be continuing');
|
||||
// Don't set error if we're connected and have received events
|
||||
}
|
||||
}, 120000); // Increased to 2 minutes for real analysis
|
||||
|
||||
return () => {
|
||||
console.log('🔌 Closing SSE connection');
|
||||
clearTimeout(timeoutId);
|
||||
eventSource.close();
|
||||
setIsConnected(false);
|
||||
};
|
||||
}, [analysisId, handleProgressEvent]);
|
||||
|
||||
const fileList = Array.from(files.values());
|
||||
const completedFiles = Math.max(fileList.filter((f) => f.status === 'completed').length, currentFileNum);
|
||||
const errorFiles = fileList.filter((f) => f.status === 'error').length;
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{/* Connection Status */}
|
||||
<div className="flex items-center gap-2 text-sm bg-black/40 border border-white/10 px-4 py-2.5 rounded-lg">
|
||||
<div className="relative">
|
||||
<div className={`h-2.5 w-2.5 rounded-full ${isConnected ? 'bg-orange-500' : 'bg-gray-600'}`} />
|
||||
{isConnected && (
|
||||
<div className="absolute inset-0 h-2.5 w-2.5 bg-orange-500 rounded-full animate-ping opacity-75"></div>
|
||||
)}
|
||||
</div>
|
||||
<span className={`font-medium ${isConnected ? 'text-orange-500' : 'text-gray-400'}`}>
|
||||
{isConnected ? '● Live Analysis' : 'Connecting...'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Main Progress Card */}
|
||||
<Card className="bg-black/60 border border-white/10">
|
||||
<CardHeader className="border-b border-white/10">
|
||||
<CardTitle className="flex items-center gap-3">
|
||||
{progress < 100 ? (
|
||||
<div className="relative">
|
||||
<Loader2 className="h-6 w-6 text-orange-500 animate-spin" />
|
||||
<div className="absolute inset-0 h-6 w-6 bg-orange-500 rounded-full animate-ping opacity-20"></div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="relative">
|
||||
<CheckCircle2 className="h-6 w-6 text-orange-500" />
|
||||
<div className="absolute inset-0 h-6 w-6 bg-orange-500 rounded-full animate-pulse opacity-30"></div>
|
||||
</div>
|
||||
)}
|
||||
<span className="text-white">{phase}</span>
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
{currentFile && (
|
||||
<span className="flex items-center gap-2 mt-2 bg-black/60 px-3 py-1.5 rounded-lg border border-orange-500/30">
|
||||
<FileCode className="h-4 w-4 text-orange-500" />
|
||||
<span className="font-mono text-orange-400 text-sm">{currentFile}</span>
|
||||
</span>
|
||||
)}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4 pt-6">
|
||||
{/* Progress Bar */}
|
||||
<div className="space-y-3">
|
||||
<div className="flex justify-between text-sm font-semibold">
|
||||
<span className="text-orange-500">{Math.round(progress)}%</span>
|
||||
{totalFiles > 0 && (
|
||||
<span className="text-gray-400">
|
||||
{currentFileNum}/{totalFiles} files
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="relative h-3 bg-white/5 rounded-full overflow-hidden border border-white/10">
|
||||
<div
|
||||
className="absolute h-full bg-gradient-to-r from-orange-600 via-orange-500 to-orange-600 transition-all duration-500 ease-out rounded-full"
|
||||
style={{ width: `${progress}%` }}
|
||||
>
|
||||
<div className="absolute inset-0 bg-white/20 animate-pulse"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Statistics */}
|
||||
{totalFiles > 0 && (
|
||||
<div className="flex gap-3 text-sm">
|
||||
<div className="flex items-center gap-2 bg-orange-500/20 px-3 py-2 rounded-lg border border-orange-500/30">
|
||||
<CheckCircle2 className="h-5 w-5 text-orange-500" />
|
||||
<span className="font-semibold text-white">{completedFiles} completed</span>
|
||||
</div>
|
||||
{errorFiles > 0 && (
|
||||
<div className="flex items-center gap-2 bg-red-500/20 px-3 py-2 rounded-lg border border-red-500/30">
|
||||
<XCircle className="h-5 w-5 text-red-500" />
|
||||
<span className="font-semibold text-white">{errorFiles} errors</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Error Display */}
|
||||
{error && (
|
||||
<div className="rounded-lg bg-red-500/20 p-4 text-sm border border-red-500/30">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="relative">
|
||||
<XCircle className="h-5 w-5 text-red-500" />
|
||||
<div className="absolute inset-0 h-5 w-5 bg-red-500 rounded-full animate-ping opacity-30"></div>
|
||||
</div>
|
||||
<span className="font-bold text-white">Error:</span>
|
||||
</div>
|
||||
<p className="mt-2 ml-7 text-gray-300">{error}</p>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* File List */}
|
||||
{totalFiles > 0 && (
|
||||
<Card className="bg-black/60 border border-white/10">
|
||||
<CardHeader className="border-b border-white/10">
|
||||
<CardTitle className="text-base flex items-center gap-2">
|
||||
<div className="relative">
|
||||
<FolderTree className="h-5 w-5 text-orange-500" />
|
||||
<div className="absolute -inset-1 bg-orange-500 rounded-full opacity-20 animate-pulse"></div>
|
||||
</div>
|
||||
<span className="text-white font-bold">Files Analysis Progress</span>
|
||||
<span className="ml-auto bg-orange-500/20 text-orange-400 px-3 py-1 rounded-lg text-sm font-semibold border border-orange-500/30">
|
||||
{fileList.length}/{totalFiles}
|
||||
</span>
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="pt-4">
|
||||
<ScrollArea className="h-[300px] pr-4">
|
||||
{fileList.length === 0 ? (
|
||||
<div className="flex items-center justify-center p-8 text-muted-foreground">
|
||||
<Loader2 className="h-5 w-5 animate-spin mr-2" />
|
||||
<span>Preparing files for analysis...</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{fileList.map((file, index) => (
|
||||
<div
|
||||
key={file.path}
|
||||
ref={file.status === 'analyzing' ? currentFileRef : null}
|
||||
className={`flex items-center justify-between p-3.5 rounded-lg border transition-all duration-300 ${
|
||||
file.status === 'analyzing'
|
||||
? 'bg-orange-500/20 border-orange-500/50 shadow-lg shadow-orange-500/20 scale-[1.02]'
|
||||
: file.status === 'completed'
|
||||
? 'bg-white/5 border-white/10'
|
||||
: file.status === 'error'
|
||||
? 'bg-red-500/20 border-red-500/30'
|
||||
: 'bg-black/40 border-white/5'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center gap-3 flex-1 min-w-0">
|
||||
{file.status === 'pending' && (
|
||||
<div className="relative flex-shrink-0">
|
||||
<Circle className="h-4 w-4 text-gray-600" />
|
||||
</div>
|
||||
)}
|
||||
{file.status === 'analyzing' && (
|
||||
<div className="relative flex-shrink-0">
|
||||
<Loader2 className="h-5 w-5 text-orange-500 animate-spin" />
|
||||
<div className="absolute inset-0 h-5 w-5 bg-orange-500 rounded-full animate-ping opacity-30"></div>
|
||||
</div>
|
||||
)}
|
||||
{file.status === 'completed' && (
|
||||
<div className="relative flex-shrink-0">
|
||||
<CheckCircle2 className="h-5 w-5 text-orange-500" />
|
||||
<div className="absolute inset-0 h-5 w-5 bg-orange-500 rounded-full animate-pulse opacity-20"></div>
|
||||
</div>
|
||||
)}
|
||||
{file.status === 'error' && (
|
||||
<XCircle className="h-5 w-5 text-red-500 flex-shrink-0" />
|
||||
)}
|
||||
|
||||
<span className={`text-sm font-mono truncate ${
|
||||
file.status === 'analyzing' ? 'text-orange-400 font-bold' :
|
||||
file.status === 'completed' ? 'text-white font-medium' :
|
||||
file.status === 'error' ? 'text-red-400' :
|
||||
'text-gray-500'
|
||||
}`}>
|
||||
{file.path}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2 flex-shrink-0">
|
||||
{file.qualityScore !== undefined && (
|
||||
<Badge
|
||||
variant={file.qualityScore >= 8 ? 'default' : file.qualityScore >= 5 ? 'secondary' : 'destructive'}
|
||||
className={`font-bold ${
|
||||
file.qualityScore >= 8 ? 'bg-orange-600 text-white border border-orange-700' :
|
||||
file.qualityScore >= 5 ? 'bg-orange-500/50 text-orange-200 border border-orange-500' :
|
||||
'bg-red-600 text-white border border-red-700'
|
||||
}`}
|
||||
>
|
||||
{file.qualityScore.toFixed(1)}/10
|
||||
</Badge>
|
||||
)}
|
||||
{file.issuesCount !== undefined && file.issuesCount > 0 && (
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="bg-black/40 border border-orange-500/30 text-orange-400 font-bold"
|
||||
>
|
||||
{file.issuesCount} {file.issuesCount === 1 ? 'issue' : 'issues'}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</ScrollArea>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AIAnalysisProgressTracker;
|
||||
|
||||
@ -108,7 +108,7 @@ const addAuthTokenInterceptor = (client: typeof authApiClient) => {
|
||||
(config.headers as any)['x-user-id'] = userId;
|
||||
|
||||
// Debug: Log user ID being sent (only for AI endpoints)
|
||||
if (config.url?.includes('/api/ai/')) {
|
||||
if (config.url?.includes('/api/ai/') || config.url?.includes('/api/ai-analysis/')) {
|
||||
console.log('🔍 [USER-ID] Sending user ID:', userId, 'for request:', config.url);
|
||||
console.log('🔍 [USER-ID] Request headers:', {
|
||||
'x-user-id': userId,
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
|
||||
//
|
||||
export const BACKEND_URL = 'http://localhost:8000';
|
||||
// Backend Configuration
|
||||
export const BACKEND_URL = process.env.NEXT_PUBLIC_API_GATEWAY_URL || 'http://localhost:8000';
|
||||
|
||||
export const SOCKET_URL = BACKEND_URL;
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user