code-review-agent/frontend/src/pages/ReviewDetail.tsx
Primakov Alexandr Alexandrovich 09cdd06307 init
2025-10-12 23:15:09 +03:00

165 lines
5.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useParams, useNavigate } from 'react-router-dom';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { useEffect } from 'react';
import { getReview, retryReview } from '../api/client';
import { wsClient } from '../api/websocket';
import ReviewProgress from '../components/ReviewProgress';
import CommentsList from '../components/CommentsList';
import { formatDistance } from 'date-fns';
import { ru } from 'date-fns/locale';
export default function ReviewDetail() {
const { id } = useParams<{ id: string }>();
const navigate = useNavigate();
const queryClient = useQueryClient();
const { data: review, isLoading } = useQuery({
queryKey: ['review', id],
queryFn: () => getReview(Number(id)),
refetchInterval: (data) => {
// Refetch if review is in progress
return data?.status && ['pending', 'fetching', 'analyzing', 'commenting'].includes(data.status)
? 5000
: false;
},
});
const retryMutation = useMutation({
mutationFn: () => retryReview(Number(id)),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['review', id] });
},
});
// Listen to WebSocket updates
useEffect(() => {
const unsubscribe = wsClient.on('review_progress', (data: any) => {
if (data.review_id === Number(id)) {
queryClient.invalidateQueries({ queryKey: ['review', id] });
}
});
return () => {
unsubscribe();
};
}, [id, queryClient]);
if (isLoading) {
return (
<div className="flex items-center justify-center min-h-96">
<div className="text-gray-400">Загрузка...</div>
</div>
);
}
if (!review) {
return (
<div className="flex items-center justify-center min-h-96">
<div className="text-gray-400">Ревью не найдено</div>
</div>
);
}
return (
<div className="space-y-8">
<div className="flex items-center gap-4">
<button
onClick={() => navigate('/reviews')}
className="text-gray-400 hover:text-white transition-colors"
>
Назад
</button>
<div className="flex-1">
<h1 className="text-3xl font-bold text-white mb-2">
Ревью #{review.id}
</h1>
<p className="text-gray-400">
PR #{review.pull_request.pr_number}: {review.pull_request.title}
</p>
</div>
</div>
{/* PR Info */}
<div className="bg-gray-800 rounded-lg p-6 border border-gray-700">
<h2 className="text-xl font-semibold text-white mb-4">Информация о Pull Request</h2>
<div className="space-y-3">
<div className="flex items-center gap-3">
<span className="text-gray-400 w-32">Автор:</span>
<span className="text-white">{review.pull_request.author}</span>
</div>
<div className="flex items-center gap-3">
<span className="text-gray-400 w-32">Ветки:</span>
<span className="text-white">
{review.pull_request.source_branch} {review.pull_request.target_branch}
</span>
</div>
<div className="flex items-center gap-3">
<span className="text-gray-400 w-32">URL:</span>
<a
href={review.pull_request.url}
target="_blank"
rel="noopener noreferrer"
className="text-blue-400 hover:text-blue-300"
>
{review.pull_request.url}
</a>
</div>
<div className="flex items-center gap-3">
<span className="text-gray-400 w-32">Начато:</span>
<span className="text-white">
{formatDistance(new Date(review.started_at), new Date(), { addSuffix: true, locale: ru })}
</span>
</div>
{review.completed_at && (
<div className="flex items-center gap-3">
<span className="text-gray-400 w-32">Завершено:</span>
<span className="text-white">
{formatDistance(new Date(review.completed_at), new Date(), { addSuffix: true, locale: ru })}
</span>
</div>
)}
</div>
</div>
{/* Progress */}
<div className="bg-gray-800 rounded-lg p-6 border border-gray-700">
<h2 className="text-xl font-semibold text-white mb-4">Прогресс</h2>
<ReviewProgress
status={review.status}
filesAnalyzed={review.files_analyzed}
commentsGenerated={review.comments_generated}
/>
{review.status === 'failed' && (
<div className="mt-6">
<div className="p-4 bg-red-900/20 border border-red-800 rounded text-sm text-red-300 mb-4">
Ошибка: {review.error_message}
</div>
<button
onClick={() => retryMutation.mutate()}
disabled={retryMutation.isPending}
className="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg transition-colors disabled:opacity-50"
>
{retryMutation.isPending ? 'Перезапуск...' : 'Повторить ревью'}
</button>
</div>
)}
</div>
{/* Comments */}
<div className="bg-gray-800 rounded-lg p-6 border border-gray-700">
<h2 className="text-xl font-semibold text-white mb-4">
Комментарии ({review.comments?.length || 0})
</h2>
<CommentsList comments={review.comments || []} />
</div>
</div>
);
}