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:
189
services/frontend/src/pages/Bibliography.tsx
Normal file
189
services/frontend/src/pages/Bibliography.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user