Spaces:
Running
Running
| import React, { useState, useEffect, useRef } from 'react'; | |
| import { motion, AnimatePresence } from 'framer-motion'; | |
| import { OnboardingTour } from '../onboarding/OnboardingTour'; | |
| import { ArrowLeft, Download, Clock, Building, CheckCircle, ShieldAlert, Activity, PanelLeftClose, PanelLeftOpen, Sparkles, Database, TrendingUp } from 'lucide-react'; | |
| import { useNavigate } from 'react-router-dom'; | |
| import SectionList from './SectionList'; | |
| import SectionEditor from './SectionEditor'; | |
| import LivePreview from './LivePreview'; | |
| import ProjectQAPanel from './ProjectQAPanel'; | |
| import ProjectChatPanel from './ProjectChatPanel'; | |
| import FinalDocumentPanel from './FinalDocumentPanel'; | |
| import ProjectResourcesPanel from './ProjectResourcesPanel'; | |
| import ProjectAuditPanel from './ProjectAuditPanel'; | |
| import AIGeneratorPanel from './AIGeneratorPanel'; | |
| import { ExportModal } from './ExportModal'; | |
| import DocumentUploadPanel from './DocumentUploadPanel'; | |
| import MatchingGrantsWidget from './MatchingGrantsWidget'; | |
| import { getProjectSections } from '../../api/client'; | |
| interface WorkspaceProps { | |
| project: any; | |
| statusLabel: string; | |
| onRefresh: () => void; | |
| } | |
| const ProjectWorkspace: React.FC<WorkspaceProps> = ({ project, statusLabel, onRefresh }) => { | |
| const navigate = useNavigate(); | |
| // Zmienne stanu logiki roboczej | |
| const [activeMainTab, setActiveMainTab] = useState('overview'); // overview, sections, final, verify, audit | |
| const previousTabRef = useRef('overview'); | |
| const [activeSectionId, setActiveSectionId] = useState<string | null>(null); | |
| const [sectionsData, setSectionsData] = useState<any[]>([]); | |
| const [isPreviewOpen, setIsPreviewOpen] = useState(false); | |
| const [isExportModalOpen, setIsExportModalOpen] = useState(false); | |
| const [isLoading, setIsLoading] = useState(true); | |
| const [isSidebarOpen, setIsSidebarOpen] = useState(true); | |
| useEffect(() => { | |
| if (activeMainTab !== 'generator') { | |
| previousTabRef.current = activeMainTab; | |
| } | |
| }, [activeMainTab]); | |
| const loadSections = async () => { | |
| try { | |
| setIsLoading(true); | |
| const data = await getProjectSections(project.id); | |
| setSectionsData(data); | |
| if (data.length > 0) { | |
| setActiveSectionId(prev => { | |
| if (prev && data.find((s: any) => s.section_type === prev)) return prev; | |
| return data[0].section_type; | |
| }); | |
| } | |
| } catch (err) { | |
| console.error("Failed to load sections", err); | |
| } finally { | |
| setIsLoading(false); | |
| } | |
| }; | |
| useEffect(() => { | |
| if(project.id) loadSections(); | |
| const handleRefresh = () => loadSections(); | |
| window.addEventListener('refresh-sections', handleRefresh); | |
| return () => { | |
| window.removeEventListener('refresh-sections', handleRefresh); | |
| }; | |
| }, [project.id]); | |
| useEffect(() => { | |
| let timeoutId: ReturnType<typeof setTimeout>; | |
| if (!isLoading && sectionsData.length > 0) { | |
| const hasSeenTourWorkspace = localStorage.getItem('has_seen_tour_workspace'); | |
| if (!hasSeenTourWorkspace) { | |
| timeoutId = setTimeout(() => setRunTour(true), 1000); | |
| } | |
| } | |
| return () => { | |
| if (timeoutId) clearTimeout(timeoutId); | |
| }; | |
| }, [isLoading, sectionsData.length]); | |
| useEffect(() => { | |
| // Pozwala zakładce audytu nawigować do konkretnej sekcji | |
| const handleNavigateToSection = (e: Event) => { | |
| const custom = e as CustomEvent; | |
| if (custom.detail?.sectionType) { | |
| const target = custom.detail.sectionType; | |
| const targetClean = target ? target.trim().toLowerCase() : ""; | |
| // Szukamy pasującej sekcji w załadowanych danych przy użyciu systemu punktacji | |
| let bestMatch = null; | |
| let highestScore = 0; | |
| sectionsData.forEach(s => { | |
| if (!s || !targetClean) return; | |
| const st = s.section_type?.toLowerCase() || ""; | |
| const stClean = s.section_type?.replace(/_/g, ' ').toLowerCase() || ""; | |
| const title = s.title?.trim().toLowerCase() || ""; | |
| let score = 0; | |
| if (st === targetClean || title === targetClean) { | |
| score = 100; | |
| } else if (stClean === targetClean) { | |
| score = 90; | |
| } else if ((st && targetClean.includes(st)) || (stClean && targetClean.includes(stClean))) { | |
| score = 80; | |
| } else if (title && targetClean && title.includes(targetClean)) { | |
| score = 70; | |
| } else if (targetClean && title && targetClean.includes(title)) { | |
| score = title.length >= 4 ? 60 : 0; | |
| } else if (stClean && targetClean && stClean.includes(targetClean)) { | |
| score = 50; | |
| } else if (targetClean && stClean && targetClean.includes(stClean)) { | |
| score = stClean.length >= 4 ? 40 : 0; | |
| } else if (targetClean && title) { | |
| const targetWords = targetClean.split(/\s+/).filter((w: string) => w.length >= 4); | |
| for (const w of targetWords) { | |
| if (title.includes(w) || (stClean && stClean.includes(w))) { | |
| score = 30; | |
| break; | |
| } | |
| } | |
| } | |
| if (score > highestScore) { | |
| highestScore = score; | |
| bestMatch = s; | |
| } | |
| }); | |
| if (bestMatch) { | |
| setActiveSectionId(bestMatch.section_type); | |
| setActiveMainTab('sections'); | |
| } else { | |
| // Fallback jeśli nie znaleziono | |
| import('react-hot-toast').then(toast => { | |
| toast.default.error(`Nie znaleziono pasującej sekcji: ${target}`); | |
| }); | |
| } | |
| } | |
| }; | |
| const handleSwitchToSections = () => { | |
| setActiveMainTab('sections'); | |
| }; | |
| window.addEventListener('navigate-to-section', handleNavigateToSection); | |
| window.addEventListener('switch-to-sections-tab', handleSwitchToSections); | |
| return () => { | |
| window.removeEventListener('navigate-to-section', handleNavigateToSection); | |
| window.removeEventListener('switch-to-sections-tab', handleSwitchToSections); | |
| }; | |
| }, [sectionsData]); | |
| const [runTour, setRunTour] = useState(false); | |
| const workspaceTourSteps = [ | |
| { | |
| target: '.tour-step-sections', | |
| content: 'Gdy utworzysz projekt, z lewej masz listę sekcji potrzebną do wniosku. Kliknij pierwszą z góry by uruchomić edytor.', | |
| placement: 'right' as const, | |
| disableBeacon: true | |
| }, | |
| { | |
| target: '.tour-step-qa', | |
| content: 'Nie wiesz jak wypełnić trudną sekcję? Ten asystent zasilany danymi Regulaminu i Wtyczką RAG rozwiąże każdy dylemat.', | |
| placement: 'left' as const | |
| } | |
| ]; | |
| const currentDbSection = sectionsData.find(s => s.section_type === activeSectionId); | |
| const activeSectionTitle = currentDbSection?.title || 'Brak tytułu'; | |
| const completedList = sectionsData.filter(s => s.is_approved).map(s => s.section_type); | |
| const isFullyApproved = sectionsData.length > 0 && sectionsData.every(s => s.is_approved && s.content && s.content.trim() !== ""); | |
| const hasUnapproved = sectionsData.some(s => !s.is_approved && s.content && s.content.length > 50); | |
| const renderMainTabContent = () => { | |
| switch(activeMainTab) { | |
| case 'overview': | |
| return ( | |
| <div style={{ maxWidth: '1000px', margin: '0 auto', padding: '2rem', height: '100%', overflowY: 'auto' }}> | |
| <div className="glass-card" style={{ marginBottom: '2rem', padding: '2rem' }}> | |
| <div style={{ display: 'flex', alignItems: 'center', gap: '1rem', marginBottom: '1.5rem' }}> | |
| <div style={{ width: '48px', height: '48px', background: 'rgba(59,130,246,0.1)', borderRadius: '12px', display: 'flex', alignItems: 'center', justifyContent: 'center', color: 'var(--accent-blue)' }}> | |
| <Building size={24} /> | |
| </div> | |
| <div> | |
| <h2 style={{ fontSize: '1.4rem', fontWeight: 800, margin: 0, color: 'var(--text-primary)' }}>Podsumowanie projektu</h2> | |
| <p style={{ color: 'var(--text-secondary)', margin: '0.2rem 0 0 0', fontSize: '0.9rem' }}>Metadane klienta i wniosku</p> | |
| </div> | |
| </div> | |
| <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '2rem' }}> | |
| <div style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}> | |
| <div><span style={{color:'var(--text-muted)', fontSize:'0.85rem', display:'block', marginBottom:'0.2rem'}}>Nazwa firmy / Opis</span><strong style={{color:'#fff'}}>{project.external_context?.company_data?.name || 'Brak wpisanego opisu firmy'}</strong></div> | |
| <div><span style={{color:'var(--text-muted)', fontSize:'0.85rem', display:'block', marginBottom:'0.2rem'}}>NIP Firmy</span><strong style={{color:'#fff'}}>{project.external_context?.company_data?.nip || 'Brak NIP'}</strong></div> | |
| <div><span style={{color:'var(--text-muted)', fontSize:'0.85rem', display:'block', marginBottom:'0.2rem'}}>Data utworzenia projektu</span><strong style={{color:'#fff'}}>{new Date(project.created_at).toLocaleDateString('pl-PL')}</strong></div> | |
| </div> | |
| <div style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}> | |
| <div><span style={{color:'var(--text-muted)', fontSize:'0.85rem', display:'block', marginBottom:'0.2rem'}}>Program dotacyjny</span><strong style={{color:'var(--accent-green)'}}>{project.program_name || 'Brak wybranego programu'}</strong></div> | |
| <div><span style={{color:'var(--text-muted)', fontSize:'0.85rem', display:'block', marginBottom:'0.2rem'}}>Maksymalna kwota dofinansowania</span><strong style={{color:'#fff'}}>{project.external_context?.grant_amount ? project.external_context.grant_amount : (project.grant_amount_max ? `${project.grant_amount_max} PLN` : 'Nie określono')}</strong></div> | |
| <div><span style={{color:'var(--text-muted)', fontSize:'0.85rem', display:'block', marginBottom:'0.2rem'}}>Status</span> | |
| <div style={{ display: 'inline-block', padding: '0.2rem 0.6rem', background: 'rgba(59, 130, 246, 0.15)', color: 'var(--accent-blue)', borderRadius: '4px', fontSize: '0.75rem', fontWeight: 800, textTransform: 'uppercase' }}>{statusLabel}</div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '2rem' }}> | |
| <div className="glass-card" style={{ display: 'flex', flexDirection: 'column' }}> | |
| <h3 style={{ fontSize: '1.1rem', marginBottom: '1rem', display: 'flex', alignItems: 'center', gap: '0.5rem' }}><Activity size={18} color="var(--accent-blue)"/> Postęp Wniosku</h3> | |
| <div style={{ display: 'flex', alignItems: 'center', gap: '1rem', marginBottom: '1rem' }}> | |
| <div className="step-circle" style={{ background: 'rgba(59,130,246,0.1)', borderColor: 'var(--accent-blue)', color: 'var(--accent-blue)', fontWeight: 'bold', width: '45px', height: '45px', fontSize: '1.1rem' }}> | |
| {sectionsData.filter(s => s.content && s.content.length > 50).length} | |
| </div> | |
| <div> | |
| <div style={{ fontWeight: 700, fontSize: '1.05rem', color: '#fff' }}>Gotowe sekcje dokumentu</div> | |
| <div style={{ fontSize: '0.85rem', color: 'var(--text-muted)' }}>z {sectionsData.length} wszystkich sekcji</div> | |
| </div> | |
| </div> | |
| <button className="btn btn-secondary" style={{ marginTop: 'auto' }} onClick={() => setActiveMainTab('sections')}>Przejdź do edycji</button> | |
| </div> | |
| <div className="glass-card" style={{ display: 'flex', flexDirection: 'column' }}> | |
| <h3 style={{ fontSize: '1.1rem', marginBottom: '1rem', display: 'flex', alignItems: 'center', gap: '0.5rem' }}><ShieldAlert size={18} color="var(--accent-green)"/> Weryfikacja</h3> | |
| {isFullyApproved ? ( | |
| <div style={{ background: 'rgba(16,185,129,0.1)', borderLeft: '3px solid var(--accent-green)', padding: '1rem', borderRadius: '4px', marginBottom: '1rem' }}> | |
| <p style={{ margin: 0, color: '#A7F3D0', fontSize: '0.9rem', fontWeight: 500 }}>Wszystkie wypełnione sekcje zostały zatwierdzone przez doradcę. Dokument jest spójny.</p> | |
| </div> | |
| ) : hasUnapproved ? ( | |
| <div style={{ background: 'rgba(239,68,68,0.1)', borderLeft: '3px solid var(--accent-red)', padding: '1rem', borderRadius: '4px', marginBottom: '1rem' }}> | |
| <p style={{ margin: 0, color: '#FECACA', fontSize: '0.9rem', fontWeight: 500 }}>Projekt zawiera sekcje bez ostatecznej akceptacji doradcy. Sprawdź je przed wygenerowaniem PDF.</p> | |
| </div> | |
| ) : ( | |
| <div style={{ background: 'rgba(255,255,255,0.05)', padding: '1rem', borderRadius: '4px', marginBottom: '1rem' }}> | |
| <p style={{ margin: 0, color: 'var(--text-muted)', fontSize: '0.9rem' }}>Rozpocznij wypełnianie wniosku, aby sprawdzić jego poprawność.</p> | |
| </div> | |
| )} | |
| <button className="btn btn-secondary" style={{ marginTop: 'auto' }} onClick={() => setActiveMainTab('verify')}>Weryfikator Projektu</button> | |
| </div> | |
| </div> | |
| {/* Widget: Pasujące nabory */} | |
| <div className="glass-card" style={{ marginTop: '2rem' }}> | |
| <h3 style={{ fontSize: '1.05rem', marginBottom: '1rem', display: 'flex', alignItems: 'center', gap: '0.5rem' }}> | |
| <TrendingUp size={16} color="#a78bfa" /> Pasujące Nabory | |
| </h3> | |
| <MatchingGrantsWidget | |
| projectId={project.id} | |
| projectContext={project.external_context} | |
| onUpdateContext={onRefresh} | |
| /> | |
| </div> | |
| </div> | |
| ); | |
| case 'sections': | |
| return ( | |
| <div className="workspace-container"> | |
| <div | |
| className="workspace-sidebar tour-step-sections" | |
| style={{ | |
| width: isSidebarOpen ? '320px' : '0px', | |
| padding: isSidebarOpen ? '1.5rem' : '0', | |
| borderRight: isSidebarOpen ? '1px solid rgba(255,255,255,0.05)' : 'none', | |
| overflow: 'hidden', | |
| transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)' | |
| }} | |
| > | |
| <div style={{ minWidth: '280px' }}> | |
| <SectionList | |
| sections={sectionsData} | |
| activeSection={activeSectionId || ''} | |
| setActiveSection={setActiveSectionId} | |
| completedSections={completedList} | |
| /> | |
| </div> | |
| </div> | |
| <div className="workspace-main" style={{ position: 'relative' }}> | |
| <button | |
| onClick={() => setIsSidebarOpen(!isSidebarOpen)} | |
| className="btn btn-secondary hover-bg" | |
| style={{ | |
| position: 'absolute', | |
| top: '1rem', | |
| left: '1rem', | |
| zIndex: 10, | |
| padding: '0.5rem', | |
| borderRadius: '8px', | |
| background: 'rgba(255,255,255,0.05)' | |
| }} | |
| title={isSidebarOpen ? "Kryj panel boczny" : "Pokaż panel boczny"} | |
| > | |
| {isSidebarOpen ? <PanelLeftClose size={18} /> : <PanelLeftOpen size={18} />} | |
| </button> | |
| <div style={{ maxWidth: '1400px', margin: '0 auto', paddingTop: isSidebarOpen ? '0' : '1rem' }}> | |
| {isLoading ? ( | |
| <div style={{ display: 'flex', flexDirection: 'column', gap: '1.5rem', padding: '1rem' }}> | |
| <div className="skeleton-box" style={{ width: '40%', height: '40px', borderRadius: '8px' }}></div> | |
| <div className="skeleton-box" style={{ width: '100%', height: '60vh', borderRadius: '16px' }}></div> | |
| </div> | |
| ) : ( | |
| <AnimatePresence mode="wait"> | |
| {activeSectionId && ( | |
| <motion.div | |
| key={activeSectionId} | |
| initial={{ opacity: 0, x: 20 }} | |
| animate={{ opacity: 1, x: 0 }} | |
| exit={{ opacity: 0, x: -20 }} | |
| transition={{ duration: 0.2 }} | |
| > | |
| <SectionEditor | |
| projectId={project.id} | |
| sectionId={activeSectionId} | |
| sectionTitle={activeSectionTitle} | |
| initialContent={currentDbSection?.content || ''} | |
| dbSectionId={currentDbSection?.id} | |
| onSectionUpdated={loadSections} | |
| /> | |
| </motion.div> | |
| )} | |
| </AnimatePresence> | |
| )} | |
| </div> | |
| </div> | |
| </div> | |
| ); | |
| case 'final': | |
| return ( | |
| <div className="workspace-main" style={{ padding: '2rem' }}> | |
| <div style={{ maxWidth: '1400px', margin: '0 auto', height: '100%' }}> | |
| <FinalDocumentPanel project={project} onUpdate={onRefresh} /> | |
| </div> | |
| </div> | |
| ); | |
| case 'verify': | |
| return ( | |
| <div className="workspace-main" style={{ padding: '2rem', height: '100%', overflowY: 'auto' }}> | |
| <div style={{ maxWidth: '1000px', margin: '0 auto' }}> | |
| <h2 style={{ fontSize: '1.8rem', fontWeight: 800, marginBottom: '1rem' }}>Weryfikator Projektu</h2> | |
| <p style={{ color: 'var(--text-secondary)', marginBottom: '2rem' }}>Pełne podsumowanie statusu zatwierdzenia przez doradcę na każdym etapie pisania dokumentacji.</p> | |
| {sectionsData.length === 0 && <p>Brak sekcji.</p>} | |
| <div style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}> | |
| {sectionsData.map(s => { | |
| const hasContent = s.content && s.content.length > 50; | |
| return ( | |
| <div key={s.id} className="glass-card" style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '1.5rem', borderLeft: s.is_approved ? '4px solid var(--accent-green)' : (hasContent ? '4px solid var(--accent-red)' : '4px solid rgba(255,255,255,0.1)') }}> | |
| <div> | |
| <h4 style={{ margin: '0 0 0.5rem 0', fontSize: '1.1rem', color: 'var(--text-primary)' }}>{s.title}</h4> | |
| <div style={{ fontSize: '0.85rem', color: 'var(--text-muted)' }}> | |
| {hasContent ? 'Sekcja posiada treść' : 'Brak treści / Wymaga uzupełnienia'} | |
| </div> | |
| </div> | |
| <div> | |
| {s.is_approved ? ( | |
| <span className="badge badge-success" style={{ display: 'flex', alignItems: 'center', gap: '0.4rem' }}><CheckCircle size={14}/> ZATWIERDZONE</span> | |
| ) : hasContent ? ( | |
| <span className="badge badge-error" style={{ display: 'flex', alignItems: 'center', gap: '0.4rem' }}><ShieldAlert size={14}/> OCZEKUJE NA WERYFIKACJĘ</span> | |
| ) : ( | |
| <span className="badge" style={{ background: 'rgba(255,255,255,0.1)', color: 'var(--text-secondary)' }}>PUSTE</span> | |
| )} | |
| </div> | |
| </div> | |
| ) | |
| })} | |
| </div> | |
| </div> | |
| </div> | |
| ); | |
| case 'audit': | |
| return ( | |
| <div className="workspace-main" style={{ padding: '0', background: 'var(--bg-document)', height: '100%', overflowY: 'auto' }}> | |
| <ProjectAuditPanel projectId={project.id} /> | |
| </div> | |
| ); | |
| case 'resources': | |
| return ( | |
| <div className="workspace-main" style={{ padding: '2rem', height: '100%', overflowY: 'auto' }}> | |
| <ProjectResourcesPanel projectId={project.id} /> | |
| </div> | |
| ); | |
| case 'generator': | |
| return ( | |
| <div className="workspace-main" style={{ padding: '2rem', height: '100%', overflowY: 'auto' }}> | |
| <AIGeneratorPanel | |
| projectId={project.id} | |
| onCompleted={() => { | |
| // Najpierw odśwież dane projektu... | |
| onRefresh(); | |
| // ...potem przenieś na poprzednią zakładkę | |
| setTimeout(() => { | |
| loadSections(); | |
| setActiveMainTab(previousTabRef.current); | |
| }, 800); | |
| }} | |
| /> | |
| </div> | |
| ); | |
| case 'documents': | |
| return ( | |
| <div className="workspace-main" style={{ padding: '0', height: '100%', overflowY: 'auto' }}> | |
| <DocumentUploadPanel projectId={project.id} /> | |
| </div> | |
| ); | |
| } | |
| }; | |
| return ( | |
| <div style={{ flex: 1, display: 'flex', flexDirection: 'column', height: '100%', overflow: 'hidden' }}> | |
| {/* CONTEXTUAL TOP BAR (STICKY) */} | |
| <div style={{ | |
| position: 'sticky', top: 0, zIndex: 50, | |
| backgroundColor: 'rgba(5, 5, 5, 0.95)', | |
| backdropFilter: 'blur(20px)', | |
| borderBottom: '1px solid rgba(255,255,255,0.05)', | |
| display: 'flex', flexDirection: 'column', flexShrink: 0 | |
| }}> | |
| {/* Górny pasek - nazwa i powrót */} | |
| <div style={{ padding: '1rem 2rem', display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}> | |
| <div style={{ display: 'flex', alignItems: 'center', gap: '1.5rem', flex: 1, minWidth: 0 }}> | |
| <div onClick={() => navigate('/projects')} style={{ width: '36px', height: '36px', borderRadius: '8px', background: 'rgba(255,255,255,0.05)', display: 'flex', alignItems: 'center', justifyContent: 'center', cursor: 'pointer', transition: '0.2s', border: '1px solid rgba(255,255,255,0.1)', flexShrink: 0 }} className="hover-lift"> | |
| <ArrowLeft size={18} color="var(--text-secondary)" /> | |
| </div> | |
| <div style={{ display: 'flex', flexDirection: 'column', gap: '0.2rem', paddingLeft: '0.5rem', flex: 1, minWidth: 0 }}> | |
| <h1 title={project.title} style={{ fontSize: '1.3rem', color: 'var(--text-primary)', fontWeight: 800, margin: 0, lineHeight: 1.2, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis', maxWidth: '100%' }}> | |
| {project.title} | |
| </h1> | |
| {(project.external_context?.company_data?.name || project.external_context?.company_data?.nip) && ( | |
| <div | |
| title={project.external_context?.company_data?.name || `NIP: ${project.external_context?.company_data?.nip}`} | |
| style={{ | |
| background: 'rgba(59, 130, 246, 0.1)', | |
| border: '1px solid rgba(59, 130, 246, 0.2)', | |
| color: 'var(--accent-blue)', | |
| padding: '0.3rem 0.6rem', | |
| borderRadius: '6px', | |
| fontSize: '0.8rem', | |
| display: 'inline-flex', | |
| alignItems: 'center', | |
| gap: '0.5rem', | |
| maxWidth: '100%', | |
| marginTop: '0.3rem' | |
| }}> | |
| <Building size={14} style={{flexShrink: 0}} /> | |
| <span style={{ lineHeight: 1.4, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}> | |
| {project.external_context?.company_data?.name || `NIP: ${project.external_context?.company_data?.nip}`} | |
| </span> | |
| </div> | |
| )} | |
| </div> | |
| </div> | |
| <div style={{ display: 'flex', alignItems: 'center', gap: '0.8rem' }}> | |
| <button onClick={() => setIsPreviewOpen(true)} className="btn hover-bg" style={{ background: 'transparent', border: '1px solid rgba(255,255,255,0.1)', color: 'var(--text-secondary)', padding: '0.5rem 1rem', display: 'flex', alignItems: 'center', gap: '0.5rem', borderRadius: '8px', cursor: 'pointer' }}> | |
| <Clock size={16}/> Szybki podgląd | |
| </button> | |
| <button | |
| className="btn" | |
| style={{ background: 'var(--accent-blue)', border: 'none', color: '#fff', padding: '0.5rem 1rem', display: 'flex', alignItems: 'center', gap: '0.5rem', borderRadius: '8px', cursor: 'pointer', fontWeight: 600 }} | |
| onClick={() => setIsExportModalOpen(true)} | |
| > | |
| <Download size={16} /> Eksportuj Wniosek | |
| </button> | |
| </div> | |
| </div> | |
| {/* Pasek zakładek */} | |
| <div style={{ padding: '0 2rem', display: 'flex', gap: '2rem', overflowX: 'auto', whiteSpace: 'nowrap' }} className="hide-scrollbar"> | |
| <button | |
| onClick={() => setActiveMainTab('overview')} | |
| style={{ padding: '1rem 0', background: 'transparent', border: 'none', borderBottom: activeMainTab === 'overview' ? '2px solid var(--accent-blue)' : '2px solid transparent', color: activeMainTab === 'overview' ? 'var(--accent-blue)' : 'var(--text-secondary)', fontWeight: activeMainTab === 'overview' ? 700 : 500, cursor: 'pointer', fontSize: '0.95rem', transition: '0.2s' }} | |
| > | |
| Podsumowanie | |
| </button> | |
| <button | |
| onClick={() => setActiveMainTab('sections')} | |
| style={{ padding: '1rem 0', background: 'transparent', border: 'none', borderBottom: activeMainTab === 'sections' ? '2px solid var(--accent-blue)' : '2px solid transparent', color: activeMainTab === 'sections' ? 'var(--accent-blue)' : 'var(--text-secondary)', fontWeight: activeMainTab === 'sections' ? 700 : 500, cursor: 'pointer', fontSize: '0.95rem', transition: '0.2s' }} | |
| > | |
| Sekcje wniosku | |
| </button> | |
| <button | |
| onClick={() => setActiveMainTab('final')} | |
| style={{ padding: '1rem 0', background: 'transparent', border: 'none', borderBottom: activeMainTab === 'final' ? '2px solid var(--accent-blue)' : '2px solid transparent', color: activeMainTab === 'final' ? 'var(--accent-blue)' : 'var(--text-secondary)', fontWeight: activeMainTab === 'final' ? 700 : 500, cursor: 'pointer', fontSize: '0.95rem', transition: '0.2s' }} | |
| > | |
| Wniosek Końcowy | |
| </button> | |
| <button | |
| onClick={() => setActiveMainTab('resources')} | |
| style={{ padding: '1rem 0', background: 'transparent', border: 'none', borderBottom: activeMainTab === 'resources' ? '2px solid var(--accent-blue)' : '2px solid transparent', color: activeMainTab === 'resources' ? 'var(--accent-blue)' : 'var(--text-secondary)', fontWeight: activeMainTab === 'resources' ? 700 : 500, cursor: 'pointer', fontSize: '0.95rem', transition: '0.2s' }} | |
| > | |
| Zasoby | |
| </button> | |
| <button | |
| onClick={() => setActiveMainTab('verify')} | |
| style={{ padding: '1rem 0', background: 'transparent', border: 'none', borderBottom: activeMainTab === 'verify' ? '2px solid var(--accent-blue)' : '2px solid transparent', color: activeMainTab === 'verify' ? 'var(--accent-blue)' : 'var(--text-secondary)', fontWeight: activeMainTab === 'verify' ? 700 : 500, cursor: 'pointer', fontSize: '0.95rem', transition: '0.2s' }} | |
| > | |
| Weryfikator Projektu | |
| {hasUnapproved && <span style={{ marginLeft: '6px', width: '8px', height: '8px', background: 'var(--accent-red)', borderRadius: '50%', display: 'inline-block' }}></span>} | |
| </button> | |
| <button | |
| onClick={() => setActiveMainTab('audit')} | |
| style={{ padding: '1rem 0', background: 'transparent', border: 'none', borderBottom: activeMainTab === 'audit' ? '2px solid var(--accent-blue)' : '2px solid transparent', color: activeMainTab === 'audit' ? 'var(--accent-blue)' : 'var(--text-secondary)', fontWeight: activeMainTab === 'audit' ? 700 : 500, cursor: 'pointer', fontSize: '0.95rem', transition: '0.2s' }} | |
| > | |
| Audyt wniosku | |
| </button> | |
| <button | |
| onClick={() => setActiveMainTab('generator')} | |
| style={{ padding: '1rem 0', background: 'transparent', border: 'none', borderBottom: activeMainTab === 'generator' ? '2px solid var(--accent-purple)' : '2px solid transparent', color: activeMainTab === 'generator' ? 'var(--accent-purple)' : 'var(--text-secondary)', fontWeight: activeMainTab === 'generator' ? 800 : 500, cursor: 'pointer', fontSize: '0.95rem', transition: '0.2s', display: 'flex', alignItems: 'center', gap: '0.5rem' }} | |
| > | |
| <Sparkles size={16} /> Autopilot AI | |
| </button> | |
| <button | |
| onClick={() => setActiveMainTab('documents')} | |
| style={{ padding: '1rem 0', background: 'transparent', border: 'none', borderBottom: activeMainTab === 'documents' ? '2px solid #a78bfa' : '2px solid transparent', color: activeMainTab === 'documents' ? '#a78bfa' : 'var(--text-secondary)', fontWeight: activeMainTab === 'documents' ? 700 : 500, cursor: 'pointer', fontSize: '0.95rem', transition: '0.2s', display: 'flex', alignItems: 'center', gap: '0.5rem' }} | |
| > | |
| <Database size={15} /> Dokumenty RAG | |
| </button> | |
| </div> | |
| </div> | |
| {/* DYNAMIC MAIN CONTENT */} | |
| <div style={{ flex: 1, overflowY: 'auto' }}> | |
| {renderMainTabContent()} | |
| </div> | |
| <AnimatePresence> | |
| {isPreviewOpen && ( | |
| <LivePreview projectId={project.id} onClose={() => setIsPreviewOpen(false)} /> | |
| )} | |
| </AnimatePresence> | |
| <ExportModal | |
| isOpen={isExportModalOpen} | |
| onClose={() => setIsExportModalOpen(false)} | |
| projectId={project.id} | |
| /> | |
| <ProjectQAPanel projectId={project.id} projectName={project.title} /> | |
| <ProjectChatPanel | |
| projectId={project.id} | |
| projectName={project.title} | |
| activeSectionId={activeSectionId || undefined} | |
| activeSectionTitle={activeSectionId ? sectionsData.find(s => s.section_type === activeSectionId)?.title || activeSectionId : undefined} | |
| /> | |
| {runTour && ( | |
| <OnboardingTour | |
| run={runTour} | |
| steps={workspaceTourSteps} | |
| onFinish={() => { | |
| setRunTour(false); | |
| localStorage.setItem('has_seen_tour_workspace', 'true'); | |
| }} | |
| /> | |
| )} | |
| <style>{`.hide-scrollbar::-webkit-scrollbar { display: none; } .hide-scrollbar { -ms-overflow-style: none; scrollbar-width: none; } .hover-bg:hover { background: rgba(255,255,255,0.03) ; color: #fff ; }`}</style> | |
| </div> | |
| ); | |
| }; | |
| export default ProjectWorkspace; | |