Files
anti-plagiarism/services/frontend/src/components/SourceCard.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

144 lines
5.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 { ExternalLink, BookmarkPlus, BookmarkCheck, Copy } from 'lucide-react';
import { clsx } from 'clsx';
import toast from 'react-hot-toast';
import { useBibliographyStore } from '../store/bibliography';
import type { SearchSource } from '../types';
interface SourceCardProps {
source: SearchSource;
}
const SOURCE_DB_LABELS: Record<string, string> = {
openalex: 'OpenAlex',
cyberleninka: 'КиберЛенинка',
arxiv: 'arXiv',
wikipedia_ru: 'Wikipedia RU',
wikipedia_en: 'Wikipedia EN',
};
function getRelevanceColor(score: number): string {
if (score >= 0.7) return 'text-green-700 bg-green-50 border-green-200';
if (score >= 0.4) return 'text-yellow-700 bg-yellow-50 border-yellow-200';
return 'text-gray-600 bg-gray-50 border-gray-200';
}
export function SourceCard({ source }: SourceCardProps) {
const { addSource, removeSource, hasSource } = useBibliographyStore();
const inBibliography = hasSource(source.id);
const handleToggleBibliography = () => {
if (inBibliography) {
removeSource(source.id);
toast.success('Удалено из библиографии');
} else {
addSource(source);
toast.success('Добавлено в библиографию');
}
};
const handleCopyCitation = () => {
navigator.clipboard.writeText(source.gost_citation).then(() => {
toast.success('ГОСТ-цитата скопирована');
});
};
const authorsStr = source.authors
.slice(0, 3)
.map((a) => `${a.last_name} ${a.initials || ''}`.trim())
.join(', ');
return (
<div className="bg-white rounded-xl border border-gray-100 p-5 hover:border-gray-200 hover:shadow-sm transition-all duration-200">
{/* Заголовок и значки */}
<div className="flex items-start justify-between gap-3 mb-3">
<div className="flex-1 min-w-0">
<h3 className="text-base font-semibold text-gray-900 leading-snug line-clamp-2 mb-1">
{source.title}
</h3>
<div className="flex items-center gap-2 flex-wrap">
{authorsStr && (
<span className="text-sm text-gray-500">{authorsStr}</span>
)}
{source.year && (
<span className="text-sm text-gray-400">{source.year}</span>
)}
{source.journal && (
<span className="text-sm text-gray-400 italic">{source.journal}</span>
)}
</div>
</div>
{/* Бейдж релевантности */}
<div className={clsx(
'flex-shrink-0 px-2 py-1 rounded-lg text-xs font-medium border',
getRelevanceColor(source.relevance_score)
)}>
{(source.relevance_score * 100).toFixed(0)}%
</div>
</div>
{/* Аннотация */}
{source.abstract && (
<p className="text-sm text-gray-600 line-clamp-3 mb-3">
{source.abstract}
</p>
)}
{/* ГОСТ-цитата */}
<div className="bg-gray-50 rounded-lg p-3 mb-3">
<p className="text-xs text-gray-500 mb-1 font-medium">ГОСТ-цитата:</p>
<p className="text-xs text-gray-700 leading-relaxed">{source.gost_citation}</p>
</div>
{/* Действия */}
<div className="flex items-center gap-2 flex-wrap">
{/* Источник */}
<span className="px-2 py-1 bg-gray-100 text-gray-500 text-xs rounded-md">
{SOURCE_DB_LABELS[source.source_db] || source.source_db}
</span>
<div className="ml-auto flex items-center gap-1.5">
{/* Открыть источник */}
{source.url && (
<a
href={source.url}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-1 px-3 py-1.5 text-xs font-medium text-gray-600 bg-white border border-gray-200 rounded-lg hover:bg-gray-50 transition-colors"
>
<ExternalLink className="w-3 h-3" />
Открыть
</a>
)}
{/* Копировать цитату */}
<button
onClick={handleCopyCitation}
className="inline-flex items-center gap-1 px-3 py-1.5 text-xs font-medium text-gray-600 bg-white border border-gray-200 rounded-lg hover:bg-gray-50 transition-colors"
>
<Copy className="w-3 h-3" />
Цитата
</button>
{/* В библиографию */}
<button
onClick={handleToggleBibliography}
className={clsx(
'inline-flex items-center gap-1 px-3 py-1.5 text-xs font-medium rounded-lg transition-colors',
inBibliography
? 'bg-brand-600 text-white hover:bg-brand-700'
: 'bg-white text-brand-600 border border-brand-200 hover:bg-brand-50'
)}
>
{inBibliography ? (
<><BookmarkCheck className="w-3 h-3" />Добавлено</>
) : (
<><BookmarkPlus className="w-3 h-3" />В библиографию</>
)}
</button>
</div>
</div>
</div>
);
}