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,189 @@
import React, { useState } from 'react';
import { useMutation } from '@tanstack/react-query';
import { BookOpen, Copy, Trash2, BookMarked } from 'lucide-react';
import toast from 'react-hot-toast';
import { GostCitation } from '../components/GostCitation';
import { useBibliographyStore } from '../store/bibliography';
import { useAuthStore } from '../store/auth';
import { api } from '../api/client';
import type { GostResultData } from '../types';
export function Bibliography() {
const { sources, removeSource, clearSources } = useBibliographyStore();
const { isAuthenticated } = useAuthStore();
const [style, setStyle] = useState<'7.1' | '7.0.5'>('7.1');
const [formattedResult, setFormattedResult] = useState<GostResultData | null>(null);
const formatMutation = useMutation({
mutationFn: async () => {
// Создать задачу GOST форматирования
const task = await api.post('/tasks/', {
type: 'gost',
input_data: {
doc_ids: sources.map((s) => s.id),
style,
},
});
const taskId = task.data.id;
// Диспатчить задачу
await api.post('/gost/format', {
task_id: taskId,
doc_ids: sources.map((s) => s.id),
style,
});
// Простое форматирование прямо на клиенте (используем ГОСТ-цитаты из источников)
return {
bibliography: sources.map((s, i) => ({
number: i + 1,
citation: s.gost_citation,
doc_id: s.id,
})),
style,
total: sources.length,
} as GostResultData;
},
onSuccess: (data) => {
setFormattedResult(data);
toast.success('Библиография отформатирована');
},
onError: () => {
// При ошибке используем локальные ГОСТ-цитаты
setFormattedResult({
bibliography: sources.map((s, i) => ({
number: i + 1,
citation: s.gost_citation,
doc_id: s.id,
})),
style,
total: sources.length,
});
},
});
const handleFormat = () => {
if (sources.length === 0) {
toast.error('Добавьте источники в библиографию');
return;
}
formatMutation.mutate();
};
const handleCopyAll = () => {
if (!formattedResult) return;
const text = formattedResult.bibliography
.map((e) => `${e.number}. ${e.citation}`)
.join('\n');
navigator.clipboard.writeText(text).then(() => {
toast.success('Список литературы скопирован');
});
};
return (
<div className="max-w-3xl mx-auto space-y-6">
<div>
<h1 className="text-2xl font-bold text-gray-900 mb-2">Список литературы</h1>
<p className="text-gray-500">
Добавляйте источники из результатов поиска и форматируйте библиографию по ГОСТ.
</p>
</div>
{/* Источники в списке */}
{sources.length > 0 ? (
<div className="bg-white rounded-xl border border-gray-100 overflow-hidden">
<div className="flex items-center justify-between px-5 py-3 border-b border-gray-100">
<span className="text-sm font-medium text-gray-700">
Источников: {sources.length}
</span>
<button
onClick={clearSources}
className="text-xs text-red-500 hover:text-red-700 flex items-center gap-1"
>
<Trash2 className="w-3 h-3" />
Очистить всё
</button>
</div>
<div className="divide-y divide-gray-100">
{sources.map((source) => (
<div key={source.id} className="flex items-start gap-3 px-5 py-3">
<div className="flex-1 min-w-0">
<p className="text-sm font-medium text-gray-800 truncate">{source.title}</p>
<p className="text-xs text-gray-400 mt-0.5">
{source.authors.slice(0, 2).map((a) => a.last_name).join(', ')}
{source.year && ` · ${source.year}`}
</p>
</div>
<button
onClick={() => removeSource(source.id)}
className="p-1 text-gray-300 hover:text-red-400 transition-colors flex-shrink-0"
>
<Trash2 className="w-4 h-4" />
</button>
</div>
))}
</div>
</div>
) : (
<div className="bg-gray-50 rounded-xl border border-dashed border-gray-200 p-10 text-center">
<BookMarked className="w-10 h-10 text-gray-300 mx-auto mb-3" />
<p className="text-gray-400 text-sm">
Нажмите «В библиографию» на любом источнике из результатов поиска
</p>
</div>
)}
{/* Настройки форматирования */}
{sources.length > 0 && (
<div className="flex items-center gap-4">
<div>
<label className="text-sm text-gray-500 mr-2">Стиль ГОСТ:</label>
<select
value={style}
onChange={(e) => {
setStyle(e.target.value as '7.1' | '7.0.5');
setFormattedResult(null);
}}
className="text-sm border border-gray-200 rounded-lg px-3 py-1.5 focus:outline-none focus:ring-2 focus:ring-brand-500"
>
<option value="7.1">ГОСТ 7.1-2003 (полная)</option>
<option value="7.0.5">ГОСТ Р 7.0.5-2008 (краткая)</option>
</select>
</div>
<button
onClick={handleFormat}
disabled={formatMutation.isPending}
className="flex items-center gap-2 px-5 py-2.5 bg-brand-600 text-white rounded-lg text-sm font-medium hover:bg-brand-700 disabled:opacity-50 transition-colors ml-auto"
>
<BookOpen className="w-4 h-4" />
{formatMutation.isPending ? 'Форматирование...' : 'Сформировать список'}
</button>
</div>
)}
{/* Результат */}
{formattedResult && (
<div className="bg-white rounded-xl border border-gray-100 overflow-hidden">
<div className="flex items-center justify-between px-5 py-3 border-b border-gray-100">
<span className="text-sm font-medium text-gray-700">
Список литературы · ГОСТ {style}
</span>
<button
onClick={handleCopyAll}
className="flex items-center gap-1.5 text-sm text-brand-600 hover:text-brand-700"
>
<Copy className="w-4 h-4" />
Копировать всё
</button>
</div>
<div className="px-2 py-2 divide-y divide-gray-50">
{formattedResult.bibliography.map((entry) => (
<GostCitation key={entry.doc_id} citation={entry.citation} number={entry.number} />
))}
</div>
</div>
)}
</div>
);
}