import React, { useState, useEffect, useRef } from 'react'; import { motion, AnimatePresence } from 'framer-motion'; import { OnboardingTour } from '../onboarding/OnboardingTour'; import { workspaceTourSteps } from '../onboarding/onboardingTours'; import { hasSeenTour, markTourSeen } from '../../utils/onboardingStorage'; import { ArrowLeft, Download, Clock, Building, CheckCircle, ShieldAlert, ShieldCheck, 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 PostAwardPanel from './PostAwardPanel'; import MatchingGrantsWidget from './MatchingGrantsWidget'; import StrategyCascadePanel from '../company/StrategyCascadePanel'; import { getProjectSections, attachRegulationUrl, updateProject } from '../../api/client'; import { GsdHitlBanner } from './GsdHitlBanner'; interface WorkspaceProps { project: any; statusLabel: string; onRefresh: () => void; } const ProjectWorkspace: React.FC = ({ 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(null); const [sectionsData, setSectionsData] = useState([]); const [isPreviewOpen, setIsPreviewOpen] = useState(false); const [isExportModalOpen, setIsExportModalOpen] = useState(false); const [isLoading, setIsLoading] = useState(true); const [isSidebarOpen, setIsSidebarOpen] = useState(true); const [regulationUrlInput, setRegulationUrlInput] = useState(''); const [regulationLoading, setRegulationLoading] = useState(false); 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; if (!isLoading && sectionsData.length > 0) { if (!hasSeenTour('workspace')) { 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: any = 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 workspaceTourStepsLocal = workspaceTourSteps; 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 regulationPending = project?.external_context?.regulation_pending === true; const regulationResolved = Boolean( project?.external_context?.regulation_snapshot_id || (project?.external_context?.required_sections?.length > 0 && !project?.external_context?.regulation_pending) ); const pendingGsdHitl = project?.external_context?.gsd_pending_hitl || project?.gsd?.hitl_question; const handleAttachRegulation = async () => { if (!regulationUrlInput.trim().startsWith('http')) return; setRegulationLoading(true); try { await attachRegulationUrl(project.id, regulationUrlInput.trim()); setRegulationUrlInput(''); onRefresh(); window.dispatchEvent(new Event('refresh-sections')); } catch (e) { console.error('Nie udało się pobrać regulaminu', e); } finally { setRegulationLoading(false); } }; const renderMainTabContent = () => { switch(activeMainTab) { case 'overview': return (
{regulationPending && (
Brak regulaminu programu

Podaj link do regulaminu naboru, aby wygenerować sekcje wniosku zgodne z wymaganiami programu.

setRegulationUrlInput(e.target.value)} style={{ flex: 1, minWidth: '200px', padding: '0.5rem 0.75rem', borderRadius: '6px', border: '1px solid rgba(255,255,255,0.15)', background: 'rgba(0,0,0,0.2)', color: '#fff' }} />
)} {!regulationPending && regulationResolved && (
Regulamin pobrany ({project.external_context?.regulation_source || 'snapshot'})
)}

Podsumowanie projektu

Metadane klienta i wniosku

{/* Cycle 11: Prominent Trust Score for end users */} {project?.trust_score !== undefined && (
80 ? '#10b981' : project.trust_score > 65 ? '#f59e0b' : '#ef4444', color: 'white', padding: '4px 12px', borderRadius: '9999px', fontSize: '0.75rem', fontWeight: 700, display: 'flex', alignItems: 'center', gap: '4px' }}> Trust Score: {project.trust_score}/100 Regulation Grounded
)}
Nazwa firmy / Opis{project.external_context?.company_data?.name || 'Brak wpisanego opisu firmy'}
NIP Firmy{project.external_context?.company_data?.nip || 'Brak NIP'}
Data utworzenia projektu{new Date(project.created_at).toLocaleDateString('pl-PL')}
Program dotacyjny{project.program_name || 'Brak wybranego programu'}
Maksymalna kwota dofinansowania{project.external_context?.grant_amount ? project.external_context.grant_amount : (project.grant_amount_max ? `${project.grant_amount_max} PLN` : 'Nie określono')}
Status
{statusLabel}

Postęp Wniosku

{sectionsData.filter(s => s.content && s.content.length > 50).length}
Gotowe sekcje dokumentu
z {sectionsData.length} wszystkich sekcji

Weryfikacja

{isFullyApproved ? (

Wszystkie wypełnione sekcje zostały zatwierdzone przez doradcę. Dokument jest spójny.

) : hasUnapproved ? (

Projekt zawiera sekcje bez ostatecznej akceptacji doradcy. Sprawdź je przed wygenerowaniem PDF.

) : (

Rozpocznij wypełnianie wniosku, aby sprawdzić jego poprawność.

)}
{/* Strategy cascade 2026 — before match */}
) || undefined } mode="auto" compact selectedPathId={ (project.external_context?.strategy_path as string) || null } onPathSelect={async (pathId) => { try { await updateProject(project.id, { external_context: { ...(project.external_context || {}), strategy_path: pathId, }, }); onRefresh?.(); } catch { /* non-blocking */ } }} />
{/* Widget: Pasujące nabory */}

Pasujące Nabory

); case 'sections': return (
{isLoading ? (
) : ( {activeSectionId && ( )} )}
); case 'final': return (
); case 'verify': return (

Weryfikator Projektu

Pełne podsumowanie statusu zatwierdzenia przez doradcę na każdym etapie pisania dokumentacji.

{sectionsData.length === 0 &&

Brak sekcji.

}
{sectionsData.map(s => { const hasContent = s.content && s.content.length > 50; return (

{s.title}

{hasContent ? 'Sekcja posiada treść' : 'Brak treści / Wymaga uzupełnienia'}
{s.is_approved ? ( ZATWIERDZONE ) : hasContent ? ( OCZEKUJE NA WERYFIKACJĘ ) : ( PUSTE )}
) })}
); case 'audit': return (
); case 'resources': return (
); case 'generator': return (
{ onRefresh(); setTimeout(() => { loadSections(); // Pełny autopilot → pokaż gotowy wniosek końcowy zamiast wracać do poprzedniej karty setActiveMainTab(opts?.fullAutopilot ? 'final' : previousTabRef.current); }, 800); }} />
); case 'documents': return (
); case 'post_award': return (
); } }; return (
{/* CONTEXTUAL TOP BAR (STICKY) */}
{/* Górny pasek - nazwa i powrót */}
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">

{project.title}

{/* Cycle 15: Prominent user-facing Trust / Credibility indicator */} {project?.trust_score !== undefined && (
80 ? 'rgba(16,185,129,0.15)' : project.trust_score > 65 ? 'rgba(245,158,11,0.15)' : 'rgba(239,68,68,0.15)', color: project.trust_score > 80 ? '#10b981' : project.trust_score > 65 ? '#f59e0b' : '#ef4444', padding: '2px 10px', borderRadius: '999px', fontSize: '0.7rem', fontWeight: 700, border: `1px solid ${project.trust_score > 80 ? '#10b981' : project.trust_score > 65 ? '#f59e0b' : '#ef4444'}` }}> Trust {project.trust_score}/100
)} {(project.external_context?.company_data?.name || project.external_context?.company_data?.nip) && (
{project.external_context?.company_data?.name || `NIP: ${project.external_context?.company_data?.nip}`}
)}
{pendingGsdHitl && (
)} {/* Pasek zakładek */}
{/* DYNAMIC MAIN CONTENT */}
{renderMainTabContent()}
{isPreviewOpen && ( setIsPreviewOpen(false)} /> )} setIsExportModalOpen(false)} projectId={project.id} /> s.section_type === activeSectionId)?.title || activeSectionId : undefined} /> {runTour && ( { setRunTour(false); markTourSeen('workspace'); }} /> )}
); }; export default ProjectWorkspace;