grantforge-api / frontend-react /src /components /project /MatchingGrantsWidget.tsx
GrantForge Bot
Deploy to Hugging Face
afd56bc
import React, { useState } from 'react';
import { Briefcase, Zap, AlertCircle } from 'lucide-react';
import AdvancedMatcherModal from './AdvancedMatcherModal';
import { updateProject } from '../../api/client';
import toast from 'react-hot-toast';
interface GrantMatchResult {
program_id: string;
program_name: string;
score: number;
rationale: string;
is_recommended: boolean;
}
interface Props {
projectId: string;
projectContext?: any;
onUpdateContext?: (matches: GrantMatchResult[]) => void;
}
export default function MatchingGrantsWidget({ projectId, projectContext, onUpdateContext }: Props) {
const [isModalOpen, setIsModalOpen] = useState(false);
// Pobierz zapisane wyniki z kontekstu
const savedMatches: GrantMatchResult[] = projectContext?.ai_matches || [];
// Posortuj od najwyższego wyniku i weź 3 najlepsze
const displayMatches = [...savedMatches].sort((a, b) => b.score - a.score).slice(0, 3);
const handleMatchesSaved = async (matches: GrantMatchResult[]) => {
try {
const newContext = { ...projectContext, ai_matches: matches };
await updateProject(projectId, { external_context: newContext });
if (onUpdateContext) {
onUpdateContext(matches);
}
setIsModalOpen(false);
toast.success("Zapisano dopasowane programy");
} catch (err) {
console.error("Failed to save matches", err);
toast.error("Wystąpił błąd podczas zapisywania");
}
};
return (
<div style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}>
{displayMatches.length > 0 ? (
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.6rem' }}>
{displayMatches.map((m, idx) => (
<div
key={idx}
style={{
background: 'rgba(255,255,255,0.025)',
border: `1px solid ${m.is_recommended ? 'rgba(16, 185, 129, 0.3)' : 'rgba(255,255,255,0.06)'}`,
borderRadius: 10,
padding: '0.65rem 0.75rem',
display: 'flex', flexDirection: 'column', gap: '0.4rem'
}}
>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', gap: 6 }}>
<div style={{ fontSize: '0.8rem', fontWeight: 700, color: 'var(--text-primary)', lineHeight: 1.3 }}>
{m.program_name}
</div>
<div style={{
background: m.score >= 70 ? 'rgba(16, 185, 129, 0.1)' : 'rgba(255,255,255,0.05)',
color: m.score >= 70 ? 'var(--accent-green)' : 'var(--text-secondary)',
padding: '2px 6px', borderRadius: '4px', fontSize: '0.7rem', fontWeight: 700, flexShrink: 0
}}>
{m.score}% Match
</div>
</div>
<div style={{ fontSize: '0.75rem', color: 'var(--text-secondary)', lineHeight: 1.4, overflow: 'hidden', display: '-webkit-box', WebkitLineClamp: 2, WebkitBoxOrient: 'vertical' }}>
{m.rationale}
</div>
</div>
))}
</div>
) : (
<div style={{
padding: '1rem', textAlign: 'center', background: 'rgba(255,255,255,0.02)',
border: '1px dashed rgba(255,255,255,0.1)', borderRadius: '8px',
display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '0.5rem'
}}>
<AlertCircle size={20} color="var(--text-muted)" />
<span style={{ color: 'var(--text-muted)', fontSize: '0.85rem' }}>
Brak zapisanych rekomendacji. Uruchom AI Matcher, aby znaleźć najlepsze programy.
</span>
</div>
)}
<button
onClick={() => setIsModalOpen(true)}
className="btn"
style={{
background: 'linear-gradient(to right, rgba(139, 92, 246, 0.15), rgba(59, 130, 246, 0.15))',
border: '1px solid rgba(139,92,246,0.3)',
borderRadius: 8,
padding: '0.5rem 0.75rem',
color: '#c4b5fd',
cursor: 'pointer',
fontSize: '0.8rem',
fontWeight: 600,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
gap: 6,
transition: 'all 0.2s',
marginTop: 4,
}}
onMouseEnter={e => {
e.currentTarget.style.background = 'linear-gradient(to right, rgba(139, 92, 246, 0.25), rgba(59, 130, 246, 0.25))';
e.currentTarget.style.borderColor = 'rgba(139,92,246,0.5)';
}}
onMouseLeave={e => {
e.currentTarget.style.background = 'linear-gradient(to right, rgba(139, 92, 246, 0.15), rgba(59, 130, 246, 0.15))';
e.currentTarget.style.borderColor = 'rgba(139,92,246,0.3)';
}}
>
<Zap size={14} fill="currentColor" /> Uruchom Zaawansowany Matcher AI
</button>
{isModalOpen && (
<AdvancedMatcherModal
projectId={projectId}
onClose={() => setIsModalOpen(false)}
onMatchesSaved={handleMatchesSaved}
/>
)}
</div>
);
}