codenuk_frontend_mine/src/app/diff-viewer/page.tsx

519 lines
18 KiB
TypeScript

// app/diff-viewer/page.tsx
'use client';
import React, { useState, useEffect, Suspense } from 'react';
import { useSearchParams } from 'next/navigation';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Badge } from '@/components/ui/badge';
import {
GitCommit,
FolderOpen,
Search,
RefreshCw,
ExternalLink,
Brain,
CheckSquare,
Square
} from 'lucide-react';
import DiffViewer from '@/components/diff-viewer/DiffViewer';
import { authApiClient } from '@/components/apis/authApiClients';
interface Repository {
id: string;
repository_name: string;
owner_name: string;
sync_status: string;
created_at: string;
}
interface Commit {
id: string;
commit_sha: string;
author_name: string;
message: string;
committed_at: string;
files_changed: number;
diffs_processed: number;
total_diff_size: number;
}
const DiffViewerContent: React.FC = () => {
const searchParams = useSearchParams();
const [repositories, setRepositories] = useState<Repository[]>([]);
const [commits, setCommits] = useState<Commit[]>([]);
const [selectedRepository, setSelectedRepository] = useState<string>('');
const [selectedCommit, setSelectedCommit] = useState<string>('');
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [searchQuery, setSearchQuery] = useState('');
const [selectedCommits, setSelectedCommits] = useState<string[]>([]);
const [bulkAnalysisLoading, setBulkAnalysisLoading] = useState(false);
const [bulkAnalysisError, setBulkAnalysisError] = useState<string | null>(null);
// Load repositories
useEffect(() => {
const loadRepositories = async () => {
try {
setIsLoading(true);
const response = await authApiClient.get('/api/diffs/repositories');
const data = response.data;
if (data.success) {
setRepositories(data.data.repositories);
// Handle URL parameters after repositories are loaded
const repoId = searchParams.get('repo') || searchParams.get('repository');
console.log('URL repoId:', repoId);
console.log('Available repositories:', data.data.repositories.map((r: Repository) => ({ id: r.id, name: r.repository_name })));
if (repoId) {
console.log('Setting selected repository to:', repoId);
setSelectedRepository(repoId);
}
} else {
setError(data.message || 'Failed to load repositories');
}
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to load repositories');
} finally {
setIsLoading(false);
}
};
loadRepositories();
}, [searchParams]);
// Load commits when repository is selected
useEffect(() => {
if (selectedRepository) {
const loadCommits = async () => {
try {
setIsLoading(true);
const response = await authApiClient.get(`/api/diffs/repositories/${selectedRepository}/commits`);
const data = response.data;
if (data.success) {
setCommits(data.data.commits);
// Auto-select first commit
if (data.data.commits.length > 0) {
setSelectedCommit(data.data.commits[0].id);
}
} else {
setError(data.message || 'Failed to load commits');
}
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to load commits');
} finally {
setIsLoading(false);
}
};
loadCommits();
}
}, [selectedRepository]);
const handleRepositoryChange = (repositoryId: string) => {
setSelectedRepository(repositoryId);
setSelectedCommit('');
setSelectedCommits([]); // Reset bulk selection
};
const handleCommitChange = (commitId: string) => {
setSelectedCommit(commitId);
};
const handleRefresh = () => {
if (selectedRepository) {
const loadCommits = async () => {
try {
setIsLoading(true);
const response = await authApiClient.get(`/api/diffs/repositories/${selectedRepository}/commits`);
const data = response.data;
if (data.success) {
setCommits(data.data.commits);
}
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to refresh commits');
} finally {
setIsLoading(false);
}
};
loadCommits();
}
};
const handleBulkAnalysis = async () => {
if (!selectedRepository || selectedCommits.length === 0) {
setBulkAnalysisError('Please select a repository and at least one commit for bulk analysis.');
return;
}
setBulkAnalysisLoading(true);
setBulkAnalysisError(null);
try {
const response = await authApiClient.post(`/api/ai/repository/${selectedRepository}/bulk-analysis`, {
commit_ids: selectedCommits,
analysis_type: "bulk",
include_content: "true",
stream: "false"
}, {
timeout: 120000 // 2 minutes timeout for bulk analysis
});
const data = response.data;
console.log('Bulk Analysis Result:', data);
// Log user ID from response
if (data.user_id) {
console.log('🔍 [USER-ID] Received user ID in bulk analysis response:', data.user_id);
}
alert(`Bulk AI Analysis completed successfully for ${selectedCommits.length} commits!`);
} catch (err) {
const errorMessage = err instanceof Error ? err.message : 'Bulk AI Analysis failed';
setBulkAnalysisError(errorMessage);
console.error('Bulk Analysis Error:', err);
} finally {
setBulkAnalysisLoading(false);
}
};
const toggleCommitSelection = (commitId: string) => {
setSelectedCommits(prev =>
prev.includes(commitId)
? prev.filter(id => id !== commitId)
: [...prev, commitId]
);
};
const selectAllCommits = () => {
setSelectedCommits(commits.map(commit => commit.id));
};
const clearSelection = () => {
setSelectedCommits([]);
};
const filteredCommits = commits.filter(commit =>
commit.message.toLowerCase().includes(searchQuery.toLowerCase()) ||
commit.author_name.toLowerCase().includes(searchQuery.toLowerCase()) ||
commit.commit_sha.toLowerCase().includes(searchQuery.toLowerCase())
);
return (
<div className="max-w-7xl mx-auto p-6 space-y-6">
<div className="flex items-center justify-between">
<div>
<h1 className="text-3xl font-bold">Git Diff Viewer</h1>
<p className="text-muted-foreground mt-2">
View and analyze git diffs from your repositories
</p>
</div>
<Button
variant="outline"
onClick={handleRefresh}
disabled={isLoading || !selectedRepository}
>
<RefreshCw className={`h-4 w-4 mr-2 ${isLoading ? 'animate-spin' : ''}`} />
Refresh
</Button>
</div>
{/* Repository and Commit Selection */}
<Card>
<CardHeader>
<CardTitle className="flex items-center space-x-2">
<FolderOpen className="h-5 w-5" />
<span>Select Repository & Commit</span>
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
{/* Repository Selection */}
<div>
<Label htmlFor="repository">Repository</Label>
<select
id="repository"
value={selectedRepository}
onChange={(e) => handleRepositoryChange(e.target.value)}
className="w-full mt-1 px-3 py-2 border border-input rounded-md bg-background"
disabled={isLoading}
>
<option value="">Select a repository...</option>
{repositories.map((repo) => (
<option key={repo.id} value={repo.id}>
{repo.owner_name}/{repo.repository_name} ({repo.sync_status})
</option>
))}
</select>
</div>
{/* Commit Selection */}
{selectedRepository && (
<div>
<div className="flex items-center justify-between mb-2">
<Label htmlFor="commit">Commit</Label>
<Badge variant="outline">
{commits.length} commits
</Badge>
</div>
{/* Search */}
<div className="relative mb-3">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input
placeholder="Search commits..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="pl-10"
/>
</div>
<select
id="commit"
value={selectedCommit}
onChange={(e) => handleCommitChange(e.target.value)}
className="w-full px-3 py-2 border border-input rounded-md bg-background"
disabled={isLoading}
>
<option value="">Select a commit...</option>
{filteredCommits.map((commit) => (
<option key={commit.id} value={commit.id}>
{commit.commit_sha.substring(0, 8)} - {commit.message.substring(0, 50)}
{commit.message.length > 50 ? '...' : ''}
</option>
))}
</select>
</div>
)}
{/* Commit Info */}
{selectedCommit && (
<div className="bg-muted/50 p-4 rounded-lg">
<div className="flex items-center space-x-2 mb-2">
<GitCommit className="h-4 w-4" />
<span className="font-medium">Selected Commit</span>
</div>
{(() => {
const commit = commits.find(c => c.id === selectedCommit);
return commit ? (
<div className="space-y-1 text-sm">
<div className="flex items-center space-x-2">
<span className="font-mono text-xs bg-muted px-2 py-1 rounded">
{commit.commit_sha.substring(0, 8)}
</span>
<span className="text-muted-foreground">by {commit.author_name}</span>
</div>
<p className="font-medium">{commit.message}</p>
<div className="flex items-center space-x-4 text-xs text-muted-foreground">
<span>{commit.files_changed} files changed</span>
<span>{commit.diffs_processed} diffs processed</span>
<span>{(commit.total_diff_size / 1024).toFixed(1)} KB</span>
</div>
</div>
) : null;
})()}
</div>
)}
</CardContent>
</Card>
{/* Error Display */}
{error && (
<Card className="border-destructive">
<CardContent className="pt-6">
<div className="text-center text-destructive">
<p className="font-medium">Error</p>
<p className="text-sm mt-2">{error}</p>
<Button
variant="outline"
className="mt-4"
onClick={() => setError(null)}
>
Dismiss
</Button>
</div>
</CardContent>
</Card>
)}
{/* Bulk Analysis Error Display */}
{bulkAnalysisError && (
<Card className="border-destructive">
<CardContent className="pt-6">
<div className="text-center text-destructive">
<p className="font-medium">Bulk Analysis Error</p>
<p className="text-sm mt-2">{bulkAnalysisError}</p>
<Button
variant="outline"
className="mt-4"
onClick={() => setBulkAnalysisError(null)}
>
Dismiss
</Button>
</div>
</CardContent>
</Card>
)}
{/* Bulk Analysis Section */}
{selectedRepository && commits.length > 0 && (
<Card>
<CardHeader>
<CardTitle className="flex items-center space-x-2">
<Brain className="h-5 w-5" />
<span>Bulk AI Analysis</span>
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex items-center justify-between">
<p className="text-sm text-muted-foreground">
Select multiple commits for bulk AI analysis
</p>
<div className="flex gap-2">
<Button
variant="outline"
size="sm"
onClick={selectAllCommits}
disabled={commits.length === 0}
>
Select All
</Button>
<Button
variant="outline"
size="sm"
onClick={clearSelection}
disabled={selectedCommits.length === 0}
>
Clear
</Button>
</div>
</div>
{/* Commit Selection List */}
<div className="max-h-60 overflow-y-auto border rounded-lg p-4 space-y-2">
{filteredCommits.map((commit) => (
<div
key={commit.id}
className={`flex items-center space-x-3 p-2 rounded-lg cursor-pointer transition-colors ${
selectedCommits.includes(commit.id)
? 'bg-primary/10 border border-primary/20'
: 'hover:bg-muted/50'
}`}
onClick={() => toggleCommitSelection(commit.id)}
>
<div className="flex-shrink-0">
{selectedCommits.includes(commit.id) ? (
<CheckSquare className="h-4 w-4 text-primary" />
) : (
<Square className="h-4 w-4 text-muted-foreground" />
)}
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center space-x-2">
<code className="text-xs bg-muted px-2 py-1 rounded">
{commit.commit_sha.substring(0, 8)}
</code>
<span className="text-sm font-medium truncate">
{commit.message}
</span>
</div>
<div className="text-xs text-muted-foreground mt-1">
by {commit.author_name} {new Date(commit.committed_at).toLocaleDateString()}
</div>
</div>
</div>
))}
</div>
{/* Bulk Analysis Button */}
<div className="flex items-center justify-between pt-4 border-t">
<div className="text-sm text-muted-foreground">
{selectedCommits.length} commit{selectedCommits.length !== 1 ? 's' : ''} selected
</div>
<Button
onClick={handleBulkAnalysis}
disabled={selectedCommits.length === 0 || bulkAnalysisLoading}
className="min-w-[140px]"
>
{bulkAnalysisLoading ? (
<>
<RefreshCw className="h-4 w-4 mr-2 animate-spin" />
Analyzing...
</>
) : (
<>
<Brain className="h-4 w-4 mr-2" />
Analyze {selectedCommits.length} Commits
</>
)}
</Button>
</div>
</CardContent>
</Card>
)}
{/* Diff Viewer */}
{selectedRepository && selectedCommit && (
<DiffViewer
repositoryId={selectedRepository}
commitId={selectedCommit}
initialView="side-by-side"
className="min-h-[600px]"
/>
)}
{/* No Selection State */}
{!selectedRepository && (
<Card>
<CardContent className="pt-6">
<div className="text-center text-muted-foreground">
<FolderOpen className="h-12 w-12 mx-auto mb-4 opacity-50" />
<p className="font-medium">No Repository Selected</p>
<p className="text-sm mt-2">
Please select a repository to view its diffs
</p>
</div>
</CardContent>
</Card>
)}
{selectedRepository && !selectedCommit && (
<Card>
<CardContent className="pt-6">
<div className="text-center text-muted-foreground">
<GitCommit className="h-12 w-12 mx-auto mb-4 opacity-50" />
<p className="font-medium">No Commit Selected</p>
<p className="text-sm mt-2">
Please select a commit to view its diffs
</p>
</div>
</CardContent>
</Card>
)}
</div>
);
};
const DiffViewerPage: React.FC = () => {
return (
<Suspense fallback={
<div className="max-w-7xl mx-auto p-6">
<div className="text-center">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary mx-auto mb-4"></div>
<p>Loading diff viewer...</p>
</div>
</div>
}>
<DiffViewerContent />
</Suspense>
);
};
export default DiffViewerPage;