Services: - api: FastAPI gateway with JWT auth, async endpoints, WebSocket - worker-gpu: CUDA sentence-transformers, FAISS IVFFlat, Ollama LLM - worker-indexer: Winnowing+MinHash plagiarism detection, PDF/DOCX extraction - worker-notifier: SMTP email notifications - worker-gost: GOST 7.1-2003 and GOST R 7.0.5-2008 formatting Infrastructure: - docker-compose.yml (production) + docker-compose.dev.yml (hot reload) - Nginx reverse proxy + WebSocket support - PostgreSQL 16 with Alembic migrations - Elasticsearch 8 with Russian/English analyzers - MinIO, RabbitMQ, Redis, Ollama Frontend: - React 18 + Vite + TypeScript + TailwindCSS + Zustand + React Query v5 - 9 pages: Home, Search, Cabinet, Task, Check, Bibliography, Pricing, Login, Register Scripts: - Parser stubs: OpenAlex, КиберЛенинка, arXiv (Phase 0 - to be filled) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
138 lines
5.6 KiB
TypeScript
138 lines
5.6 KiB
TypeScript
import React, { useState } from 'react';
|
||
import { useNavigate } from 'react-router-dom';
|
||
import { useMutation } from '@tanstack/react-query';
|
||
import { Link } from 'react-router-dom';
|
||
import { Shield, LayoutDashboard } from 'lucide-react';
|
||
import toast from 'react-hot-toast';
|
||
import { DropZone } from '../components/DropZone';
|
||
import { documentsApi } from '../api/client';
|
||
import { useAuthStore } from '../store/auth';
|
||
import type { Task } from '../types';
|
||
|
||
export function Check() {
|
||
const { isAuthenticated } = useAuthStore();
|
||
const navigate = useNavigate();
|
||
const [file, setFile] = useState<File | null>(null);
|
||
const [submittedTask, setSubmittedTask] = useState<Task | null>(null);
|
||
|
||
const upload = useMutation({
|
||
mutationFn: () => documentsApi.uploadForCheck(file!),
|
||
onSuccess: (response) => {
|
||
const task: Task = response.data;
|
||
setSubmittedTask(task);
|
||
toast.success('Файл загружен, проверка начата!');
|
||
},
|
||
onError: (error: any) => {
|
||
const msg = error.response?.data?.detail || 'Ошибка загрузки';
|
||
if (error.response?.status === 401) {
|
||
toast.error('Войдите для проверки плагиата');
|
||
navigate('/login');
|
||
} else {
|
||
toast.error(msg);
|
||
}
|
||
},
|
||
});
|
||
|
||
const handleSubmit = (e: React.FormEvent) => {
|
||
e.preventDefault();
|
||
if (!file) return;
|
||
if (!isAuthenticated) {
|
||
toast.error('Необходима авторизация');
|
||
navigate('/login');
|
||
return;
|
||
}
|
||
upload.mutate();
|
||
};
|
||
|
||
if (submittedTask) {
|
||
return (
|
||
<div className="max-w-xl mx-auto pt-8">
|
||
<div className="bg-white rounded-2xl border border-gray-100 p-8 text-center">
|
||
<div className="w-16 h-16 bg-green-50 rounded-2xl flex items-center justify-center mx-auto mb-4">
|
||
<Shield className="w-8 h-8 text-green-500" />
|
||
</div>
|
||
<h2 className="text-xl font-semibold text-gray-900 mb-2">Проверка запущена</h2>
|
||
<p className="text-gray-500 mb-2">
|
||
Файл <strong>{(submittedTask.input_data as any).filename}</strong> передан на проверку.
|
||
</p>
|
||
<p className="text-sm text-gray-400 mb-6">
|
||
Вы получите email когда проверка завершится. Обычно это занимает 1-3 минуты.
|
||
</p>
|
||
<div className="flex flex-col sm:flex-row gap-3 justify-center">
|
||
<Link
|
||
to={`/tasks/${submittedTask.id}`}
|
||
className="flex items-center justify-center gap-2 px-5 py-2.5 bg-brand-600 text-white rounded-lg text-sm font-medium hover:bg-brand-700 transition-colors"
|
||
>
|
||
Следить за прогрессом
|
||
</Link>
|
||
<Link
|
||
to="/cabinet"
|
||
className="flex items-center justify-center gap-2 px-5 py-2.5 border border-gray-200 text-gray-600 rounded-lg text-sm font-medium hover:bg-gray-50 transition-colors"
|
||
>
|
||
<LayoutDashboard className="w-4 h-4" />
|
||
Личный кабинет
|
||
</Link>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
return (
|
||
<div className="max-w-2xl mx-auto">
|
||
<div className="mb-8">
|
||
<h1 className="text-2xl font-bold text-gray-900 mb-2">Проверка плагиата</h1>
|
||
<p className="text-gray-500">
|
||
Загрузите документ для проверки на 4 уровнях: точные совпадения, нечёткий поиск,
|
||
семантика и LLM-анализ парафраза.
|
||
</p>
|
||
</div>
|
||
|
||
<form onSubmit={handleSubmit} className="space-y-6">
|
||
<DropZone
|
||
onFile={setFile}
|
||
file={file}
|
||
onClear={() => setFile(null)}
|
||
/>
|
||
|
||
{!isAuthenticated && (
|
||
<div className="bg-amber-50 border border-amber-200 rounded-xl p-4 text-sm text-amber-700">
|
||
<Link to="/login" className="underline font-medium">Войдите</Link> или{' '}
|
||
<Link to="/register" className="underline font-medium">зарегистрируйтесь</Link>{' '}
|
||
для проверки плагиата
|
||
</div>
|
||
)}
|
||
|
||
<button
|
||
type="submit"
|
||
disabled={!file || upload.isPending || !isAuthenticated}
|
||
className="w-full flex items-center justify-center gap-2 py-3 bg-brand-600 text-white rounded-xl font-medium hover:bg-brand-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
||
>
|
||
{upload.isPending ? (
|
||
<>
|
||
<Shield className="w-5 h-5 animate-pulse" />
|
||
Загрузка...
|
||
</>
|
||
) : (
|
||
<>
|
||
<Shield className="w-5 h-5" />
|
||
Проверить на плагиат
|
||
</>
|
||
)}
|
||
</button>
|
||
</form>
|
||
|
||
{/* Описание уровней */}
|
||
<div className="mt-8 bg-gray-50 rounded-xl p-5">
|
||
<h3 className="text-sm font-semibold text-gray-700 mb-3">4 уровня проверки:</h3>
|
||
<div className="space-y-2 text-sm text-gray-500">
|
||
<div>1. <strong>Winnowing + MinHash</strong> — точные и нечёткие совпадения (~мс)</div>
|
||
<div>2. <strong>n-граммы + Jaccard</strong> — перестановки слов (~сек)</div>
|
||
<div>3. <strong>FAISS GPU cosine</strong> — семантическая близость (~мс)</div>
|
||
<div>4. <strong>Ollama Llama3</strong> — анализ парафраза (~2 сек)</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|