Spaces:
Running
Running
| import React, { useState, useEffect } from 'react'; | |
| import { createPortal } from 'react-dom'; | |
| import { motion, AnimatePresence } from 'framer-motion'; | |
| import { X, Search, Sparkles, Building, ChevronRight, Loader2, CheckCircle, Cpu, Tractor, HardHat, Info } from 'lucide-react'; | |
| import { useNavigate } from 'react-router-dom'; | |
| import { useAuth } from '@clerk/clerk-react'; | |
| import { createProject, lookupCompany, matchProgram } from '../../api/client'; | |
| import toast from 'react-hot-toast'; | |
| interface Props { | |
| onClose: () => void; | |
| } | |
| const WizardModal: React.FC<Props> = ({ onClose }) => { | |
| const navigate = useNavigate(); | |
| const { userId } = useAuth(); | |
| const [step, setStep] = useState(1); | |
| const [nip, setNip] = useState(''); | |
| const [desc, setDesc] = useState(''); | |
| const [isSearching, setIsSearching] = useState(false); | |
| const [companyFound, setCompanyFound] = useState(false); | |
| const [analyzing, setAnalyzing] = useState(false); | |
| const [analysisLogs, setAnalysisLogs] = useState<string[]>([]); | |
| const [selectedProgram, setSelectedProgram] = useState<number | null>(1); | |
| const [expandedProgram, setExpandedProgram] = useState<number | null>(null); | |
| const [companyDetails, setCompanyDetails] = useState<{name: string, status: string, nip?: string} | null>(null); | |
| const [recommendedPrograms, setRecommendedPrograms] = useState<any[]>([]); | |
| const [manualEntry, setManualEntry] = useState(false); | |
| const [clarifyingQuestions, setClarifyingQuestions] = useState<string[]>([]); | |
| const [clarificationAnswers, setClarificationAnswers] = useState<string[]>([]); | |
| const [clarificationPanelOpen, setClarificationPanelOpen] = useState(false); | |
| const handleSearchCompany = async () => { | |
| const cleanedNip = nip.replace(/\D/g, ''); | |
| if (cleanedNip.length !== 10) { | |
| toast.error("NIP musi składać się dokładnie z 10 cyfr."); | |
| return; | |
| } | |
| setIsSearching(true); | |
| try { | |
| const data = await lookupCompany(cleanedNip); | |
| setCompanyDetails({ name: data.name, status: data.status, nip: cleanedNip }); | |
| setCompanyFound(true); | |
| setManualEntry(false); | |
| } catch(e: any) { | |
| const errMsg = e.response?.data?.detail || "Nie znaleziono w bazie GUS. Możesz kontynuować, wpisując nazwę ręcznie."; | |
| toast.error(errMsg); | |
| setCompanyFound(false); | |
| setManualEntry(true); | |
| setCompanyDetails({ name: '', status: 'Dane wprowadzone ręcznie', nip: nip }); | |
| } finally { | |
| setIsSearching(false); | |
| } | |
| }; | |
| const handleStartAnalysis = async (additionalContext?: string) => { | |
| if (!desc && !additionalContext) return; | |
| setStep(3); | |
| setAnalyzing(true); | |
| setAnalysisLogs(prev => [...prev, additionalContext ? 'Aktualizacja analizy z nowymi danymi...' : 'Inicjalizowanie silnika RAG...']); | |
| // Złącz główny opis i dodatkowy kontekst z pytań jeśli są | |
| const finalDesc = additionalContext ? `${desc}\n\n[DOPRECYZOWANIE]:\n${additionalContext}` : desc; | |
| if (additionalContext) setDesc(finalDesc); | |
| try { | |
| const data = await matchProgram(finalDesc, nip); | |
| setRecommendedPrograms(data.programs || []); | |
| if (data.programs && data.programs.length > 0) setSelectedProgram(data.programs[0].id); | |
| setClarifyingQuestions(data.clarifying_questions || []); | |
| setClarificationAnswers(new Array((data.clarifying_questions || []).length).fill('')); | |
| setClarificationPanelOpen((data.clarifying_questions || []).length > 0); | |
| setAnalysisLogs(prev => [...prev, 'Zakończono generowanie raportu dopasowania.']); | |
| } catch(e) { | |
| toast.error("Wystąpił problem ze zgłaszaniem do modelu AI"); | |
| } finally { | |
| setAnalyzing(false); | |
| } | |
| }; | |
| const renderStepContent = () => { | |
| switch(step) { | |
| case 1: | |
| return ( | |
| <motion.div | |
| key="step1" | |
| initial={{ opacity: 0, x: -20 }} | |
| animate={{ opacity: 1, x: 0 }} | |
| exit={{ opacity: 0, x: 20 }} | |
| style={{ display: 'flex', flexDirection: 'column', gap: '1.5rem', width: '100%' }} | |
| > | |
| <div> | |
| <h3 style={{ fontSize: '1.2rem', marginBottom: '0.4rem', color: 'var(--text-primary)' }}>Podłącz profil firmy</h3> | |
| <p style={{ color: 'var(--text-secondary)', fontSize: '0.9rem' }}>System automatycznie pobierze dane rejestrowe z bazy GUS/KRS, by dopasować odpowiednie programy.</p> | |
| </div> | |
| <div style={{ display: 'flex', gap: '1rem', width: '100%' }}> | |
| <div style={{ flex: 1, position: 'relative' }}> | |
| <div style={{ position: 'absolute', left: '1rem', top: '50%', transform: 'translateY(-50%)', color: 'var(--accent-blue)' }}> | |
| <Building size={20} /> | |
| </div> | |
| <input | |
| type="text" | |
| className="wizard-input" | |
| placeholder="Wpisz NIP firmy..." | |
| value={nip} | |
| onChange={e => setNip(e.target.value)} | |
| style={{ width: '100%', padding: '1rem 1rem 1rem 3rem', background: 'rgba(255,255,255,0.03)', border: '1px solid rgba(59, 130, 246, 0.4)', borderRadius: '12px', color: '#fff', fontSize: '1.1rem', outline: 'none' }} | |
| onKeyDown={e => e.key === 'Enter' && handleSearchCompany()} | |
| /> | |
| </div> | |
| <button className="btn btn-secondary" onClick={handleSearchCompany} disabled={isSearching || nip.replace(/\D/g, '').length !== 10} style={{ minWidth: '140px' }}> | |
| {isSearching ? <Loader2 size={20} className="spin" /> : <><Search size={18}/> Szukaj w GUS</>} | |
| </button> | |
| </div> | |
| <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}> | |
| <button | |
| className="btn" | |
| onClick={() => { | |
| setNip('5213641211'); // Przykładowy realny NIP Testowy | |
| setDesc('Jesteśmy firmą produkcyjną produkującą części do maszyn rolniczych. Planujemy zakup nowoczesnych robotów spawalniczych i obrabiarek CNC dla automatyzacji najcięższych procesów. Budżet projektu to 4.5 mln PLN. Wdrażamy innowację na skalę krajową.'); | |
| }} | |
| style={{ fontSize: '0.85rem', color: 'var(--accent-blue)', background: 'rgba(59,130,246,0.1)', border: '1px dashed rgba(59,130,246,0.4)', padding: '0.5rem 1rem' }} | |
| > | |
| <Sparkles size={14} style={{ marginRight: '6px' }} /> | |
| Uzupełnij Zgłoszeniem Demo (Testowe) | |
| </button> | |
| {!manualEntry && ( | |
| <span style={{ fontSize: '0.85rem', color: 'var(--text-muted)', cursor: 'pointer', textDecoration: 'underline' }} onClick={() => { setManualEntry(true); setCompanyDetails({ name: '', status: 'Dane wprowadzone ręcznie', nip: nip }); }}> | |
| Wprowadź dane ręcznie | |
| </span> | |
| )} | |
| </div> | |
| {companyFound && !manualEntry && companyDetails && ( | |
| <motion.div initial={{ opacity: 0, height: 0 }} animate={{ opacity: 1, height: 'auto' }} style={{ background: 'rgba(16, 185, 129, 0.1)', borderLeft: '4px solid var(--accent-green)', padding: '1rem', borderRadius: '8px', marginTop: '1rem' }}> | |
| <div style={{ fontWeight: 'bold', display: 'flex', alignItems: 'center', gap: '0.5rem', color: 'var(--accent-green)' }}><CheckCircle size={18}/> Znaleziono firmę</div> | |
| <div style={{ marginTop: '0.5rem', fontSize: '0.9rem', color: 'var(--text-primary)' }}>NIP: {nip.replace(/\D/g, '')}</div> | |
| <div style={{ fontSize: '0.9rem', color: 'var(--text-primary)' }}>{companyDetails.name} • {companyDetails.status}</div> | |
| </motion.div> | |
| )} | |
| {manualEntry && ( | |
| <motion.div initial={{ opacity: 0, height: 0 }} animate={{ opacity: 1, height: 'auto' }} style={{ display: 'flex', flexDirection: 'column', gap: '0.5rem', marginTop: '1rem' }}> | |
| <label style={{ fontSize: '0.9rem', color: 'var(--text-secondary)' }}>Pełna nazwa firmy (podmiotu):</label> | |
| <input | |
| type="text" | |
| value={companyDetails?.name || ''} | |
| onChange={e => setCompanyDetails({ name: e.target.value, status: 'Dane wpisane ręcznie', nip: nip })} | |
| placeholder="Wpisz pełną nazwę przedsiębiorstwa..." | |
| style={{ width: '100%', padding: '1rem', background: 'rgba(255,255,255,0.03)', border: '1px solid rgba(245, 158, 11, 0.4)', borderRadius: '12px', color: '#fff', fontSize: '1.1rem', outline: 'none' }} | |
| /> | |
| </motion.div> | |
| )} | |
| <button | |
| className="btn btn-primary" | |
| style={{ marginTop: '1rem', alignSelf: 'flex-end' }} | |
| onClick={() => setStep(2)} | |
| disabled={(!companyFound && !manualEntry) || (manualEntry && (!companyDetails?.name || companyDetails.name.trim() === ''))} | |
| > | |
| Przejdź dalej <ChevronRight size={18} /> | |
| </button> | |
| </motion.div> | |
| ); | |
| case 2: | |
| return ( | |
| <motion.div | |
| key="step2" | |
| initial={{ opacity: 0, x: -20 }} | |
| animate={{ opacity: 1, x: 0 }} | |
| exit={{ opacity: 0, x: 20 }} | |
| style={{ display: 'flex', flexDirection: 'column', gap: '1.5rem', width: '100%' }} | |
| > | |
| <div> | |
| <h3 style={{ fontSize: '1.2rem', marginBottom: '0.4rem', color: 'var(--text-primary)' }}>Opisz planowaną inwestycję</h3> | |
| <p style={{ color: 'var(--text-secondary)', fontSize: '0.9rem' }}>Napisz swoimi słowami, co zamierzasz zrobić. Sztuczna Inteligencja przejdzie przez tekst wyciągając kryteria kluczowe dla dotacji.</p> | |
| </div> | |
| <div style={{ position: 'relative', width: '100%' }}> | |
| <Sparkles size={20} color="var(--accent-green)" style={{ position: 'absolute', top: '1.2rem', left: '1.2rem' }} /> | |
| <textarea | |
| value={desc} | |
| onChange={e => setDesc(e.target.value)} | |
| className="wizard-textarea" | |
| placeholder="Np. Planujemy zakup nowej linii produkcyjnej z robotami spawalniczymi oraz wdrożenie platformy chmurowej AI do przewidywania awarii maszyn. Szacowany budżet inwestycji to ok. 6 mln PLN..." | |
| style={{ width: '100%', height: '220px', padding: '1.2rem 1.2rem 1.2rem 3.2rem', background: 'rgba(255,255,255,0.03)', border: '1px solid rgba(16, 185, 129, 0.4)', borderRadius: '12px', color: '#fff', fontSize: '1.05rem', lineHeight: '1.5', outline: 'none', resize: 'none' }} | |
| /> | |
| </div> | |
| <button className="btn btn-primary" style={{ alignSelf: 'flex-end', background: 'linear-gradient(90deg, var(--accent-green), var(--accent-blue))', padding: '1rem 2rem', fontSize: '1.1rem', boxShadow: '0 0 20px rgba(16, 185, 129, 0.3)' }} onClick={() => handleStartAnalysis()} disabled={!desc}> | |
| <Sparkles size={20} /> Rozpocznij Analizę AI | |
| </button> | |
| </motion.div> | |
| ); | |
| case 3: { | |
| const activeExpandedProg = expandedProgram ? recommendedPrograms.find((p: any) => p.id === expandedProgram) : null; | |
| return ( | |
| <motion.div | |
| key="step3" | |
| initial={{ opacity: 0 }} | |
| animate={{ opacity: 1 }} | |
| style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', gap: '2rem', width: '100%', minHeight: '350px' }} | |
| > | |
| {analyzing ? ( | |
| <> | |
| <div style={{ position: 'relative', width: '100px', height: '100px' }}> | |
| <motion.div animate={{ rotate: 360 }} transition={{ repeat: Infinity, duration: 4, ease: 'linear' }} style={{ position: 'absolute', inset: 0, borderRadius: '50%', border: '4px solid transparent', borderTopColor: 'var(--accent-green)', borderRightColor: 'var(--accent-blue)' }}></motion.div> | |
| <motion.div animate={{ rotate: -360 }} transition={{ repeat: Infinity, duration: 3, ease: 'linear' }} style={{ position: 'absolute', inset: '10px', borderRadius: '50%', border: '4px solid transparent', borderTopColor: 'var(--accent-blue)', borderLeftColor: 'var(--accent-green)' }}></motion.div> | |
| <div style={{ position: 'absolute', inset: 0, display: 'flex', alignItems: 'center', justifyContent: 'center' }}> | |
| <Sparkles size={32} color="var(--accent-green)" /> | |
| </div> | |
| </div> | |
| <div style={{ textAlign: 'center', width: '100%' }}> | |
| <h3 style={{ fontSize: '1.5rem', marginBottom: '1rem', color: 'var(--text-primary)' }}>Silnik RAG przetwarza wniosek...</h3> | |
| <div style={{ height: '80px', display: 'flex', flexDirection: 'column', gap: '0.4rem', alignItems: 'center', color: 'var(--text-secondary)', fontSize: '0.9rem' }}> | |
| <AnimatePresence mode="popLayout"> | |
| {analysisLogs.slice(-2).map((log, i) => ( | |
| <motion.div key={log} initial={{ opacity: 0, y: 10 }} animate={{ opacity: i === 1 ? 1 : 0.5, y: 0 }} exit={{ opacity: 0 }}> | |
| {log} | |
| </motion.div> | |
| ))} | |
| </AnimatePresence> | |
| </div> | |
| </div> | |
| </> | |
| ) : ( | |
| <motion.div initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} style={{ display: 'flex', flexDirection: 'column', width: '100%', gap: '1rem', marginTop: '-1rem' }}> | |
| <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '0.5rem' }}> | |
| <h2 style={{ fontSize: '1.4rem', color: 'var(--text-primary)' }}>Wybierz docelowy program dotacyjny</h2> | |
| <div style={{ padding: '0.3rem 0.8rem', background: 'rgba(16,185,129,0.1)', color: 'var(--accent-green)', borderRadius: '20px', fontSize: '0.8rem', fontWeight: 'bold' }}>{recommendedPrograms.length} szans zidentyfikowanych</div> | |
| </div> | |
| {clarifyingQuestions.length > 0 && ( | |
| <div style={{ marginBottom: '1rem', background: 'rgba(56, 189, 248, 0.05)', borderRadius: '16px', border: '1px solid rgba(56, 189, 248, 0.2)', overflow: 'hidden' }}> | |
| <div | |
| style={{ padding: '1rem', display: 'flex', justifyContent: 'space-between', alignItems: 'center', cursor: 'pointer', background: 'rgba(56, 189, 248, 0.1)' }} | |
| onClick={() => setClarificationPanelOpen(!clarificationPanelOpen)} | |
| > | |
| <div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem' }}> | |
| <Sparkles size={18} color="var(--accent-blue)" /> | |
| <strong style={{ color: 'var(--accent-blue)', fontSize: '0.95rem' }}>Doprecyzuj swój projekt, by uzyskać lepsze dopasowanie</strong> | |
| </div> | |
| <ChevronRight size={18} color="var(--accent-blue)" style={{ transform: clarificationPanelOpen ? 'rotate(90deg)' : 'none', transition: '0.3s' }} /> | |
| </div> | |
| <AnimatePresence> | |
| {clarificationPanelOpen && ( | |
| <motion.div initial={{ height: 0, opacity: 0 }} animate={{ height: 'auto', opacity: 1 }} exit={{ height: 0, opacity: 0 }}> | |
| <div style={{ padding: '1rem' }}> | |
| <p style={{ color: 'var(--text-secondary)', fontSize: '0.9rem', marginBottom: '1rem' }}>AI zauważyło, że brakuje kilku ważnych informacji. Odpowiedz na poniższe pytania, a system poszuka lepszych dotacji.</p> | |
| <div style={{ display: 'flex', flexDirection: 'column', gap: '0.8rem' }}> | |
| {clarifyingQuestions.map((q, idx) => ( | |
| <div key={idx} style={{ display: 'flex', flexDirection: 'column', gap: '0.4rem' }}> | |
| <span style={{ color: 'var(--text-primary)', fontSize: '0.9rem' }}>{q}</span> | |
| <input | |
| type="text" | |
| value={clarificationAnswers[idx] || ''} | |
| onChange={e => { | |
| const newAnswers = [...clarificationAnswers]; | |
| newAnswers[idx] = e.target.value; | |
| setClarificationAnswers(newAnswers); | |
| }} | |
| className="wizard-input" | |
| placeholder="Twoja odpowiedź..." | |
| style={{ width: '100%', padding: '0.7rem', background: 'rgba(255,255,255,0.03)', border: '1px solid rgba(56, 189, 248, 0.3)', borderRadius: '8px', color: '#fff', fontSize: '0.95rem', outline: 'none' }} | |
| /> | |
| </div> | |
| ))} | |
| </div> | |
| <button | |
| className="btn btn-primary" | |
| style={{ marginTop: '1rem', width: '100%', background: 'var(--accent-blue)', color: '#000', fontWeight: 'bold' }} | |
| onClick={() => { | |
| const additionalContext = clarifyingQuestions.map((q, i) => clarificationAnswers[i] ? `Pytanie: ${q}\nOdpowiedź: ${clarificationAnswers[i]}` : '').filter(Boolean).join("\n\n"); | |
| if (additionalContext) handleStartAnalysis(additionalContext); | |
| else toast.error('Podaj przynajmniej jedną odpowiedź.'); | |
| }} | |
| > | |
| Zaktualizuj dopasowanie | |
| </button> | |
| </div> | |
| </motion.div> | |
| )} | |
| </AnimatePresence> | |
| </div> | |
| )} | |
| <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(230px, 1fr))', gap: '1rem', maxHeight: '380px', overflowY: 'auto', paddingRight: '0.5rem' }}> | |
| {recommendedPrograms.map((prog) => ( | |
| <div key={prog.id} | |
| className="hover-lift" | |
| style={{ | |
| borderRadius: '16px', | |
| border: selectedProgram === prog.id ? '2px solid var(--accent-green)' : '1px solid rgba(255,255,255,0.05)', | |
| background: selectedProgram === prog.id ? 'rgba(16, 185, 129, 0.05)' : 'rgba(255,255,255,0.02)', | |
| transition: 'all 0.2s', | |
| boxShadow: selectedProgram === prog.id ? '0 0 20px rgba(16,185,129,0.1)' : 'none', | |
| cursor: 'pointer', | |
| display: 'flex', flexDirection: 'column' | |
| }} | |
| onClick={() => setSelectedProgram(prog.id)} | |
| > | |
| <div style={{ padding: '1.2rem', display: 'flex', flexDirection: 'column', gap: '1rem', flex: 1 }}> | |
| <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start' }}> | |
| <div style={{ padding: '0.7rem', borderRadius: '12px', background: 'rgba(255,255,255,0.08)', display: 'flex', alignItems: 'center', justifyContent: 'center' }}> | |
| {prog.type === 'SMART' && <Cpu size={24} color="var(--accent-blue)" />} | |
| {prog.type === 'ARIMR' && <Tractor size={24} color="var(--accent-green)" />} | |
| {prog.type === 'ZUS_BHP' && <HardHat size={24} color="var(--accent-orange)" />} | |
| </div> | |
| <div style={{ width: '22px', height: '22px', borderRadius: '50%', border: '2px solid', borderColor: selectedProgram === prog.id ? 'var(--accent-green)' : 'rgba(255,255,255,0.2)', display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0 }}> | |
| {selectedProgram === prog.id && <div style={{ width: '12px', height: '12px', borderRadius: '50%', background: 'var(--accent-green)' }}></div>} | |
| </div> | |
| </div> | |
| <div> | |
| <div style={{ fontWeight: 800, fontSize: '1.1rem', color: 'var(--text-primary)', marginBottom: '0.4rem' }}>{prog.name}</div> | |
| <div style={{ fontSize: '0.85rem', color: 'var(--text-secondary)' }}> | |
| {prog.shortDesc} | |
| </div> | |
| </div> | |
| <div style={{ marginTop: 'auto', display: 'flex', justifyContent: 'space-between', alignItems: 'flex-end', borderTop: '1px solid rgba(255,255,255,0.05)', paddingTop: '0.8rem' }}> | |
| <div style={{ display: 'flex', flexDirection: 'column', gap: '0.4rem' }}> | |
| <div style={{ fontSize: '0.75rem', fontWeight: 'bold', display: 'inline-block', padding: '0.2rem 0.6rem', borderRadius: '4px', background: prog.match > 80 ? 'rgba(16,185,129,0.2)' : prog.match > 60 ? 'rgba(245,158,11,0.2)' : 'rgba(239,68,68,0.2)', color: prog.match > 80 ? 'var(--accent-green)' : prog.match > 60 ? 'var(--accent-orange)' : 'var(--accent-red)', width: 'fit-content' }}> | |
| MATCH: {prog.match}% | |
| </div> | |
| <div style={{ fontSize: '0.75rem', color: 'var(--text-muted)' }}>Maks: <span style={{ color: 'var(--text-primary)', fontWeight: 'bold' }}>{prog.amount}</span></div> | |
| </div> | |
| <button | |
| onClick={(e) => { e.stopPropagation(); setExpandedProgram(expandedProgram === prog.id ? null : prog.id); }} | |
| className="btn hover-lift" | |
| style={{ background: 'rgba(255,255,255,0.05)', border: '1px solid rgba(255,255,255,0.1)', padding: '0.4rem', color: 'var(--text-secondary)', borderRadius: '8px' }} | |
| title="Więcej informacji" | |
| > | |
| <Info size={16} /> | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| ))} | |
| </div> | |
| <div style={{ display: 'flex', justifyContent: 'flex-end', marginTop: '1rem' }}> | |
| <button | |
| className="btn btn-primary" | |
| onClick={async () => { | |
| onClose(); | |
| const dynName = desc ? (desc.length > 100 ? `${desc.slice(0, 100)}...` : desc) : `Nowy Projekt - ${new Date().toLocaleDateString()}`; | |
| const recommendedProgram = recommendedPrograms.find(p => p.id === selectedProgram); | |
| const programName = recommendedProgram?.name || 'Nieznany Program'; | |
| const programType = recommendedProgram?.type || 'SMART'; | |
| const grantAmount = recommendedProgram?.amount || 'Nie określono'; | |
| try { | |
| const newProject = await createProject({ | |
| title: dynName, | |
| description: desc, | |
| program_type: programType, | |
| program_name: programName, | |
| external_context: { | |
| company_data: companyDetails, | |
| resources: [], | |
| grant_amount: grantAmount | |
| } | |
| }); | |
| import('react-hot-toast').then(rt => rt.toast.success("Projekt został wygenerowany pomyślnie.")); | |
| navigate(`/projects/${newProject.id}`, { state: { projectName: dynName } }); | |
| } catch (err) { | |
| console.error(err); | |
| import('react-hot-toast').then(rt => rt.toast.error("Błąd podczas tworzenia projektu.")); | |
| } | |
| }} | |
| disabled={!selectedProgram} | |
| style={{ | |
| padding: '1rem 2rem', | |
| fontWeight: 'bold', | |
| background: selectedProgram ? 'linear-gradient(90deg, var(--accent-green), var(--accent-blue))' : 'rgba(255,255,255,0.05)', | |
| color: selectedProgram ? '#000' : 'rgba(255,255,255,0.4)', | |
| boxShadow: selectedProgram ? '0 0 20px rgba(16,185,129,0.3)' : 'none' | |
| }} | |
| > | |
| {selectedProgram ? 'Przejdź do Generatora Wniosku' : 'Wybierz program aby kontynuować'} | |
| </button> | |
| </div> | |
| {/* MODAL DETALI (OVERLAY) */} | |
| <AnimatePresence> | |
| {activeExpandedProg && ( | |
| <motion.div | |
| initial={{ opacity: 0, scale: 0.96 }} | |
| animate={{ opacity: 1, scale: 1 }} | |
| exit={{ opacity: 0, scale: 0.96 }} | |
| style={{ | |
| position: 'absolute', | |
| inset: 0, | |
| background: 'rgba(11, 14, 20, 0.95)', | |
| backdropFilter: 'blur(10px)', | |
| borderRadius: '16px', | |
| padding: '2.5rem', | |
| display: 'flex', | |
| flexDirection: 'column', | |
| zIndex: 20 | |
| }} | |
| > | |
| <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', marginBottom: '1.5rem' }}> | |
| <h2 style={{ fontSize: '1.8rem', color: 'var(--text-primary)', margin: 0 }}>{activeExpandedProg.name}</h2> | |
| </div> | |
| <div style={{ display: 'flex', gap: '1rem', marginBottom: '2rem' }}> | |
| <span style={{ background: 'rgba(16,185,129,0.1)', color: 'var(--accent-green)', padding: '0.4rem 1rem', borderRadius: '6px', fontSize: '0.9rem', fontWeight: 'bold' }}> | |
| Szansa wg AI: {activeExpandedProg.chance} | |
| </span> | |
| <span style={{ background: 'rgba(255,255,255,0.05)', color: 'var(--text-secondary)', padding: '0.4rem 1rem', borderRadius: '6px', fontSize: '0.9rem' }}> | |
| Kwota wsparcia: {activeExpandedProg.amount} | |
| </span> | |
| </div> | |
| <div style={{ flex: 1, overflowY: 'auto', paddingRight: '1rem' }}> | |
| <div style={{ fontSize: '1.15rem', color: 'var(--text-primary)', marginBottom: '1.5rem', lineHeight: '1.6' }}> | |
| {activeExpandedProg.shortDesc} | |
| </div> | |
| {activeExpandedProg.explanation && ( | |
| <div style={{ marginBottom: '1.5rem', display: 'flex', flexDirection: 'column', gap: '1rem' }}> | |
| <div style={{ background: 'rgba(16, 185, 129, 0.05)', padding: '1.2rem', borderRadius: '12px', border: '1px solid rgba(16, 185, 129, 0.2)' }}> | |
| <strong style={{ color: 'var(--accent-green)', display: 'block', marginBottom: '0.6rem', fontSize: '1.05rem' }}>Dlaczego ten program jest dla Ciebie najlepszy?</strong> | |
| <span style={{ color: 'var(--text-primary)', fontSize: '0.95rem', lineHeight: '1.5' }}>{activeExpandedProg.explanation.reason}</span> | |
| </div> | |
| {activeExpandedProg.explanation.criteria && activeExpandedProg.explanation.criteria.length > 0 && ( | |
| <div style={{ background: 'rgba(255,255,255,0.03)', padding: '1.2rem', borderRadius: '12px', border: '1px solid rgba(255,255,255,0.05)' }}> | |
| <strong style={{ color: 'var(--text-primary)', display: 'block', marginBottom: '0.6rem', fontSize: '1.05rem' }}>Główne kryteria, które spełniasz:</strong> | |
| <ul style={{ margin: 0, paddingLeft: '1.2rem', color: 'var(--text-secondary)', fontSize: '0.95rem', lineHeight: '1.6' }}> | |
| {activeExpandedProg.explanation.criteria.map((crit: string, idx: number) => ( | |
| <li key={idx} style={{ marginBottom: '0.3rem' }}>{crit}</li> | |
| ))} | |
| </ul> | |
| </div> | |
| )} | |
| {activeExpandedProg.explanation.risks && ( | |
| <div style={{ background: 'rgba(239, 68, 68, 0.05)', padding: '1.2rem', borderRadius: '12px', border: '1px solid rgba(239, 68, 68, 0.2)' }}> | |
| <strong style={{ color: 'var(--accent-red)', display: 'block', marginBottom: '0.6rem', fontSize: '1.05rem' }}>Potencjalne wyzwania do zaadresowania:</strong> | |
| <span style={{ color: 'var(--text-secondary)', fontSize: '0.95rem', lineHeight: '1.5' }}>{activeExpandedProg.explanation.risks}</span> | |
| </div> | |
| )} | |
| </div> | |
| )} | |
| <div style={{ fontSize: '1rem', color: 'var(--text-secondary)', lineHeight: '1.7', background: 'rgba(56, 189, 248, 0.05)', padding: '1.5rem', borderRadius: '16px', border: '1px solid rgba(56, 189, 248, 0.1)' }}> | |
| <strong style={{ color: 'var(--accent-blue)', display: 'block', marginBottom: '0.8rem', fontSize: '1.1rem' }}><Sparkles size={18} style={{ display: 'inline', position: 'relative', top: '3px', marginRight: '6px' }} /> Raport zgodności:</strong> | |
| {activeExpandedProg.fullDesc} | |
| </div> | |
| </div> | |
| <div style={{ display: 'flex', justifyContent: 'flex-end', gap: '1rem', marginTop: '1.5rem', borderTop: '1px solid rgba(255,255,255,0.05)', paddingTop: '1.5rem' }}> | |
| <button className="btn" onClick={() => setExpandedProgram(null)} style={{ padding: '0.8rem 1.5rem', background: 'transparent', color: 'var(--text-secondary)', border: '1px solid rgba(255,255,255,0.1)' }}> | |
| Wróć do listy | |
| </button> | |
| <button | |
| className="btn btn-primary" | |
| onClick={() => { setSelectedProgram(activeExpandedProg.id); setExpandedProgram(null); }} | |
| style={{ padding: '0.8rem 1.5rem', background: 'var(--accent-green)', color: '#000', fontWeight: 'bold' }} | |
| > | |
| Wybieram i kontynuuję | |
| </button> | |
| </div> | |
| </motion.div> | |
| )} | |
| </AnimatePresence> | |
| </motion.div> | |
| )} | |
| </motion.div> | |
| ); | |
| } | |
| } | |
| }; | |
| return createPortal( | |
| <AnimatePresence> | |
| <motion.div | |
| className="pricing-backdrop" | |
| initial={{ opacity: 0 }} | |
| animate={{ opacity: 1 }} | |
| exit={{ opacity: 0 }} | |
| style={{ position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.85)', backdropFilter: 'blur(5px)', display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: 9999 }} | |
| > | |
| <motion.div | |
| className="glass-card" | |
| style={{ width: '800px', maxWidth: '95%', background: '#0B0E14', padding: '0', overflow: 'hidden' }} | |
| initial={{ scale: 0.95, y: 20 }} | |
| animate={{ scale: 1, y: 0 }} | |
| exit={{ scale: 0.95, y: 20 }} | |
| > | |
| {/* Header */} | |
| <div style={{ padding: '2rem', borderBottom: '1px solid rgba(255,255,255,0.05)', display: 'flex', justifyContent: 'space-between', alignItems: 'center', background: 'rgba(255,255,255,0.02)' }}> | |
| <div> | |
| <h2 style={{ display: 'flex', alignItems: 'center', gap: '0.6rem', fontSize: '1.4rem' }}> | |
| <Sparkles color="var(--accent-blue)" /> Nowy Projekt Dotacyjny | |
| </h2> | |
| <div style={{ display: 'flex', gap: '0.5rem', marginTop: '0.8rem' }}> | |
| {[1, 2, 3].map(i => ( | |
| <div key={i} style={{ height: '4px', width: '40px', background: i <= step ? 'var(--accent-green)' : 'rgba(255,255,255,0.1)', borderRadius: '2px', transition: '0.3s all' }}></div> | |
| ))} | |
| <span style={{ fontSize: '0.8rem', color: 'var(--text-muted)', marginLeft: '0.5rem' }}>Krok {step} / 3</span> | |
| </div> | |
| </div> | |
| {step < 3 && <X size={24} style={{ cursor: 'pointer', color: 'var(--text-muted)', alignSelf: 'flex-start' }} onClick={onClose} />} | |
| </div> | |
| {/* Body */} | |
| <div style={{ padding: '2.5rem 2rem', minHeight: '380px', display: 'flex' }}> | |
| <AnimatePresence mode="wait"> | |
| {renderStepContent()} | |
| </AnimatePresence> | |
| </div> | |
| </motion.div> | |
| </motion.div> | |
| </AnimatePresence>, | |
| document.body | |
| ); | |
| }; | |
| export default WizardModal; | |