Files
anti-plagiarism/services/frontend/src/pages/Task.tsx
jze9 673e72a15a feat(infra): переход прод/test на общую инфраструктуру вместо self-hosted
Postgres/Redis/RabbitMQ/MinIO/Ollama теперь общие серверы сети (адреса в .env),
локально в докере остаётся только Elasticsearch + app-сервисы. Старый
docker-compose.yml (полностью автономный стек) сохранён как
docker-compose.selfhosted.yml.example на случай отдельного GPU-сервера в
будущем. Добавлен docker-compose.prod.yml (app + свой nginx с TLS,
собирающий фронтенд в статику). Makefile переписан под три режима
(dev/test-up/prod). Мелкая чистка неиспользуемых импортов во фронтенде.
2026-07-27 12:22:36 +05:00

140 lines
5.7 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, Navigate } from 'react-router-dom';
import { useQuery } from '@tanstack/react-query';
import { Loader2, ArrowLeft } from 'lucide-react';
import { Link } from 'react-router-dom';
import { StatusBadge } from '../components/StatusBadge';
import { SourceCard } from '../components/SourceCard';
import { PlagiarismReport } from '../components/PlagiarismReport';
import { GostCitation } from '../components/GostCitation';
import { tasksApi } from '../api/client';
import { useAuthStore } from '../store/auth';
import { useTaskWebSocket } from '../hooks/useTaskPolling';
import type { Task as TaskType, SearchResultData, PlagiarismResultData, GostResultData } from '../types';
export function Task() {
const { taskId } = useParams<{ taskId: string }>();
const { isAuthenticated } = useAuthStore();
if (!isAuthenticated) return <Navigate to="/login" replace />;
if (!taskId) return <Navigate to="/cabinet" replace />;
const { data: task, isLoading } = useQuery({
queryKey: ['task', taskId],
queryFn: () => tasksApi.get(taskId).then((r) => r.data as TaskType),
refetchInterval: (query) => {
const status = query.state.data?.status;
return (status === 'done' || status === 'failed') ? false : 3000;
},
});
useTaskWebSocket(taskId, !!task && task.status !== 'done' && task.status !== 'failed');
if (isLoading) {
return (
<div className="flex items-center justify-center py-16">
<Loader2 className="w-8 h-8 text-brand-500 animate-spin" />
</div>
);
}
if (!task) {
return <Navigate to="/cabinet" replace />;
}
return (
<div className="space-y-6">
{/* Шапка */}
<div className="flex items-center gap-3">
<Link to="/cabinet" className="p-2 rounded-lg text-gray-400 hover:text-gray-600 hover:bg-gray-100 transition-colors">
<ArrowLeft className="w-5 h-5" />
</Link>
<div>
<h1 className="text-xl font-semibold text-gray-900 capitalize">
{{
search: 'Поиск источников',
plagiarism: 'Проверка плагиата',
gost: 'ГОСТ библиография',
summarize: 'Краткое изложение',
}[task.type] || task.type}
</h1>
<div className="flex items-center gap-2 mt-0.5">
<StatusBadge status={task.status} />
<span className="text-xs text-gray-400">{task.public_id}</span>
</div>
</div>
</div>
{/* В процессе */}
{(task.status === 'queued' || task.status === 'processing') && (
<div className="bg-white rounded-xl border border-gray-100 p-10 text-center">
<Loader2 className="w-10 h-10 text-brand-400 animate-spin mx-auto mb-3" />
<p className="text-base font-medium text-gray-700">
{task.status === 'queued' ? 'Задача в очереди...' : 'Выполняется...'}
</p>
{task.queue_position && (
<p className="text-sm text-gray-400 mt-1">Позиция: {task.queue_position}</p>
)}
<p className="text-sm text-gray-400 mt-1">Вы получите email когда задача завершится</p>
</div>
)}
{/* Ошибка */}
{task.status === 'failed' && (
<div className="bg-red-50 border border-red-200 rounded-xl p-6">
<p className="font-medium text-red-700 mb-1">Задача завершилась с ошибкой</p>
<p className="text-sm text-red-500">{task.error}</p>
</div>
)}
{/* Результаты поиска */}
{task.status === 'done' && task.type === 'search' && task.result && (
<div className="space-y-4">
<p className="text-sm text-gray-500">
Найдено {(task.result as SearchResultData).total} источников
</p>
{(task.result as SearchResultData).sources.map((source) => (
<SourceCard key={source.id} source={source} />
))}
</div>
)}
{/* Отчёт о плагиате */}
{task.status === 'done' && task.type === 'plagiarism' && task.result && (
<div className="bg-white rounded-xl border border-gray-100 p-6">
<h2 className="text-base font-semibold text-gray-900 mb-4">
Результат проверки плагиата
</h2>
<PlagiarismReport data={task.result as PlagiarismResultData} />
</div>
)}
{/* ГОСТ библиография */}
{task.status === 'done' && task.type === 'gost' && task.result && (
<div className="bg-white rounded-xl border border-gray-100 p-6">
<div className="flex items-center justify-between mb-4">
<h2 className="text-base font-semibold text-gray-900">
Список литературы (ГОСТ {(task.result as GostResultData).style}-2003)
</h2>
<button
onClick={() => {
const text = (task.result as GostResultData).bibliography
.map((e) => `${e.number}. ${e.citation}`)
.join('\n');
navigator.clipboard.writeText(text);
}}
className="px-3 py-1.5 text-xs font-medium text-brand-600 border border-brand-200 rounded-lg hover:bg-brand-50 transition-colors"
>
Копировать всё
</button>
</div>
<div className="divide-y divide-gray-100">
{(task.result as GostResultData).bibliography.map((entry) => (
<GostCitation key={entry.doc_id} citation={entry.citation} number={entry.number} />
))}
</div>
</div>
)}
</div>
);
}