gebeya-dala-error-fixer
Version:
Automatic runtime error detection and fix suggestions for Next.js applications with config-based setup
457 lines (451 loc) • 15.7 kB
JavaScript
import { loadConfig, validateConfig } from './config';
import { ErrorDetector } from './ErrorDetector';
export class AutoInitializer {
constructor() {
this.errorDetector = null;
this.modalContainer = null;
this.floatingButton = null;
this.errorCount = 0;
this.currentError = null;
this.currentSuggestion = null;
this.isModalOpen = false;
this.config = validateConfig(loadConfig());
this.init();
}
static getInstance() {
if (!AutoInitializer.instance) {
AutoInitializer.instance = new AutoInitializer();
}
return AutoInitializer.instance;
}
init() {
// Only initialize in browser environment
if (typeof window === 'undefined')
return;
// Check if should be enabled based on environment
if (!this.shouldEnable())
return;
// Wait for DOM to be ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => this.setupErrorDetection());
}
else {
this.setupErrorDetection();
}
}
shouldEnable() {
if (!this.config.enabled)
return false;
const nodeEnv = process.env.NODE_ENV;
switch (this.config.environment) {
case 'development':
return nodeEnv === 'development';
case 'production':
return nodeEnv === 'production';
case 'all':
return true;
default:
return nodeEnv === 'development';
}
}
setupErrorDetection() {
// Initialize error detector
this.errorDetector = new ErrorDetector((error, suggestion) => {
this.handleError(error, suggestion);
});
// Create UI elements
this.createFloatingButton();
this.createModalContainer();
// Setup error boundary for React errors
this.setupGlobalErrorBoundary();
}
handleError(error, suggestion) {
console.log('Error detected by auto-initializer:', error);
this.currentError = error;
this.currentSuggestion = suggestion;
this.errorCount++;
// Update floating button
this.updateFloatingButton();
// Auto-show modal if enabled
if (this.config.autoShow) {
this.showModal();
}
}
createFloatingButton() {
this.floatingButton = document.createElement('button');
this.floatingButton.innerHTML = '✓';
this.floatingButton.title = 'No errors detected';
// Apply styles
const styles = {
position: 'fixed',
zIndex: '9998',
width: '50px',
height: '50px',
borderRadius: '50%',
backgroundColor: '#333',
border: '2px solid #555',
color: '#fff',
fontSize: '18px',
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.3)',
transition: 'all 0.2s ease',
fontFamily: 'system-ui, -apple-system, sans-serif'
};
// Set position based on config
const position = this.getButtonPosition();
Object.assign(this.floatingButton.style, styles, position);
// Add click handler
this.floatingButton.addEventListener('click', () => {
this.showModal();
});
// Add to document
document.body.appendChild(this.floatingButton);
}
getButtonPosition() {
switch (this.config.position) {
case 'top-right':
return { top: '20px', right: '20px' };
case 'top-left':
return { top: '20px', left: '20px' };
case 'bottom-right':
return { bottom: '20px', right: '20px' };
case 'bottom-left':
return { bottom: '20px', left: '20px' };
default:
return { top: '20px', right: '20px' };
}
}
updateFloatingButton() {
if (!this.floatingButton)
return;
if (this.errorCount > 0) {
this.floatingButton.innerHTML = '!';
this.floatingButton.style.backgroundColor = '#ef4444';
this.floatingButton.title = `${this.errorCount} error${this.errorCount > 1 ? 's' : ''} detected`;
// Add error count badge
const existingBadge = this.floatingButton.querySelector('.error-count');
if (existingBadge) {
existingBadge.remove();
}
const badge = document.createElement('div');
badge.className = 'error-count';
badge.textContent = this.errorCount > 99 ? '99+' : this.errorCount.toString();
badge.style.cssText = `
position: absolute;
top: -8px;
right: -8px;
background-color: #dc2626;
color: #fff;
border-radius: 50%;
width: 20px;
height: 20px;
font-size: 12px;
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
`;
this.floatingButton.appendChild(badge);
}
}
createModalContainer() {
this.modalContainer = document.createElement('div');
this.modalContainer.id = 'error-detector-modal';
this.modalContainer.style.display = 'none';
document.body.appendChild(this.modalContainer);
}
showModal() {
if (!this.modalContainer || !this.currentError || !this.currentSuggestion)
return;
this.isModalOpen = true;
this.modalContainer.style.display = 'block';
// Render modal content
this.renderModal();
}
hideModal() {
if (!this.modalContainer)
return;
this.isModalOpen = false;
this.modalContainer.style.display = 'none';
this.modalContainer.innerHTML = '';
}
renderModal() {
if (!this.modalContainer || !this.currentError || !this.currentSuggestion)
return;
const isDark = this.config.theme === 'dark';
const bgColor = isDark ? '#1a1a1a' : '#ffffff';
const textColor = isDark ? '#ffffff' : '#000000';
const borderColor = isDark ? '#333' : '#e5e5e5';
this.modalContainer.innerHTML = `
<div style="
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.8);
display: flex;
align-items: center;
justify-content: center;
z-index: 9999;
font-family: system-ui, -apple-system, sans-serif;
">
<div style="
background-color: ${bgColor};
color: ${textColor};
border-radius: 12px;
padding: 24px;
max-width: 600px;
width: 90%;
max-height: 80vh;
overflow: auto;
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.3);
border: 1px solid ${borderColor};
">
${this.renderModalContent()}
</div>
</div>
`;
// Add event listeners
this.addModalEventListeners();
}
renderModalContent() {
var _a;
if (!this.currentError || !this.currentSuggestion)
return '';
const isDark = this.config.theme === 'dark';
const secondaryBg = isDark ? '#2a2a2a' : '#f5f5f5';
const secondaryColor = isDark ? '#ccc' : '#666';
return `
<!-- Header -->
<div style="
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
padding-bottom: 16px;
border-bottom: 1px solid ${isDark ? '#333' : '#e5e5e5'};
">
<div style="display: flex; align-items: center; gap: 12px;">
<div style="
width: 24px;
height: 24px;
background-color: #ef4444;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
font-weight: bold;
color: white;
">!</div>
<h2 style="margin: 0; font-size: 18px; font-weight: 600;">
${this.currentSuggestion.title}
</h2>
</div>
<button id="close-modal" style="
background: none;
border: none;
color: #999;
font-size: 24px;
cursor: pointer;
padding: 0;
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
">×</button>
</div>
<!-- Error Details -->
<div style="margin-bottom: 20px;">
<div style="font-size: 12px; color: #999; margin-bottom: 8px;">
Error occurred at ${new Date(this.currentError.timestamp).toLocaleTimeString()}
</div>
<div style="
background-color: ${secondaryBg};
padding: 12px;
border-radius: 6px;
border: 1px solid ${isDark ? '#333' : '#e5e5e5'};
font-size: 14px;
font-family: monospace;
word-break: break-word;
max-height: 100px;
overflow: auto;
">
${this.currentError.message}
</div>
</div>
<!-- Description -->
<div style="margin-bottom: 20px;">
<h3 style="margin: 0 0 8px 0; font-size: 14px; font-weight: 600;">
What's happening?
</h3>
<p style="margin: 0; font-size: 14px; color: ${secondaryColor}; line-height: 1.4;">
${this.currentSuggestion.description}
</p>
</div>
${this.currentSuggestion.code ? `
<!-- Code Example -->
<div style="margin-bottom: 20px;">
<div style="
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
">
<h3 style="margin: 0; font-size: 14px; font-weight: 600;">
How to fix it:
</h3>
<button id="copy-code" style="
background: ${isDark ? '#333' : '#f5f5f5'};
border: 1px solid ${isDark ? '#555' : '#ccc'};
color: inherit;
padding: 4px 8px;
border-radius: 4px;
font-size: 12px;
cursor: pointer;
">Copy</button>
</div>
<pre style="
background-color: ${secondaryBg};
padding: 12px;
border-radius: 6px;
border: 1px solid ${isDark ? '#333' : '#e5e5e5'};
font-size: 13px;
font-family: monospace;
margin: 0;
overflow: auto;
white-space: pre-wrap;
">${this.currentSuggestion.code}</pre>
</div>
` : ''}
<!-- Actions -->
<div style="margin-bottom: 20px;">
<h3 style="margin: 0 0 12px 0; font-size: 14px; font-weight: 600;">
Suggested actions:
</h3>
<ul style="margin: 0; padding-left: 20px; font-size: 14px; color: ${secondaryColor};">
${this.currentSuggestion.actions.map(action => `<li style="margin-bottom: 4px;">${action}</li>`).join('')}
</ul>
</div>
<!-- Fix It Button -->
${((_a = this.config.fixItButton) === null || _a === void 0 ? void 0 : _a.enabled) ? `
<div style="
display: flex;
gap: 12px;
justify-content: flex-end;
margin-top: 24px;
padding-top: 16px;
border-top: 1px solid ${isDark ? '#333' : '#e5e5e5'};
">
<button id="fix-it-button" style="
background: linear-gradient(135deg, #10b981, #059669);
color: white;
border: none;
padding: 12px 24px;
border-radius: 8px;
font-size: 14px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s ease;
box-shadow: 0 2px 8px rgba(16, 185, 129, 0.3);
">
🔧 Fix it
</button>
<button id="dismiss-button" style="
background: ${isDark ? '#333' : '#f5f5f5'};
color: inherit;
border: 1px solid ${isDark ? '#555' : '#ccc'};
padding: 12px 24px;
border-radius: 8px;
font-size: 14px;
cursor: pointer;
transition: all 0.2s ease;
">
Dismiss
</button>
</div>
` : ''}
`;
}
addModalEventListeners() {
var _a, _b;
// Close button
const closeBtn = document.getElementById('close-modal');
if (closeBtn) {
closeBtn.addEventListener('click', () => this.hideModal());
}
// Copy code button
const copyBtn = document.getElementById('copy-code');
if (copyBtn && ((_a = this.currentSuggestion) === null || _a === void 0 ? void 0 : _a.code)) {
copyBtn.addEventListener('click', () => {
navigator.clipboard.writeText(this.currentSuggestion.code);
copyBtn.textContent = 'Copied!';
setTimeout(() => {
copyBtn.textContent = 'Copy';
}, 2000);
});
}
// Fix it button
const fixBtn = document.getElementById('fix-it-button');
if (fixBtn) {
fixBtn.addEventListener('click', () => this.handleFixIt());
}
// Dismiss button
const dismissBtn = document.getElementById('dismiss-button');
if (dismissBtn) {
dismissBtn.addEventListener('click', () => this.hideModal());
}
// Click outside to close
(_b = this.modalContainer) === null || _b === void 0 ? void 0 : _b.addEventListener('click', (e) => {
if (e.target === this.modalContainer) {
this.hideModal();
}
});
}
handleFixIt() {
var _a, _b;
if (((_a = this.config.fixItButton) === null || _a === void 0 ? void 0 : _a.customAction) && this.currentError && this.currentSuggestion) {
this.config.fixItButton.customAction(this.currentError, this.currentSuggestion);
}
else {
// Default fix it action - copy code to clipboard and show instructions
if ((_b = this.currentSuggestion) === null || _b === void 0 ? void 0 : _b.code) {
navigator.clipboard.writeText(this.currentSuggestion.code);
alert('Code copied to clipboard! Apply the fix to your code.');
}
else {
alert('Please follow the suggested actions to fix this error.');
}
}
this.hideModal();
}
setupGlobalErrorBoundary() {
// This will be handled by the ErrorDetector class
// which already sets up global error handlers
}
destroy() {
if (this.errorDetector) {
this.errorDetector.destroy();
}
if (this.floatingButton) {
this.floatingButton.remove();
}
if (this.modalContainer) {
this.modalContainer.remove();
}
AutoInitializer.instance = null;
}
}
AutoInitializer.instance = null;
// Auto-initialize when script is loaded
if (typeof window !== 'undefined') {
// Use setTimeout to ensure DOM is ready
setTimeout(() => {
AutoInitializer.getInstance();
}, 0);
}