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:
144
services/frontend/src/components/SourceCard.tsx
Normal file
144
services/frontend/src/components/SourceCard.tsx
Normal file
@@ -0,0 +1,144 @@
|
||||
import React from 'react';
|
||||
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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user