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,149 @@
import React from 'react';
import { Link, NavLink, useNavigate } from 'react-router-dom';
import { GraduationCap, Search, BookOpen, Upload, LayoutDashboard, LogOut, User, BookMarked } from 'lucide-react';
import { clsx } from 'clsx';
import { useAuthStore } from '../store/auth';
interface LayoutProps {
children: React.ReactNode;
}
export function Layout({ children }: LayoutProps) {
const { isAuthenticated, user, logout } = useAuthStore();
const navigate = useNavigate();
const handleLogout = () => {
logout();
navigate('/');
};
return (
<div className="min-h-screen bg-gray-50">
{/* Навигация */}
<nav className="bg-white border-b border-gray-100 sticky top-0 z-50">
<div className="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex items-center justify-between h-16">
{/* Логотип */}
<Link
to="/"
className="flex items-center gap-2.5 text-gray-900 hover:text-brand-600 transition-colors"
>
<div className="p-1.5 bg-brand-600 rounded-lg">
<GraduationCap className="w-5 h-5 text-white" />
</div>
<span className="font-semibold text-base hidden sm:block">Академический помощник</span>
<span className="font-semibold text-base sm:hidden">АкадПомощник</span>
</Link>
{/* Центральная навигация */}
<div className="flex items-center gap-1">
<NavLink
to="/search"
className={({ isActive }) =>
clsx(
'flex items-center gap-1.5 px-3 py-2 rounded-lg text-sm font-medium transition-colors',
isActive
? 'bg-brand-50 text-brand-700'
: 'text-gray-600 hover:bg-gray-100'
)
}
>
<Search className="w-4 h-4" />
<span className="hidden md:block">Поиск</span>
</NavLink>
<NavLink
to="/check"
className={({ isActive }) =>
clsx(
'flex items-center gap-1.5 px-3 py-2 rounded-lg text-sm font-medium transition-colors',
isActive
? 'bg-brand-50 text-brand-700'
: 'text-gray-600 hover:bg-gray-100'
)
}
>
<Upload className="w-4 h-4" />
<span className="hidden md:block">Плагиат</span>
</NavLink>
<NavLink
to="/bibliography"
className={({ isActive }) =>
clsx(
'flex items-center gap-1.5 px-3 py-2 rounded-lg text-sm font-medium transition-colors',
isActive
? 'bg-brand-50 text-brand-700'
: 'text-gray-600 hover:bg-gray-100'
)
}
>
<BookOpen className="w-4 h-4" />
<span className="hidden md:block">Библиография</span>
</NavLink>
</div>
{/* Авторизация */}
<div className="flex items-center gap-2">
{isAuthenticated ? (
<>
<NavLink
to="/cabinet"
className={({ isActive }) =>
clsx(
'flex items-center gap-1.5 px-3 py-2 rounded-lg text-sm font-medium transition-colors',
isActive
? 'bg-brand-50 text-brand-700'
: 'text-gray-600 hover:bg-gray-100'
)
}
>
<LayoutDashboard className="w-4 h-4" />
<span className="hidden md:block">Кабинет</span>
</NavLink>
<div className="flex items-center gap-2 pl-2 border-l border-gray-100">
<div className="flex items-center gap-1.5">
<div className="w-8 h-8 bg-brand-100 rounded-full flex items-center justify-center">
<span className="text-xs font-semibold text-brand-700">
{user?.name?.[0]?.toUpperCase() || 'U'}
</span>
</div>
<span className="text-sm text-gray-600 hidden lg:block">{user?.name}</span>
</div>
<button
onClick={handleLogout}
className="p-1.5 text-gray-400 hover:text-gray-600 hover:bg-gray-100 rounded-lg transition-colors"
title="Выйти"
>
<LogOut className="w-4 h-4" />
</button>
</div>
</>
) : (
<>
<Link
to="/login"
className="px-4 py-2 text-sm font-medium text-gray-600 hover:text-gray-900 transition-colors"
>
Войти
</Link>
<Link
to="/register"
className="px-4 py-2 text-sm font-medium bg-brand-600 text-white rounded-lg hover:bg-brand-700 transition-colors"
>
Регистрация
</Link>
</>
)}
</div>
</div>
</div>
</nav>
{/* Основной контент */}
<main className="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
{children}
</main>
</div>
);
}