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 = ({ projectId }) => { const [activeTab, setActiveTab] = useState<'audit' | 'holistic'>('audit'); const [audit, setAudit] = useState(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(null); const pollIntervalRef = useRef(null); const pollAttemptsRef = useRef(0); const MAX_POLL_ATTEMPTS = 72; // ~6 min przy interwale 5s const [holisticReview, setHolisticReview] = useState(null); const [isHolisticLoading, setIsHolisticLoading] = useState(false); const [isHolisticRunning, setIsHolisticRunning] = useState(false); const pollHolisticIntervalRef = useRef(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 ; if (sev === 'high') return ; return ; }; 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
Ładowanie raportu audytu...
; } if (fixProgress) { return (

Trwa automatyczna poprawa wniosku…

Sekcja {fixProgress.current} z {fixProgress.total}

Może to potrwać 30–60 sekund w zależności od długości dokumentu. Prosimy nie odświeżać okna.

); } if (isRunning) { return (

Trwa szczegółowy audyt sekcji

Sprawdzanie reguł dla poszczególnych akapitów i generowanie rekomendacji...

); } if (!audit) { return (

Brak Danych Audytowych

Ten projekt nie został jeszcze sprawdzony przez Szczegółowego Audytora. Uruchom proces, aby wykryć drobne niespójności i błędy językowe.

); } return (

Wykryte Zagrożenia i Uwagi ({audit.issues?.length || 0})

{!audit.issues || audit.issues.length === 0 ? (

Wniosek jest idealny!

Audytor nie wykrył żadnych nieprawidłowości, kosztów niekwalifikowalnych czy niespójności.

) : (
{audit.issues?.map((issue: any, idx: number) => (
setExpandedIssue(expandedIssue === idx ? null : idx)} >
{getSeverityIcon(issue.severity)}
{issue.category} {getSeverityLabel(issue.severity)} {issue.affected_section && ( Sekcja: {issue.affected_section} )}
{issue.message}
{expandedIssue === idx && (
{issue.problem_quote && (
Zidentyfikowany fragment:
"{issue.problem_quote}"
)} {issue.recommendation && (
Rekomendacja Audytora:
{issue.recommendation}
)} {issue.rule_citation && (
Reguła / Kwalifikowalność: {issue.rule_citation}
)} {issue.affected_section && ( )}
)}
))}
)}
Wynik Audytu Sekcji
= 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' }}>
{audit.overall_score} / 100
Status Audytu:
{audit.overall_score >= 80 ? ( <>Niewielkie poprawki. ) : audit.overall_score >= 60 ? ( <>Wymaga poprawek redakcyjnych. ) : ( <>Wiele sekcji do poprawy. )}
{audit.issues && audit.issues.length > 0 && (
)}
); }; const renderHolisticTab = () => { if (isHolisticLoading) { return
Ładowanie raportu spójności...
; } if (isHolisticRunning) { return (

Analiza spójności w toku...

Globalny Krytyk analizuje wniosek pod kątem logicznego przepływu, finansów, DNSH i wytycznych programu.

); } if (!holisticReview) { return (

Brak Raportu Spójności

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.

); } return (

Ogólna Ocena Projektu

{holisticReview.overall_assessment}

Zgodność DNSH

{holisticReview.dnsh_compliance}

Spójność Budżetowa

{holisticReview.budget_consistency}

Przepływ Logiczny

{holisticReview.logical_flow}

Zgodność z Programem

{holisticReview.program_alignment}

{holisticReview.critical_flaws && holisticReview.critical_flaws.length > 0 && (

Wykryte Wady Krytyczne

    {holisticReview.critical_flaws.map((flaw: string, i: number) => (
  • {flaw}
  • ))}
)}
Ogólna Ocena Spójności
= 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' }}>
{holisticReview.score} / 100
{holisticReview.recommendations && holisticReview.recommendations.length > 0 && (

Rekomendacje

    {holisticReview.recommendations.map((rec: string, i: number) => (
  • {rec}
  • ))}
)}
); }; return (

Centrum Audytu PRO

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ść.

{/* Tab Navigation */}
{/* Tab Content */}
{activeTab === 'audit' ? renderAuditTab() : renderHolisticTab()}
); }; export default ProjectAuditPanel;