Spaces:
Running
Running
| import { Component, ErrorInfo, ReactNode } from 'react'; | |
| interface Props { | |
| children: ReactNode; | |
| /** Opcjonalny custom fallback UI. Jeśli nie podany — wyświetla domyślny ekran błędu. */ | |
| fallback?: ReactNode; | |
| /** Nazwa kontekstu — pomaga w logowaniu (np. "GeneratorPanel", "AuditPanel") */ | |
| context?: string; | |
| } | |
| interface State { | |
| hasError: boolean; | |
| error: Error | null; | |
| errorInfo: ErrorInfo | null; | |
| } | |
| /** | |
| * React Error Boundary — globalny łapacz błędów komponentów. | |
| * | |
| * Użycie: | |
| * <ErrorBoundary context="GeneratorPanel"> | |
| * <AIGeneratorPanel /> | |
| * </ErrorBoundary> | |
| * | |
| * Krytyczny wymóg Beta 1.0 — bez tego crash komponentu = biały ekran. | |
| * Zgodność: React 18+, wymaga klasy (hooks nie obsługują componentDidCatch). | |
| */ | |
| export class ErrorBoundary extends Component<Props, State> { | |
| constructor(props: Props) { | |
| super(props); | |
| this.state = { hasError: false, error: null, errorInfo: null }; | |
| } | |
| static getDerivedStateFromError(error: Error): Partial<State> { | |
| return { hasError: true, error }; | |
| } | |
| componentDidCatch(error: Error, errorInfo: ErrorInfo) { | |
| const context = this.props.context || 'unknown'; | |
| console.error(`[ErrorBoundary][${context}] Caught error:`, error, errorInfo); | |
| this.setState({ errorInfo }); | |
| // Opcjonalne wysłanie do serwisu monitoringu (Sentry / LangSmith) | |
| try { | |
| if (typeof window !== 'undefined' && (window as any).Sentry) { | |
| (window as any).Sentry.captureException(error, { | |
| extra: { context, componentStack: errorInfo.componentStack }, | |
| }); | |
| } | |
| } catch { | |
| // Sentry niedostępny — ignoruj | |
| } | |
| } | |
| handleReset = () => { | |
| this.setState({ hasError: false, error: null, errorInfo: null }); | |
| }; | |
| render() { | |
| if (this.state.hasError) { | |
| if (this.props.fallback) { | |
| return this.props.fallback; | |
| } | |
| return ( | |
| <div style={{ | |
| display: 'flex', | |
| flexDirection: 'column', | |
| alignItems: 'center', | |
| justifyContent: 'center', | |
| minHeight: '200px', | |
| padding: '2rem', | |
| background: 'rgba(239,68,68,0.05)', | |
| border: '1px solid rgba(239,68,68,0.2)', | |
| borderRadius: '12px', | |
| margin: '1rem', | |
| textAlign: 'center', | |
| }}> | |
| <div style={{ fontSize: '2rem', marginBottom: '0.5rem' }}>⚠️</div> | |
| <h3 style={{ color: '#f87171', fontWeight: 700, marginBottom: '0.5rem' }}> | |
| Wystąpił nieoczekiwany błąd | |
| </h3> | |
| <p style={{ color: 'var(--text-muted)', fontSize: '0.875rem', maxWidth: '400px', marginBottom: '1.5rem' }}> | |
| {this.state.error?.message || 'Nieznany błąd komponentu.'} | |
| {this.props.context && ( | |
| <><br /><span style={{ opacity: 0.6 }}>Kontekst: {this.props.context}</span></> | |
| )} | |
| </p> | |
| <button | |
| onClick={this.handleReset} | |
| style={{ | |
| background: 'rgba(239,68,68,0.15)', | |
| border: '1px solid rgba(239,68,68,0.3)', | |
| color: '#f87171', | |
| borderRadius: '8px', | |
| padding: '0.6rem 1.5rem', | |
| cursor: 'pointer', | |
| fontWeight: 600, | |
| fontSize: '0.875rem', | |
| }} | |
| > | |
| 🔄 Spróbuj ponownie | |
| </button> | |
| {process.env.NODE_ENV === 'development' && this.state.errorInfo && ( | |
| <details style={{ marginTop: '1rem', textAlign: 'left', maxWidth: '600px' }}> | |
| <summary style={{ color: 'var(--text-muted)', cursor: 'pointer', fontSize: '0.75rem' }}> | |
| Stack trace (dev only) | |
| </summary> | |
| <pre style={{ | |
| fontSize: '0.7rem', color: '#f87171', opacity: 0.7, | |
| overflow: 'auto', maxHeight: '200px', marginTop: '0.5rem', | |
| }}> | |
| {this.state.errorInfo.componentStack} | |
| </pre> | |
| </details> | |
| )} | |
| </div> | |
| ); | |
| } | |
| return this.props.children; | |
| } | |
| } | |
| export default ErrorBoundary; | |