grantforge-api / frontend-react /src /components /project /ProjectWorkspace.tsx
GrantForge Bot
Deploy to Hugging Face
afd56bc
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) !important; color: #fff !important; }`}</style>
</div>
);
};
export default ProjectWorkspace;