GrantForge Bot
Deploy to Hugging Face
afd56bc
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;