Files
anti-plagiarism/services/frontend/src/pages/Bibliography.tsx
jze9 673e72a15a feat(infra): переход прод/test на общую инфраструктуру вместо self-hosted
Postgres/Redis/RabbitMQ/MinIO/Ollama теперь общие серверы сети (адреса в .env),
локально в докере остаётся только Elasticsearch + app-сервисы. Старый
docker-compose.yml (полностью автономный стек) сохранён как
docker-compose.selfhosted.yml.example на случай отдельного GPU-сервера в
будущем. Добавлен docker-compose.prod.yml (app + свой nginx с TLS,
собирающий фронтенд в статику). Makefile переписан под три режима
(dev/test-up/prod). Мелкая чистка неиспользуемых импортов во фронтенде.
2026-07-27 12:22:36 +05:00

188 lines
7.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { 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 { api } from '../api/client';
import type { GostResultData } from '../types';
export function Bibliography() {
const { sources, removeSource, clearSources } = useBibliographyStore();
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>
);
}