Spaces:
Running
Running
| import React, { useState, useEffect, useRef } from 'react'; | |
| import { ShieldAlert, RefreshCw, AlertTriangle, CheckCircle, Info, ChevronRight, Check, Bot, Sparkles, FileText, Activity, DollarSign, ShieldCheck, AlertCircle } from 'lucide-react'; | |
| import { getProjectAudit, runProjectAudit, getProjectSections, autofixProjectSection, clearGlobalAudit, getHolisticReview, runHolisticReview } from '../../api/client'; | |
| import { motion, AnimatePresence } from 'framer-motion'; | |
| import toast from 'react-hot-toast'; | |
| interface AuditPanelProps { | |
| projectId: string; | |
| } | |
| const normalizeHolisticReport = (report: any) => { | |
| if (!report || report.status === 'pending' || report.status === 'error') { | |
| return report; | |
| } | |
| const categoryText = (key: string) => { | |
| const value = report[key]; | |
| if (typeof value === 'string') return value; | |
| if (value && typeof value === 'object') { | |
| const parts = [ | |
| value.feedback, | |
| value.xai_justification, | |
| ...(value.inconsistencies_flagged || []).map((item: string) => `- ${item}`), | |
| ].filter(Boolean); | |
| return parts.join('\n\n'); | |
| } | |
| return ''; | |
| }; | |
| const criticalFlaws = report.critical_flaws || ['dnsh_assessment', 'budget_consistency', 'logical_flow', 'program_alignment'] | |
| .flatMap((key: string) => { | |
| const value = report[key]; | |
| return value && typeof value === 'object' ? (value.inconsistencies_flagged || []) : []; | |
| }) | |
| .filter(Boolean); | |
| return { | |
| ...report, | |
| overall_assessment: report.overall_assessment || report.xai_justification_overall || '', | |
| score: report.score ?? report.overall_score ?? 0, | |
| dnsh_compliance: report.dnsh_compliance || categoryText('dnsh_assessment'), | |
| budget_consistency: typeof report.budget_consistency === 'string' | |
| ? report.budget_consistency | |
| : categoryText('budget_consistency'), | |
| logical_flow: typeof report.logical_flow === 'string' | |
| ? report.logical_flow | |
| : categoryText('logical_flow'), | |
| program_alignment: typeof report.program_alignment === 'string' | |
| ? report.program_alignment | |
| : categoryText('program_alignment'), | |
| recommendations: report.recommendations || report.key_recommendations || [], | |
| critical_flaws: criticalFlaws, | |
| }; | |
| }; | |
| const ProjectAuditPanel: React.FC<AuditPanelProps> = ({ projectId }) => { | |
| const [activeTab, setActiveTab] = useState<'audit' | 'holistic'>('audit'); | |
| const [audit, setAudit] = useState<any>(null); | |
| const [isLoading, setIsLoading] = useState(true); | |
| const [isRunning, setIsRunning] = useState(false); | |
| const [fixProgress, setFixProgress] = useState<{current: number, total: number} | null>(null); | |
| const [expandedIssue, setExpandedIssue] = useState<number | null>(null); | |
| const pollIntervalRef = useRef<NodeJS.Timeout | null>(null); | |
| const pollAttemptsRef = useRef(0); | |
| const MAX_POLL_ATTEMPTS = 72; // ~6 min przy interwale 5s | |
| const [holisticReview, setHolisticReview] = useState<any>(null); | |
| const [isHolisticLoading, setIsHolisticLoading] = useState(false); | |
| const [isHolisticRunning, setIsHolisticRunning] = useState(false); | |
| const pollHolisticIntervalRef = useRef<NodeJS.Timeout | null>(null); | |
| const loadAudit = async () => { | |
| try { | |
| setIsLoading(true); | |
| const res = await getProjectAudit(projectId); | |
| if (res && res.status === "pending") { | |
| setIsRunning(true); | |
| startPolling(); | |
| } else if (res && res.status === "completed") { | |
| setAudit(res); | |
| setIsRunning(false); | |
| } else if (res && res.status === "error") { | |
| setAudit(null); | |
| setIsRunning(false); | |
| } else if (res && res.status !== "no_audit") { | |
| setAudit(res); | |
| } else { | |
| setAudit(null); | |
| } | |
| } catch (e) { | |
| console.error("Failed to load audit", e); | |
| } finally { | |
| setIsLoading(false); | |
| } | |
| }; | |
| const loadHolisticReview = async () => { | |
| try { | |
| setIsHolisticLoading(true); | |
| const res = await getHolisticReview(projectId); | |
| if (res && res.status === "pending") { | |
| setIsHolisticRunning(true); | |
| startHolisticPolling(); | |
| } else if (res && res.status === "completed" && res.report) { | |
| setHolisticReview(normalizeHolisticReport(res.report)); | |
| } else { | |
| setHolisticReview(null); | |
| } | |
| } catch (e) { | |
| console.error("Failed to load holistic review", e); | |
| } finally { | |
| setIsHolisticLoading(false); | |
| } | |
| }; | |
| const clearPolling = () => { | |
| if (pollIntervalRef.current) { | |
| clearInterval(pollIntervalRef.current); | |
| pollIntervalRef.current = null; | |
| } | |
| }; | |
| const startPolling = () => { | |
| clearPolling(); | |
| pollAttemptsRef.current = 0; | |
| pollIntervalRef.current = setInterval(async () => { | |
| pollAttemptsRef.current += 1; | |
| if (pollAttemptsRef.current > MAX_POLL_ATTEMPTS) { | |
| clearPolling(); | |
| setIsRunning(false); | |
| toast.error('Audyt trwa zbyt długo. Spróbuj ponownie za chwilę.'); | |
| return; | |
| } | |
| try { | |
| const auditRes = await getProjectAudit(projectId); | |
| if (auditRes && auditRes.status === "completed") { | |
| clearPolling(); | |
| setAudit(auditRes); | |
| setIsRunning(false); | |
| toast.success('Audyt zakończony!'); | |
| } else if (auditRes && auditRes.status === "error") { | |
| clearPolling(); | |
| toast.error(`Błąd: ${auditRes.message || 'Wystąpił błąd podczas audytu'}`); | |
| setIsRunning(false); | |
| setAudit(null); | |
| } else if (auditRes && auditRes.status === "no_audit") { | |
| clearPolling(); | |
| setIsRunning(false); | |
| setAudit(null); | |
| } | |
| } catch (e) { | |
| console.error("Polling error", e); | |
| } | |
| }, 5000); | |
| }; | |
| const clearHolisticPolling = () => { | |
| if (pollHolisticIntervalRef.current) { | |
| clearInterval(pollHolisticIntervalRef.current); | |
| pollHolisticIntervalRef.current = null; | |
| } | |
| }; | |
| const startHolisticPolling = () => { | |
| clearHolisticPolling(); | |
| pollHolisticIntervalRef.current = setInterval(async () => { | |
| try { | |
| const res = await getHolisticReview(projectId); | |
| if (res && res.status === "completed") { | |
| clearHolisticPolling(); | |
| setHolisticReview(normalizeHolisticReport(res.report)); | |
| setIsHolisticRunning(false); | |
| toast.success('Raport Spójności wygenerowany pomyślnie!', { id: 'holistic' }); | |
| } else if (res && res.status === "error") { | |
| clearHolisticPolling(); | |
| toast.error(`Błąd: ${res.message || 'Wystąpił błąd podczas generowania raportu'}`, { id: 'holistic' }); | |
| setIsHolisticRunning(false); | |
| setHolisticReview(null); | |
| } | |
| } catch (e) { | |
| console.error("Holistic Polling error", e); | |
| } | |
| }, 5000); | |
| }; | |
| useEffect(() => { | |
| loadAudit(); | |
| loadHolisticReview(); | |
| return () => { | |
| clearPolling(); | |
| clearHolisticPolling(); | |
| }; | |
| }, [projectId]); | |
| const handleRunAudit = async () => { | |
| try { | |
| setIsRunning(true); | |
| const res = await runProjectAudit(projectId); | |
| if (res && res.status === "pending") { | |
| toast.success('Audyt został uruchomiony w tle. Proszę czekać...'); | |
| startPolling(); | |
| } else { | |
| setAudit(res); | |
| setIsRunning(false); | |
| toast.success('Audyt zakończony!'); | |
| } | |
| } catch (e: any) { | |
| let msg = e?.response?.data?.detail || 'Wystąpił błąd podczas audytu.'; | |
| if (typeof msg !== 'string') msg = JSON.stringify(msg); | |
| toast.error(msg); | |
| console.error('Failed to run audit', e); | |
| setIsRunning(false); | |
| } | |
| }; | |
| const handleQualityCycle = async () => { | |
| try { | |
| setIsHolisticRunning(true); | |
| toast.loading('Krytyk całości + korekta wszystkich słabych sekcji...', { id: 'quality-cycle' }); | |
| const { runDocumentQualityCycle } = await import('../../api/client'); | |
| const res = await runDocumentQualityCycle(projectId, 2); | |
| toast.success( | |
| res.message || | |
| `Cykl jakości: score ${res.holistic?.overall_score ?? '—'} · gotowość ${res.readiness?.score ?? '—'}%`, | |
| { id: 'quality-cycle', duration: 7000 }, | |
| ); | |
| // Refresh holistic view | |
| try { | |
| const h = await getHolisticReview(projectId); | |
| if (h?.report) setHolisticReview(normalizeHolisticReport(h.report)); | |
| else if (res.holistic) setHolisticReview(normalizeHolisticReport(res.holistic as any)); | |
| } catch { | |
| if (res.holistic) setHolisticReview(normalizeHolisticReport(res.holistic as any)); | |
| } | |
| window.dispatchEvent(new Event('refresh-sections')); | |
| } catch (e: any) { | |
| const msg = e?.response?.data?.detail || 'Błąd cyklu jakości.'; | |
| toast.error(typeof msg === 'string' ? msg : JSON.stringify(msg), { id: 'quality-cycle' }); | |
| } finally { | |
| setIsHolisticRunning(false); | |
| } | |
| }; | |
| const handleRunHolisticReview = async () => { | |
| try { | |
| setIsHolisticRunning(true); | |
| toast.loading('Generowanie Raportu Spójności...', { id: 'holistic' }); | |
| const res = await runHolisticReview(projectId); | |
| if (res && res.status === "pending") { | |
| startHolisticPolling(); | |
| } else if (res && res.status === "completed" && res.report) { | |
| setHolisticReview(normalizeHolisticReport(res.report)); | |
| setIsHolisticRunning(false); | |
| toast.success('Raport Spójności wygenerowany pomyślnie!', { id: 'holistic' }); | |
| } else { | |
| setIsHolisticRunning(false); | |
| toast.error('Błąd generowania raportu.', { id: 'holistic' }); | |
| } | |
| } catch (e: any) { | |
| let msg = e?.response?.data?.detail || 'Wystąpił błąd podczas generowania raportu.'; | |
| if (typeof msg !== 'string') msg = JSON.stringify(msg); | |
| toast.error(msg, { id: 'holistic' }); | |
| console.error('Failed to run holistic review', e); | |
| setIsHolisticRunning(false); | |
| } | |
| }; | |
| const handleAutofix = async () => { | |
| const t = toast.loading('Przygotowywanie automatycznej poprawki...'); | |
| try { | |
| const sections = await getProjectSections(projectId); | |
| const validSections = sections.filter((s:any) => s.content && s.content.trim().length > 0); | |
| if (validSections.length === 0) { | |
| toast.dismiss(t); | |
| toast.error('Brak sekcji z treścią do poprawy.'); | |
| return; | |
| } | |
| toast.dismiss(t); | |
| setFixProgress({ current: 0, total: validSections.length }); | |
| let failedCount = 0; | |
| for (let i = 0; i < validSections.length; i++) { | |
| setFixProgress({ current: i + 1, total: validSections.length }); | |
| let success = false; | |
| let retries = 0; | |
| const maxRetries = 4; // Increased from 3 to 4 for better resilience | |
| while (!success && retries < maxRetries) { | |
| try { | |
| await autofixProjectSection(projectId, validSections[i].id); | |
| success = true; | |
| } catch (err: any) { | |
| const status = err?.response?.status; | |
| console.warn(`[Autofix] Error for section ${validSections[i].id}:`, err?.message || err); | |
| retries++; | |
| if (retries < maxRetries) { | |
| // Exponential Backoff: base = 5000ms | |
| // Try 1: 5s, Try 2: 10s, Try 3: 20s | |
| let baseDelay = Math.pow(2, retries - 1) * 5000; | |
| // If API explicitely returns 429, wait even longer | |
| if (status === 429) { | |
| baseDelay = Math.pow(2, retries) * 5000; | |
| toast(`Oczekiwanie na reset limitów AI... (próba ${retries}/${maxRetries})`, { icon: '⏳', id: 'ai-limit' }); | |
| } | |
| // Jitter to prevent thundering herd (1000 - 3000 ms) | |
| const jitter = Math.floor(Math.random() * 2000) + 1000; | |
| const totalDelay = baseDelay + jitter; | |
| console.log(`[Autofix] Exponential backoff: waiting ${totalDelay}ms before retry ${retries}/${maxRetries}...`); | |
| await new Promise(resolve => setTimeout(resolve, totalDelay)); | |
| } else { | |
| failedCount++; | |
| } | |
| } | |
| } | |
| if (i < validSections.length - 1) { | |
| await new Promise(resolve => setTimeout(resolve, 3000)); | |
| } | |
| } | |
| if (failedCount === 0) { | |
| await clearGlobalAudit(projectId); | |
| setAudit(null); | |
| toast.success('Wniosek został automatycznie poprawiony. Zalecamy wykonanie nowego audytu.', { duration: 5000 }); | |
| } else if (failedCount < validSections.length) { | |
| toast.success(`Poprawiono część sekcji. Nie udało się poprawić ${failedCount} sekcji. Spróbuj ponownie za chwilę.`, { duration: 6000 }); | |
| } else { | |
| toast.error('Nie udało się poprawić żadnej sekcji z powodu błędów.'); | |
| } | |
| try { | |
| const updatedSections = await getProjectSections(projectId); | |
| updatedSections.forEach((s: any) => { | |
| window.dispatchEvent(new CustomEvent('external-section-update', { | |
| detail: { sectionType: s.section_type, content: s.content } | |
| })); | |
| }); | |
| } catch (e) { | |
| console.error("Failed to fetch updated sections after autofix", e); | |
| } | |
| window.dispatchEvent(new CustomEvent('refresh-sections')); | |
| } catch (e) { | |
| toast.dismiss(t); | |
| console.error('Autofix setup failed', e); | |
| toast.error('Wystąpił błąd podczas uruchamiania automatycznej naprawy dokumentu.'); | |
| } finally { | |
| setFixProgress(null); | |
| } | |
| }; | |
| const handleGoToSection = (sectionType: string | undefined) => { | |
| if (!sectionType) return; | |
| window.dispatchEvent(new CustomEvent('navigate-to-section', { | |
| detail: { sectionType } | |
| })); | |
| }; | |
| const getSeverityColor = (sev: string) => { | |
| if (sev === 'critical') return 'var(--accent-red)'; | |
| if (sev === 'high') return 'var(--accent-yellow)'; | |
| return 'var(--accent-blue)'; | |
| }; | |
| const getSeverityIcon = (sev: string) => { | |
| if (sev === 'critical') return <ShieldAlert size={20} />; | |
| if (sev === 'high') return <AlertTriangle size={20} />; | |
| return <Info size={20} />; | |
| }; | |
| const getSeverityLabel = (sev: string) => { | |
| if (sev === 'critical') return 'Krytyczny Błąd'; | |
| if (sev === 'high') return 'Poważne Ostrzeżenie'; | |
| return 'Sugestia / Medium'; | |
| }; | |
| const renderAuditTab = () => { | |
| if (isLoading) { | |
| return <div style={{ padding: '3rem', color: 'var(--text-muted)', textAlign: 'center' }}>Ładowanie raportu audytu...</div>; | |
| } | |
| if (fixProgress) { | |
| return ( | |
| <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', padding: '6rem 2rem', color: '#8b5cf6' }}> | |
| <Sparkles size={56} className="spin" style={{ marginBottom: '1.5rem', filter: 'drop-shadow(0 0 10px rgba(139, 92, 246, 0.4))' }} /> | |
| <h3 style={{ margin: '0 0 1rem 0', color: 'var(--text-primary)' }}>Trwa automatyczna poprawa wniosku…</h3> | |
| <p style={{ color: 'var(--text-muted)', fontSize: '1.2rem', marginBottom: '1rem' }}> | |
| Sekcja {fixProgress.current} z {fixProgress.total} | |
| </p> | |
| <div style={{ marginTop: '1rem', width: '300px', height: '6px', background: 'rgba(255,255,255,0.05)', borderRadius: '3px', overflow: 'hidden' }}> | |
| <motion.div | |
| initial={{ width: 0 }} | |
| animate={{ width: `${(fixProgress.current / fixProgress.total) * 100}%` }} | |
| transition={{ duration: 0.3 }} | |
| style={{ height: '100%', background: 'linear-gradient(90deg, #6366f1, #8b5cf6)' }} | |
| /> | |
| </div> | |
| <p style={{ marginTop: '2rem', fontSize: '0.85rem', color: 'var(--text-muted)', maxWidth: '400px', textAlign: 'center' }}> | |
| Może to potrwać 30–60 sekund w zależności od długości dokumentu. Prosimy nie odświeżać okna. | |
| </p> | |
| </div> | |
| ); | |
| } | |
| if (isRunning) { | |
| return ( | |
| <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', padding: '6rem 2rem', color: 'var(--accent-blue)' }}> | |
| <RefreshCw size={48} className="spin" style={{ marginBottom: '1.5rem' }} /> | |
| <h3 style={{ margin: '0 0 1rem 0', color: 'var(--text-primary)' }}>Trwa szczegółowy audyt sekcji</h3> | |
| <p style={{ color: 'var(--text-muted)', textAlign: 'center', maxWidth: '400px' }}>Sprawdzanie reguł dla poszczególnych akapitów i generowanie rekomendacji...</p> | |
| <div style={{ marginTop: '2rem', width: '200px', height: '4px', background: 'rgba(255,255,255,0.1)', borderRadius: '2px', overflow: 'hidden' }}> | |
| <motion.div | |
| initial={{ x: '-100%' }} | |
| animate={{ x: '100%' }} | |
| transition={{ repeat: Infinity, duration: 2, ease: "linear" }} | |
| style={{ width: '50%', height: '100%', background: 'linear-gradient(90deg, transparent, var(--accent-blue), transparent)' }} | |
| /> | |
| </div> | |
| </div> | |
| ); | |
| } | |
| if (!audit) { | |
| return ( | |
| <div className="glass-card" style={{ padding: '6rem 2rem', textAlign: 'center', color: 'var(--text-muted)' }}> | |
| <ShieldAlert size={64} color="rgba(255,255,255,0.2)" style={{ marginBottom: '1.5rem' }} /> | |
| <h3 style={{ color: 'var(--text-primary)', margin: '0 0 1rem 0', fontSize: '1.5rem' }}>Brak Danych Audytowych</h3> | |
| <p style={{ margin: '0 auto 2rem auto', maxWidth: '500px', lineHeight: 1.6 }}>Ten projekt nie został jeszcze sprawdzony przez Szczegółowego Audytora. Uruchom proces, aby wykryć drobne niespójności i błędy językowe.</p> | |
| <button | |
| className="btn btn-primary" | |
| onClick={handleRunAudit} | |
| style={{ background: 'var(--accent-blue)', color: '#fff', border: 'none', display: 'inline-flex', alignItems: 'center', gap: '0.8rem', padding: '1rem 2rem', fontWeight: 600, borderRadius: '8px', fontSize: '1.1rem' }} | |
| > | |
| <RefreshCw size={20} /> Uruchom Szczegółowy Audyt | |
| </button> | |
| </div> | |
| ); | |
| } | |
| return ( | |
| <div style={{ display: 'grid', gridTemplateColumns: '1fr 300px', gap: '1.5rem', alignItems: 'start' }}> | |
| <div style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}> | |
| <h3 style={{ margin: '0 0 0.5rem 0' }}>Wykryte Zagrożenia i Uwagi ({audit.issues?.length || 0})</h3> | |
| {!audit.issues || audit.issues.length === 0 ? ( | |
| <div className="glass-card" style={{ padding: '2rem', textAlign: 'center', color: 'var(--accent-green)', background: 'rgba(16, 185, 129, 0.05)' }}> | |
| <CheckCircle size={48} style={{ marginBottom: '1rem' }} /> | |
| <h3>Wniosek jest idealny!</h3> | |
| <p style={{ color: 'var(--text-muted)' }}>Audytor nie wykrył żadnych nieprawidłowości, kosztów niekwalifikowalnych czy niespójności.</p> | |
| </div> | |
| ) : ( | |
| <div className="glass-card" style={{ padding: 0, overflow: 'hidden' }}> | |
| {audit.issues?.map((issue: any, idx: number) => ( | |
| <div key={idx} style={{ borderBottom: idx !== audit.issues.length - 1 ? '1px solid rgba(255,255,255,0.05)' : 'none' }}> | |
| <div | |
| style={{ padding: '1.5rem', display: 'flex', gap: '1rem', cursor: 'pointer', transition: '0.2s', background: expandedIssue === idx ? 'rgba(255,255,255,0.02)' : 'transparent' }} | |
| className="hover-bg-subtle" | |
| onClick={() => setExpandedIssue(expandedIssue === idx ? null : idx)} | |
| > | |
| <div style={{ color: getSeverityColor(issue.severity), marginTop: '0.2rem' }}> | |
| {getSeverityIcon(issue.severity)} | |
| </div> | |
| <div style={{ flex: 1 }}> | |
| <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', marginBottom: '0.5rem' }}> | |
| <div style={{ display: 'flex', flexWrap: 'wrap', alignItems: 'center', gap: '0.8rem' }}> | |
| <span style={{ fontWeight: 600, color: '#fff' }}>{issue.category}</span> | |
| <span style={{ fontSize: '0.75rem', padding: '0.1rem 0.5rem', borderRadius: '4px', border: `1px solid ${getSeverityColor(issue.severity)}`, color: getSeverityColor(issue.severity), background: 'rgba(0,0,0,0.2)' }}> | |
| {getSeverityLabel(issue.severity)} | |
| </span> | |
| {issue.affected_section && ( | |
| <span style={{ fontSize: '0.75rem', padding: '0.1rem 0.5rem', borderRadius: '4px', background: 'rgba(255,255,255,0.1)', color: 'var(--text-secondary)' }}> | |
| Sekcja: {issue.affected_section} | |
| </span> | |
| )} | |
| </div> | |
| <ChevronRight size={18} color="var(--text-muted)" style={{ transform: expandedIssue === idx ? 'rotate(90deg)' : 'rotate(0)', transition: '0.2s' }} /> | |
| </div> | |
| <div style={{ color: 'var(--text-secondary)', fontSize: '0.95rem', lineHeight: 1.5 }}> | |
| {issue.message} | |
| </div> | |
| </div> | |
| </div> | |
| <AnimatePresence> | |
| {expandedIssue === idx && ( | |
| <motion.div | |
| initial={{ height: 0, opacity: 0 }} | |
| animate={{ height: 'auto', opacity: 1 }} | |
| exit={{ height: 0, opacity: 0 }} | |
| style={{ overflow: 'hidden' }} | |
| > | |
| <div style={{ padding: '0 1.5rem 1.5rem 3.5rem', display: 'flex', flexDirection: 'column', gap: '1rem' }}> | |
| {issue.problem_quote && ( | |
| <div style={{ background: 'rgba(0,0,0,0.3)', padding: '1.2rem', borderRadius: '8px', borderLeft: '3px solid var(--accent-red)' }}> | |
| <div style={{ fontSize: '0.8rem', color: 'var(--text-muted)', marginBottom: '0.5rem', textTransform: 'uppercase', letterSpacing: '1px' }}>Zidentyfikowany fragment:</div> | |
| <div style={{ fontStyle: 'italic', fontSize: '0.95rem', color: '#fff', lineHeight: 1.6 }}>"{issue.problem_quote}"</div> | |
| </div> | |
| )} | |
| {issue.recommendation && ( | |
| <div style={{ background: 'rgba(59, 130, 246, 0.1)', padding: '1.2rem', borderRadius: '8px', borderLeft: '3px solid var(--accent-blue)' }}> | |
| <div style={{ fontSize: '0.8rem', color: 'var(--accent-blue)', marginBottom: '0.5rem', textTransform: 'uppercase', letterSpacing: '1px' }}>Rekomendacja Audytora:</div> | |
| <div style={{ fontSize: '0.95rem', color: 'var(--text-primary)', lineHeight: 1.6 }}>{issue.recommendation}</div> | |
| </div> | |
| )} | |
| {issue.rule_citation && ( | |
| <div style={{ fontSize: '0.85rem', color: 'var(--text-muted)', display: 'flex', alignItems: 'flex-start', gap: '0.6rem', padding: '0.5rem 0' }}> | |
| <Info size={16} style={{ marginTop: '0.1rem', flexShrink: 0 }} /> | |
| <div><strong>Reguła / Kwalifikowalność:</strong> {issue.rule_citation}</div> | |
| </div> | |
| )} | |
| <button | |
| className="btn hover-lift" | |
| style={{ | |
| marginTop: '0.5rem', alignSelf: 'flex-start', padding: '0.6rem 1.2rem', | |
| background: 'rgba(99, 102, 241, 0.1)', color: '#818cf8', | |
| border: '1px solid rgba(99, 102, 241, 0.2)', borderRadius: '6px', | |
| display: 'flex', alignItems: 'center', gap: '0.5rem', fontWeight: 600, fontSize: '0.85rem' | |
| }} | |
| onClick={(e) => { | |
| e.stopPropagation(); | |
| window.dispatchEvent(new CustomEvent('open-project-chat', { | |
| detail: { | |
| prefillMessage: `Zauważono problem w sekcji [${issue.affected_section || 'nieznanej'}].\nZarzut audytora: "${issue.message}"\n\nRekomendacja: ${issue.recommendation}\nCytat z wniosku: "${issue.problem_quote}"\n\nProszę, wygeneruj poprawiony tekst dla tej sekcji używając tagu <SUGGESTION section="${issue.affected_section}">.`, | |
| autoSend: true | |
| } | |
| })); | |
| }} | |
| > | |
| <Bot size={16} /> Popraw z Asystentem AI | |
| </button> | |
| {issue.affected_section && ( | |
| <button | |
| className="btn hover-lift" | |
| style={{ | |
| marginTop: '0.5rem', alignSelf: 'flex-start', padding: '0.6rem 1.2rem', | |
| background: 'rgba(16,185,129,0.08)', color: 'var(--accent-green)', | |
| border: '1px solid rgba(16,185,129,0.2)', borderRadius: '6px', | |
| display: 'flex', alignItems: 'center', gap: '0.5rem', fontWeight: 600, fontSize: '0.85rem' | |
| }} | |
| onClick={(e) => { | |
| e.stopPropagation(); | |
| handleGoToSection(issue.affected_section); | |
| }} | |
| > | |
| <ChevronRight size={16} /> Przejdź do sekcji | |
| </button> | |
| )} | |
| </div> | |
| </motion.div> | |
| )} | |
| </AnimatePresence> | |
| </div> | |
| ))} | |
| </div> | |
| )} | |
| </div> | |
| <div style={{ display: 'flex', flexDirection: 'column', gap: '1.5rem' }}> | |
| <div className="glass-card" style={{ padding: '2rem', display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '1rem', textAlign: 'center' }}> | |
| <div style={{ fontSize: '0.9rem', color: 'var(--text-muted)', textTransform: 'uppercase', letterSpacing: '1px' }}>Wynik Audytu Sekcji</div> | |
| <div style={{ | |
| width: '120px', height: '120px', borderRadius: '50%', | |
| background: `conic-gradient(${audit.overall_score >= 80 ? 'var(--accent-green)' : audit.overall_score >= 50 ? 'var(--accent-yellow)' : 'var(--accent-red)'} ${audit.overall_score}%, rgba(255,255,255,0.05) 0)`, | |
| display: 'flex', alignItems: 'center', justifyContent: 'center', | |
| position: 'relative' | |
| }}> | |
| <div style={{ width: '100px', height: '100px', borderRadius: '50%', background: 'var(--bg-glass)', display: 'flex', alignItems: 'center', justifyContent: 'center', flexDirection: 'column' }}> | |
| <span style={{ fontSize: '2.5rem', fontWeight: 800, color: '#fff', lineHeight: 1 }}>{audit.overall_score}</span> | |
| <span style={{ fontSize: '0.8rem', color: 'var(--text-muted)' }}>/ 100</span> | |
| </div> | |
| </div> | |
| <div style={{ marginTop: '1rem', padding: '1.2rem', background: 'rgba(0,0,0,0.2)', borderRadius: '12px', width: '100%' }}> | |
| <div style={{ fontSize: '0.85rem', color: 'var(--text-muted)', marginBottom: '0.8rem' }}>Status Audytu:</div> | |
| <div style={{ | |
| padding: '0.8rem', borderRadius: '8px', fontWeight: 600, display: 'flex', alignItems: 'center', justifyContent: 'center', gap: '0.5rem', textAlign: 'center', lineHeight: 1.4, | |
| background: audit.export_status === 'ok' ? 'rgba(16, 185, 129, 0.1)' : audit.export_status === 'warning' ? 'rgba(245, 158, 11, 0.1)' : 'rgba(239, 68, 68, 0.1)', | |
| color: audit.export_status === 'ok' ? 'var(--accent-green)' : audit.export_status === 'warning' ? 'var(--accent-yellow)' : 'var(--accent-red)' | |
| }}> | |
| {audit.overall_score >= 80 ? ( | |
| <>Niewielkie poprawki.</> | |
| ) : audit.overall_score >= 60 ? ( | |
| <>Wymaga poprawek redakcyjnych.</> | |
| ) : ( | |
| <>Wiele sekcji do poprawy.</> | |
| )} | |
| </div> | |
| </div> | |
| {audit.issues && audit.issues.length > 0 && ( | |
| <div style={{ marginTop: '1rem', width: '100%' }}> | |
| <button | |
| className="btn hover-lift" | |
| onClick={handleAutofix} | |
| style={{ | |
| width: '100%', padding: '1rem', background: 'rgba(139, 92, 246, 0.1)', color: '#c4b5fd', | |
| border: '1px solid rgba(139, 92, 246, 0.25)', borderRadius: '8px', | |
| display: 'flex', alignItems: 'center', gap: '0.8rem', justifyContent: 'center', fontWeight: 'bold' | |
| }} | |
| > | |
| <Sparkles size={18} /> Popraw cały wniosek (Autofix) | |
| </button> | |
| </div> | |
| )} | |
| <button | |
| className="btn btn-primary" | |
| onClick={handleRunAudit} | |
| style={{ width: '100%', marginTop: '0.5rem', background: 'rgba(255,255,255,0.1)', color: '#fff', border: '1px solid rgba(255,255,255,0.2)', display: 'inline-flex', alignItems: 'center', justifyContent: 'center', gap: '0.8rem', padding: '0.8rem 1.5rem', fontWeight: 600, borderRadius: '8px' }} | |
| > | |
| <RefreshCw size={16} /> Ponów Audyt Sekcji | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| ); | |
| }; | |
| const renderHolisticTab = () => { | |
| if (isHolisticLoading) { | |
| return <div style={{ padding: '3rem', color: 'var(--text-muted)', textAlign: 'center' }}>Ładowanie raportu spójności...</div>; | |
| } | |
| if (isHolisticRunning) { | |
| return ( | |
| <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', padding: '6rem 2rem', color: 'var(--accent-green)' }}> | |
| <Activity size={48} className="spin" style={{ marginBottom: '1.5rem' }} /> | |
| <h3 style={{ margin: '0 0 1rem 0', color: 'var(--text-primary)' }}>Analiza spójności w toku...</h3> | |
| <p style={{ color: 'var(--text-muted)', textAlign: 'center', maxWidth: '400px' }}>Globalny Krytyk analizuje wniosek pod kątem logicznego przepływu, finansów, DNSH i wytycznych programu.</p> | |
| <div style={{ marginTop: '2rem', width: '200px', height: '4px', background: 'rgba(255,255,255,0.1)', borderRadius: '2px', overflow: 'hidden' }}> | |
| <motion.div | |
| initial={{ x: '-100%' }} | |
| animate={{ x: '100%' }} | |
| transition={{ repeat: Infinity, duration: 2, ease: "linear" }} | |
| style={{ width: '50%', height: '100%', background: 'linear-gradient(90deg, transparent, var(--accent-green), transparent)' }} | |
| /> | |
| </div> | |
| </div> | |
| ); | |
| } | |
| if (!holisticReview) { | |
| return ( | |
| <div className="glass-card" style={{ padding: '6rem 2rem', textAlign: 'center', color: 'var(--text-muted)' }}> | |
| <Activity size={64} color="rgba(255,255,255,0.2)" style={{ marginBottom: '1.5rem' }} /> | |
| <h3 style={{ color: 'var(--text-primary)', margin: '0 0 1rem 0', fontSize: '1.5rem' }}>Brak Raportu Spójności</h3> | |
| <p style={{ margin: '0 auto 2rem auto', maxWidth: '500px', lineHeight: 1.6 }}> | |
| Ten projekt nie został jeszcze poddany analizie całościowej (Holistic Review). | |
| Uruchom raport albo od razu pełny cykl: krytyk całości + automatyczna korekta wszystkich słabych sekcji | |
| (bez kasowania dobrych fragmentów) — szybsza droga do odblokowania PDF/DOCX. | |
| </p> | |
| <div style={{ display: 'flex', flexWrap: 'wrap', gap: '0.75rem', justifyContent: 'center' }}> | |
| <button | |
| className="btn btn-primary hover-lift" | |
| onClick={handleQualityCycle} | |
| disabled={isHolisticRunning} | |
| style={{ | |
| background: 'linear-gradient(135deg, #7c3aed, #2563eb)', | |
| color: '#fff', | |
| border: 'none', | |
| display: 'inline-flex', | |
| alignItems: 'center', | |
| gap: '0.8rem', | |
| padding: '1rem 1.75rem', | |
| fontWeight: 600, | |
| borderRadius: '8px', | |
| fontSize: '1.05rem', | |
| }} | |
| > | |
| <Sparkles size={20} /> Krytyk całości + auto-korekta | |
| </button> | |
| <button | |
| className="btn btn-primary hover-lift" | |
| onClick={handleRunHolisticReview} | |
| disabled={isHolisticRunning} | |
| style={{ | |
| background: 'var(--accent-green)', | |
| color: '#fff', | |
| border: 'none', | |
| display: 'inline-flex', | |
| alignItems: 'center', | |
| gap: '0.8rem', | |
| padding: '1rem 1.75rem', | |
| fontWeight: 600, | |
| borderRadius: '8px', | |
| fontSize: '1.05rem', | |
| }} | |
| > | |
| <Activity size={20} /> Tylko Raport Spójności | |
| </button> | |
| </div> | |
| </div> | |
| ); | |
| } | |
| return ( | |
| <div style={{ display: 'grid', gridTemplateColumns: '1fr 350px', gap: '1.5rem', alignItems: 'start' }}> | |
| <div style={{ display: 'flex', flexDirection: 'column', gap: '1.5rem' }}> | |
| <div className="glass-card" style={{ padding: '1.5rem' }}> | |
| <h3 style={{ margin: '0 0 1rem 0', display: 'flex', alignItems: 'center', gap: '0.8rem' }}> | |
| <FileText size={20} color="var(--accent-blue)" /> Ogólna Ocena Projektu | |
| </h3> | |
| <p style={{ margin: 0, lineHeight: 1.6, color: 'var(--text-secondary)' }}>{holisticReview.overall_assessment}</p> | |
| </div> | |
| <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '1rem' }}> | |
| <div className="glass-card" style={{ padding: '1.5rem', borderLeft: '3px solid var(--accent-green)' }}> | |
| <h4 style={{ margin: '0 0 0.8rem 0', display: 'flex', alignItems: 'center', gap: '0.5rem' }}> | |
| <ShieldCheck size={18} color="var(--accent-green)" /> Zgodność DNSH | |
| </h4> | |
| <p style={{ margin: 0, fontSize: '0.9rem', color: 'var(--text-secondary)', lineHeight: 1.5 }}>{holisticReview.dnsh_compliance}</p> | |
| </div> | |
| <div className="glass-card" style={{ padding: '1.5rem', borderLeft: '3px solid var(--accent-yellow)' }}> | |
| <h4 style={{ margin: '0 0 0.8rem 0', display: 'flex', alignItems: 'center', gap: '0.5rem' }}> | |
| <DollarSign size={18} color="var(--accent-yellow)" /> Spójność Budżetowa | |
| </h4> | |
| <p style={{ margin: 0, fontSize: '0.9rem', color: 'var(--text-secondary)', lineHeight: 1.5 }}>{holisticReview.budget_consistency}</p> | |
| </div> | |
| <div className="glass-card" style={{ padding: '1.5rem', borderLeft: '3px solid var(--accent-blue)' }}> | |
| <h4 style={{ margin: '0 0 0.8rem 0', display: 'flex', alignItems: 'center', gap: '0.5rem' }}> | |
| <Activity size={18} color="var(--accent-blue)" /> Przepływ Logiczny | |
| </h4> | |
| <p style={{ margin: 0, fontSize: '0.9rem', color: 'var(--text-secondary)', lineHeight: 1.5 }}>{holisticReview.logical_flow}</p> | |
| </div> | |
| <div className="glass-card" style={{ padding: '1.5rem', borderLeft: '3px solid #a855f7' }}> | |
| <h4 style={{ margin: '0 0 0.8rem 0', display: 'flex', alignItems: 'center', gap: '0.5rem' }}> | |
| <CheckCircle size={18} color="#a855f7" /> Zgodność z Programem | |
| </h4> | |
| <p style={{ margin: 0, fontSize: '0.9rem', color: 'var(--text-secondary)', lineHeight: 1.5 }}>{holisticReview.program_alignment}</p> | |
| </div> | |
| </div> | |
| {holisticReview.critical_flaws && holisticReview.critical_flaws.length > 0 && ( | |
| <div className="glass-card" style={{ padding: '1.5rem', border: '1px solid rgba(239, 68, 68, 0.3)', background: 'rgba(239, 68, 68, 0.05)' }}> | |
| <h3 style={{ margin: '0 0 1rem 0', display: 'flex', alignItems: 'center', gap: '0.8rem', color: 'var(--accent-red)' }}> | |
| <AlertCircle size={20} /> Wykryte Wady Krytyczne | |
| </h3> | |
| <ul style={{ margin: 0, paddingLeft: '1.2rem', color: 'var(--text-primary)', display: 'flex', flexDirection: 'column', gap: '0.8rem', lineHeight: 1.5 }}> | |
| {holisticReview.critical_flaws.map((flaw: string, i: number) => ( | |
| <li key={i}>{flaw}</li> | |
| ))} | |
| </ul> | |
| </div> | |
| )} | |
| </div> | |
| <div style={{ display: 'flex', flexDirection: 'column', gap: '1.5rem' }}> | |
| <div className="glass-card" style={{ padding: '2rem', display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '1rem', textAlign: 'center' }}> | |
| <div style={{ fontSize: '0.9rem', color: 'var(--text-muted)', textTransform: 'uppercase', letterSpacing: '1px' }}>Ogólna Ocena Spójności</div> | |
| <div style={{ | |
| width: '120px', height: '120px', borderRadius: '50%', | |
| background: `conic-gradient(${holisticReview.score >= 80 ? 'var(--accent-green)' : holisticReview.score >= 50 ? 'var(--accent-yellow)' : 'var(--accent-red)'} ${holisticReview.score}%, rgba(255,255,255,0.05) 0)`, | |
| display: 'flex', alignItems: 'center', justifyContent: 'center', | |
| position: 'relative' | |
| }}> | |
| <div style={{ width: '100px', height: '100px', borderRadius: '50%', background: 'var(--bg-glass)', display: 'flex', alignItems: 'center', justifyContent: 'center', flexDirection: 'column' }}> | |
| <span style={{ fontSize: '2.5rem', fontWeight: 800, color: '#fff', lineHeight: 1 }}>{holisticReview.score}</span> | |
| <span style={{ fontSize: '0.8rem', color: 'var(--text-muted)' }}>/ 100</span> | |
| </div> | |
| </div> | |
| <button | |
| className="btn btn-primary" | |
| onClick={handleQualityCycle} | |
| disabled={isHolisticRunning} | |
| style={{ | |
| marginTop: '1rem', | |
| width: '100%', | |
| background: 'linear-gradient(135deg, #7c3aed, #2563eb)', | |
| color: '#fff', | |
| border: 'none', | |
| display: 'inline-flex', | |
| alignItems: 'center', | |
| justifyContent: 'center', | |
| gap: '0.8rem', | |
| padding: '0.8rem 1.5rem', | |
| fontWeight: 600, | |
| borderRadius: '8px', | |
| }} | |
| > | |
| <Sparkles size={16} /> Krytyk całości + auto-korekta | |
| </button> | |
| <button | |
| className="btn btn-primary" | |
| onClick={handleRunHolisticReview} | |
| disabled={isHolisticRunning} | |
| style={{ | |
| width: '100%', | |
| background: 'rgba(255,255,255,0.1)', | |
| color: '#fff', | |
| border: '1px solid rgba(255,255,255,0.2)', | |
| display: 'inline-flex', | |
| alignItems: 'center', | |
| justifyContent: 'center', | |
| gap: '0.8rem', | |
| padding: '0.8rem 1.5rem', | |
| fontWeight: 600, | |
| borderRadius: '8px', | |
| }} | |
| > | |
| <RefreshCw size={16} /> Odśwież Raport Spójności | |
| </button> | |
| </div> | |
| {holisticReview.recommendations && holisticReview.recommendations.length > 0 && ( | |
| <div className="glass-card" style={{ padding: '1.5rem' }}> | |
| <h4 style={{ margin: '0 0 1rem 0', display: 'flex', alignItems: 'center', gap: '0.5rem', color: 'var(--accent-blue)' }}> | |
| <Sparkles size={18} /> Rekomendacje | |
| </h4> | |
| <ul style={{ margin: 0, paddingLeft: '1.2rem', color: 'var(--text-secondary)', fontSize: '0.9rem', display: 'flex', flexDirection: 'column', gap: '0.8rem', lineHeight: 1.5 }}> | |
| {holisticReview.recommendations.map((rec: string, i: number) => ( | |
| <li key={i}>{rec}</li> | |
| ))} | |
| </ul> | |
| </div> | |
| )} | |
| </div> | |
| </div> | |
| ); | |
| }; | |
| return ( | |
| <div style={{ padding: '2rem', display: 'flex', flexDirection: 'column', gap: '1.5rem' }}> | |
| <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', borderBottom: '1px solid rgba(255,255,255,0.1)', paddingBottom: '1.5rem' }}> | |
| <div> | |
| <h2 style={{ margin: '0 0 0.5rem 0', display: 'flex', alignItems: 'center', gap: '0.8rem' }}> | |
| Centrum Audytu <span style={{ fontSize: '0.8rem', padding: '0.2rem 0.6rem', background: 'rgba(59, 130, 246, 0.2)', color: 'var(--accent-blue)', borderRadius: '12px' }}>PRO</span> | |
| </h2> | |
| <p style={{ margin: 0, color: 'var(--text-muted)', maxWidth: '700px' }}> | |
| Analizuj poszczególne sekcje pod kątem błędów językowych i merytorycznych, lub uruchom raport spójności całego projektu (Holistic Review), aby upewnić się, że wniosek stanowi logiczną całość. | |
| </p> | |
| </div> | |
| </div> | |
| {/* Tab Navigation */} | |
| <div style={{ display: 'flex', gap: '1rem', borderBottom: '1px solid rgba(255,255,255,0.05)', paddingBottom: '1rem' }}> | |
| <button | |
| onClick={() => setActiveTab('audit')} | |
| style={{ | |
| padding: '0.8rem 1.5rem', | |
| background: activeTab === 'audit' ? 'rgba(59, 130, 246, 0.1)' : 'transparent', | |
| color: activeTab === 'audit' ? 'var(--accent-blue)' : 'var(--text-muted)', | |
| border: 'none', | |
| borderBottom: activeTab === 'audit' ? '2px solid var(--accent-blue)' : '2px solid transparent', | |
| borderRadius: '8px 8px 0 0', | |
| fontWeight: 600, | |
| cursor: 'pointer', | |
| display: 'flex', | |
| alignItems: 'center', | |
| gap: '0.5rem', | |
| transition: 'all 0.2s' | |
| }} | |
| > | |
| <ShieldAlert size={18} /> Szczegółowy Audyt Sekcji | |
| </button> | |
| <button | |
| onClick={() => setActiveTab('holistic')} | |
| style={{ | |
| padding: '0.8rem 1.5rem', | |
| background: activeTab === 'holistic' ? 'rgba(16, 185, 129, 0.1)' : 'transparent', | |
| color: activeTab === 'holistic' ? 'var(--accent-green)' : 'var(--text-muted)', | |
| border: 'none', | |
| borderBottom: activeTab === 'holistic' ? '2px solid var(--accent-green)' : '2px solid transparent', | |
| borderRadius: '8px 8px 0 0', | |
| fontWeight: 600, | |
| cursor: 'pointer', | |
| display: 'flex', | |
| alignItems: 'center', | |
| gap: '0.5rem', | |
| transition: 'all 0.2s' | |
| }} | |
| > | |
| <Activity size={18} /> Raport Spójności (Holistic Review) | |
| </button> | |
| </div> | |
| {/* Tab Content */} | |
| <div style={{ marginTop: '0.5rem' }}> | |
| {activeTab === 'audit' ? renderAuditTab() : renderHolisticTab()} | |
| </div> | |
| <style>{`.spin { animation: spin 1s linear infinite; } @keyframes spin { 100% { transform: rotate(360deg); } } .hover-bg-subtle:hover { background: rgba(255,255,255,0.02) ; }`}</style> | |
| </div> | |
| ); | |
| }; | |
| export default ProjectAuditPanel; | |