implemented webshocket for ai analysis

This commit is contained in:
Pradeep 2025-10-28 09:19:21 +05:30
parent 1c23876181
commit 9adb24799b
5 changed files with 879 additions and 11 deletions

View File

@ -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
View 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>
);
}

View 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;

View File

@ -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,

View File

@ -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;