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
183 lines
4.4 KiB
TypeScript
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,
|
|
},
|
|
});
|