Spaces:
Running
Running
| import React, { useState, useEffect } from 'react'; | |
| import { X, Loader2, Search, CheckCircle, ArrowRight, MessageSquare, Briefcase, AlertTriangle } from 'lucide-react'; | |
| import { createPortal } from 'react-dom'; | |
| import { matchGrantsForProject, UserAnswer } from '../../api/client'; | |
| interface GrantMatchResult { | |
| program_id: string; | |
| program_name: string; | |
| score: number; | |
| rationale: string; | |
| is_recommended: boolean; | |
| requires_verification?: boolean; | |
| source?: string; | |
| legal_basis?: string; | |
| confidence_score?: number; | |
| } | |
| interface AdvancedMatcherModalProps { | |
| projectId: string; | |
| onClose: () => void; | |
| onMatchesSaved: (matches: GrantMatchResult[]) => void; | |
| } | |
| export default function AdvancedMatcherModal({ projectId, onClose, onMatchesSaved }: AdvancedMatcherModalProps) { | |
| const [step, setStep] = useState<'loading' | 'questions' | 'results'>('loading'); | |
| const [questions, setQuestions] = useState<string[]>([]); | |
| const [answers, setAnswers] = useState<Record<string, string>>({}); | |
| const [matches, setMatches] = useState<GrantMatchResult[]>([]); | |
| const [error, setError] = useState<string | null>(null); | |
| const [isSubmitting, setIsSubmitting] = useState(false); | |
| const [userAnswersHistory, setUserAnswersHistory] = useState<UserAnswer[]>([]); | |
| useEffect(() => { | |
| runMatcher([]); | |
| }, [projectId]); | |
| const runMatcher = async (currentAnswers: UserAnswer[]) => { | |
| setStep('loading'); | |
| setError(null); | |
| try { | |
| const data = await matchGrantsForProject(projectId, currentAnswers); | |
| if (data.status === 'error') { | |
| setError('Wyst膮pi艂 b艂膮d podczas analizy dopasowa艅.'); | |
| setStep('questions'); // fallback | |
| return; | |
| } | |
| if (data.needs_more_info && data.clarifying_questions && data.clarifying_questions.length > 0) { | |
| setQuestions(data.clarifying_questions); | |
| // Inicjalizuj puste odpowiedzi | |
| const initialAnswers: Record<string, string> = {}; | |
| data.clarifying_questions.forEach(q => { | |
| initialAnswers[q] = ''; | |
| }); | |
| setAnswers(initialAnswers); | |
| setStep('questions'); | |
| } else { | |
| setMatches(data.matches || []); | |
| onMatchesSaved(data.matches || []); | |
| setStep('results'); | |
| } | |
| } catch (err) { | |
| console.error('Error in matchGrantsForProject:', err); | |
| setError('B艂膮d po艂膮czenia z serwerem AI.'); | |
| setStep('questions'); | |
| } | |
| }; | |
| const handleAnswerSubmit = async () => { | |
| setIsSubmitting(true); | |
| // Zbierz nowe odpowiedzi | |
| const newAnswers: UserAnswer[] = questions.map(q => ({ | |
| question: q, | |
| answer: answers[q] || 'Brak odpowiedzi' | |
| })); | |
| const combinedAnswers = [...userAnswersHistory, ...newAnswers]; | |
| setUserAnswersHistory(combinedAnswers); | |
| await runMatcher(combinedAnswers); | |
| setIsSubmitting(false); | |
| }; | |
| const modalContent = ( | |
| <div style={{ | |
| position: 'fixed', top: 0, left: 0, right: 0, bottom: 0, | |
| backgroundColor: 'rgba(0, 0, 0, 0.75)', backdropFilter: 'blur(5px)', | |
| display: 'flex', alignItems: 'center', justifyContent: 'center', | |
| zIndex: 99999, | |
| padding: '2rem 1rem', // Add vertical padding to prevent cutting off | |
| boxSizing: 'border-box', | |
| overflowY: 'auto' | |
| }}> | |
| <div className="glass-card" style={{ | |
| width: '100%', maxWidth: '600px', maxHeight: '90vh', | |
| display: 'flex', flexDirection: 'column', | |
| overflow: 'hidden', padding: 0, margin: 'auto', | |
| boxSizing: 'border-box', | |
| transform: 'none', // Override any hover transforms from glass-card | |
| animation: 'none' | |
| }}> | |
| {/* Header */} | |
| <div style={{ | |
| padding: '1.5rem', borderBottom: '1px solid rgba(255,255,255,0.05)', | |
| display: 'flex', justifyContent: 'space-between', alignItems: 'center', | |
| background: 'rgba(255,255,255,0.02)' | |
| }}> | |
| <div style={{ display: 'flex', alignItems: 'center', gap: '0.75rem' }}> | |
| <div style={{ | |
| width: '36px', height: '36px', borderRadius: '8px', | |
| background: 'rgba(139, 92, 246, 0.1)', color: 'var(--accent-purple)', | |
| display: 'flex', alignItems: 'center', justifyContent: 'center' | |
| }}> | |
| <Search size={20} /> | |
| </div> | |
| <div> | |
| <h2 style={{ fontSize: '1.2rem', margin: 0, color: 'var(--text-primary)' }}>Advanced AI Matcher</h2> | |
| <p style={{ margin: 0, fontSize: '0.85rem', color: 'var(--text-muted)' }}>Wieloetapowe dopasowanie program贸w</p> | |
| </div> | |
| </div> | |
| <button onClick={onClose} style={{ | |
| background: 'none', border: 'none', color: 'var(--text-muted)', cursor: 'pointer', | |
| padding: '0.5rem' | |
| }} className="hover-lift"> | |
| <X size={20} /> | |
| </button> | |
| </div> | |
| {/* Content */} | |
| <div style={{ padding: '2rem', overflowY: 'auto', flex: 1 }}> | |
| {step === 'loading' && ( | |
| <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', padding: '3rem 0' }}> | |
| <Loader2 size={48} color="var(--accent-purple)" style={{ animation: 'spin 2s linear infinite', marginBottom: '1rem' }} /> | |
| <h3 style={{ color: 'var(--text-primary)', marginBottom: '0.5rem' }}>Analizuj臋 opis projektu...</h3> | |
| <p style={{ color: 'var(--text-muted)', textAlign: 'center', maxWidth: '400px' }}> | |
| AI skanuje aktualne nabory w PARP i NCBR, aby znale藕膰 najlepsze dopasowania lub przygotowa膰 pytania doprecyzowuj膮ce. | |
| </p> | |
| </div> | |
| )} | |
| {step === 'questions' && ( | |
| <div style={{ display: 'flex', flexDirection: 'column', gap: '1.5rem' }}> | |
| {error && ( | |
| <div style={{ padding: '1rem', background: 'rgba(239, 68, 68, 0.1)', borderLeft: '3px solid var(--accent-red)', borderRadius: '4px', color: '#FECACA', fontSize: '0.9rem' }}> | |
| {error} | |
| </div> | |
| )} | |
| <div style={{ display: 'flex', alignItems: 'flex-start', gap: '1rem', background: 'rgba(59, 130, 246, 0.1)', padding: '1.5rem', borderRadius: '12px', border: '1px solid rgba(59, 130, 246, 0.2)' }}> | |
| <MessageSquare size={24} color="var(--accent-blue)" style={{ flexShrink: 0 }} /> | |
| <div> | |
| <h4 style={{ margin: '0 0 0.5rem 0', color: 'var(--accent-blue)' }}>AI potrzebuje wi臋cej informacji</h4> | |
| <p style={{ margin: 0, fontSize: '0.9rem', color: 'var(--text-secondary)', lineHeight: 1.5 }}> | |
| Aby dok艂adnie dobra膰 programy dotacyjne, prosz臋 odpowiedz na poni偶sze pytania. Twoje odpowiedzi zostan膮 zapisane i wykorzystane podczas tworzenia wniosku. | |
| </p> | |
| </div> | |
| </div> | |
| <div style={{ display: 'flex', flexDirection: 'column', gap: '1.5rem' }}> | |
| {questions.map((q, idx) => ( | |
| <div key={idx} style={{ display: 'flex', flexDirection: 'column', gap: '0.5rem' }}> | |
| <label style={{ fontSize: '0.95rem', color: 'var(--text-primary)', fontWeight: 500 }}> | |
| {idx + 1}. {q} | |
| </label> | |
| <textarea | |
| value={answers[q] || ''} | |
| onChange={(e) => setAnswers(prev => ({ ...prev, [q]: e.target.value }))} | |
| placeholder="Twoja odpowied藕..." | |
| style={{ | |
| background: 'rgba(0,0,0,0.2)', border: '1px solid rgba(255,255,255,0.1)', | |
| borderRadius: '8px', padding: '0.75rem', color: '#fff', | |
| minHeight: '80px', resize: 'vertical', fontSize: '0.9rem', fontFamily: 'inherit' | |
| }} | |
| /> | |
| </div> | |
| ))} | |
| </div> | |
| <div style={{ display: 'flex', justifyContent: 'flex-end', marginTop: '1rem' }}> | |
| <button | |
| onClick={handleAnswerSubmit} | |
| disabled={isSubmitting} | |
| className="btn btn-primary" | |
| style={{ display: 'flex', alignItems: 'center', gap: '0.5rem' }} | |
| > | |
| {isSubmitting ? <Loader2 size={16} className="spin" /> : <ArrowRight size={16} />} | |
| Prze艣lij odpowiedzi | |
| </button> | |
| </div> | |
| </div> | |
| )} | |
| {step === 'results' && ( | |
| <div style={{ display: 'flex', flexDirection: 'column', gap: '1.5rem' }}> | |
| <div style={{ display: 'flex', alignItems: 'flex-start', gap: '1rem', background: 'rgba(16, 185, 129, 0.1)', padding: '1.5rem', borderRadius: '12px', border: '1px solid rgba(16, 185, 129, 0.2)' }}> | |
| <CheckCircle size={24} color="var(--accent-green)" style={{ flexShrink: 0 }} /> | |
| <div> | |
| <h4 style={{ margin: '0 0 0.5rem 0', color: 'var(--accent-green)' }}>Analiza zako艅czona sukcesem</h4> | |
| <p style={{ margin: 0, fontSize: '0.9rem', color: 'var(--text-secondary)', lineHeight: 1.5 }}> | |
| Znaleziono {matches.length} potencjalnych program贸w. Rekomendowane opcje zosta艂y oznaczone najwy偶szym wynikiem. | |
| </p> | |
| </div> | |
| </div> | |
| {matches.length === 0 ? ( | |
| <div style={{ padding: '2rem', textAlign: 'center', color: 'var(--text-muted)' }}> | |
| Brak pasuj膮cych program贸w dla podanych kryteri贸w. | |
| </div> | |
| ) : ( | |
| <div style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}> | |
| {matches.sort((a,b) => b.score - a.score).map((m, idx) => ( | |
| <div key={idx} style={{ | |
| background: 'rgba(255,255,255,0.03)', | |
| border: `1px solid ${m.is_recommended ? 'rgba(16, 185, 129, 0.3)' : 'rgba(255,255,255,0.1)'}`, | |
| borderRadius: '10px', padding: '1.25rem', | |
| display: 'flex', flexDirection: 'column', gap: '0.75rem' | |
| }}> | |
| <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start' }}> | |
| <h4 style={{ margin: 0, fontSize: '1.05rem', color: 'var(--text-primary)', display: 'flex', alignItems: 'center', gap: '0.5rem' }}> | |
| <Briefcase size={16} color="var(--text-muted)" /> | |
| {m.program_name} | |
| </h4> | |
| <div style={{ display: 'flex', gap: '0.5rem', flexWrap: 'wrap' }}> | |
| {m.requires_verification && ( | |
| <div style={{ | |
| background: 'rgba(245, 158, 11, 0.1)', | |
| color: '#F59E0B', | |
| padding: '0.25rem 0.75rem', borderRadius: '12px', fontSize: '0.85rem', fontWeight: 600, | |
| display: 'flex', alignItems: 'center', gap: '0.25rem' | |
| }}> | |
| <AlertTriangle size={14} /> Wymaga weryfikacji | |
| </div> | |
| )} | |
| <div style={{ | |
| background: m.score >= 70 ? 'rgba(16, 185, 129, 0.2)' : 'rgba(255,255,255,0.1)', | |
| color: m.score >= 70 ? 'var(--accent-green)' : 'var(--text-secondary)', | |
| padding: '0.25rem 0.75rem', borderRadius: '12px', fontSize: '0.85rem', fontWeight: 700 | |
| }}> | |
| {m.score}% Match | |
| </div> | |
| </div> | |
| </div> | |
| <p style={{ margin: 0, fontSize: '0.9rem', color: 'var(--text-secondary)', lineHeight: 1.5 }}> | |
| {m.rationale} | |
| </p> | |
| <div style={{ display: 'flex', gap: '1rem', marginTop: '0.5rem', fontSize: '0.8rem', color: 'var(--text-muted)' }}> | |
| {m.legal_basis && ( | |
| <div style={{ display: 'flex', alignItems: 'center', gap: '0.25rem' }}> | |
| <strong>Podstawa prawna:</strong> {m.legal_basis} | |
| </div> | |
| )} | |
| {m.source && ( | |
| <div style={{ display: 'flex', alignItems: 'center', gap: '0.25rem' }}> | |
| <strong>殴r贸d艂o:</strong> {m.source} | |
| </div> | |
| )} | |
| {m.confidence_score !== undefined && ( | |
| <div style={{ display: 'flex', alignItems: 'center', gap: '0.25rem' }}> | |
| <strong>Pewno艣膰 AI:</strong> {m.confidence_score}% | |
| </div> | |
| )} | |
| </div> | |
| </div> | |
| ))} | |
| </div> | |
| )} | |
| <div style={{ display: 'flex', justifyContent: 'flex-end', marginTop: '1rem' }}> | |
| <button onClick={onClose} className="btn btn-secondary"> | |
| Zamknij | |
| </button> | |
| </div> | |
| </div> | |
| )} | |
| </div> | |
| </div> | |
| <style>{` | |
| @keyframes spin { | |
| from { transform: rotate(0deg); } | |
| to { transform: rotate(360deg); } | |
| } | |
| .spin { animation: spin 1s linear infinite; } | |
| `}</style> | |
| </div> | |
| ); | |
| return createPortal(modalContent, document.body); | |
| } | |