519 lines
18 KiB
TypeScript
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;
|