feat: initial microservices project structure

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>
This commit is contained in:
jze9
2026-05-24 19:42:39 +05:00
commit 7758315632
120 changed files with 9500 additions and 0 deletions

View File

@@ -0,0 +1,137 @@
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>
);
}