wellnua-lite/components/ErrorBoundary.tsx
Sergei b2639dd540 Add Sherpa TTS voice synthesis system
Core TTS infrastructure:
- sherpaTTS.ts: Sherpa ONNX integration for offline TTS
- TTSErrorBoundary.tsx: Error boundary for TTS failures
- ErrorBoundary.tsx: Generic error boundary component
- VoiceIndicator.tsx: Visual indicator for voice activity
- useSpeechRecognition.ts: Speech-to-text hook
- DebugLogger.ts: Debug logging utility

Features:
- Offline voice synthesis (no internet needed)
- Multiple voices support
- Real-time voice activity indication
- Error recovery and fallback
- Debug logging for troubleshooting

Tech stack:
- Sherpa ONNX runtime
- React Native Audio
- Expo modules
2026-01-14 19:09:27 -08:00

183 lines
4.4 KiB
TypeScript

import React, { Component, ReactNode } from 'react';
import { View, Text, StyleSheet, TouchableOpacity } from 'react-native';
import { debugLogger } from '@/services/DebugLogger';
import { AppColors, Spacing, FontSizes, BorderRadius } from '@/constants/theme';
interface Props {
children: ReactNode;
fallback?: ReactNode;
onError?: (error: Error, errorInfo: React.ErrorInfo) => void;
category?: string;
}
interface State {
hasError: boolean;
error: Error | null;
errorInfo: React.ErrorInfo | null;
}
/**
* ErrorBoundary - catches JavaScript errors in child components
* Logs errors to DebugLogger and shows fallback UI
*/
export class ErrorBoundary extends Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = {
hasError: false,
error: null,
errorInfo: null,
};
}
static getDerivedStateFromError(error: Error): State {
return {
hasError: true,
error,
errorInfo: null,
};
}
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
const category = this.props.category || 'ErrorBoundary';
// Log to debug console
debugLogger.error(
category,
`💥 Component crashed: ${error.message}`,
{
error: error.toString(),
stack: error.stack,
componentStack: errorInfo.componentStack,
}
);
// Update state with error info
this.setState({
errorInfo,
});
// Call optional error handler
this.props.onError?.(error, errorInfo);
}
handleReset = () => {
this.setState({
hasError: false,
error: null,
errorInfo: null,
});
};
render() {
if (this.state.hasError) {
// Custom fallback UI
if (this.props.fallback) {
return this.props.fallback;
}
// Default fallback UI
return (
<View style={styles.container}>
<View style={styles.errorCard}>
<Text style={styles.errorTitle}>Something went wrong</Text>
<Text style={styles.errorMessage}>
{this.state.error?.message || 'Unknown error'}
</Text>
{this.state.error?.stack && (
<View style={styles.stackContainer}>
<Text style={styles.stackLabel}>Stack trace:</Text>
<Text style={styles.stackTrace} numberOfLines={5}>
{this.state.error.stack}
</Text>
</View>
)}
<TouchableOpacity
style={styles.resetButton}
onPress={this.handleReset}
>
<Text style={styles.resetButtonText}>Try Again</Text>
</TouchableOpacity>
<Text style={styles.debugHint}>
Check Debug tab for full error details
</Text>
</View>
</View>
);
}
return this.props.children;
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: AppColors.background,
padding: Spacing.lg,
},
errorCard: {
backgroundColor: AppColors.surface,
borderRadius: BorderRadius.lg,
padding: Spacing.lg,
maxWidth: 400,
width: '100%',
borderLeftWidth: 4,
borderLeftColor: AppColors.error || '#E53935',
},
errorTitle: {
fontSize: FontSizes.xl,
fontWeight: '700',
color: AppColors.error || '#E53935',
marginBottom: Spacing.sm,
},
errorMessage: {
fontSize: FontSizes.base,
color: AppColors.textPrimary,
marginBottom: Spacing.md,
lineHeight: 22,
},
stackContainer: {
backgroundColor: AppColors.background,
borderRadius: BorderRadius.md,
padding: Spacing.sm,
marginBottom: Spacing.md,
},
stackLabel: {
fontSize: FontSizes.xs,
color: AppColors.textSecondary,
fontWeight: '600',
marginBottom: Spacing.xs,
},
stackTrace: {
fontSize: FontSizes.xs,
color: AppColors.textMuted,
fontFamily: 'monospace',
lineHeight: 16,
},
resetButton: {
backgroundColor: AppColors.primary,
borderRadius: BorderRadius.md,
paddingVertical: Spacing.sm,
paddingHorizontal: Spacing.md,
alignItems: 'center',
marginBottom: Spacing.sm,
},
resetButtonText: {
color: AppColors.white,
fontSize: FontSizes.base,
fontWeight: '600',
},
debugHint: {
fontSize: FontSizes.xs,
color: AppColors.textMuted,
textAlign: 'center',
marginTop: Spacing.xs,
},
});