import React, { useState, useEffect } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { Bug, X, Info, Send, AlertTriangle, ScreenShare, ShieldAlert, Camera, Trash2, Crop, MousePointer2, ChevronDown, Check } from 'lucide-react';
import html2canvas from 'html2canvas';
import { supabase } from '../../lib/supabaseClient';
import useAuthStore from '../../store/authStore';
import useToastStore from '../../store/toastStore';
import { API_CONFIG } from '../../config';
// Reusable Hook for Auto-Diagnostics
function useDiagnostics() {
const [diagnostics, setDiagnostics] = useState({
url: '',
browser: '',
screen: '',
consoleErrors: [],
networkErrors: []
});
useEffect(() => {
// Collect static info
const browserInfo = navigator.userAgent;
const screenInfo = `${window.innerWidth}x${window.innerHeight}`;
setDiagnostics(prev => ({
...prev,
url: window.location.href,
browser: browserInfo,
screen: screenInfo
}));
// Intercept console.error
const originalConsoleError = console.error;
console.error = function (...args) {
setDiagnostics(prev => ({
...prev,
consoleErrors: [...prev.consoleErrors, args.join(' ')].slice(-10) // keep last 10
}));
originalConsoleError.apply(console, args);
};
// Global Error Listener
const handleError = (e) => {
setDiagnostics(prev => ({
...prev,
consoleErrors: [...prev.consoleErrors, `Uncaught: ${e.message}`].slice(-10)
}));
};
window.addEventListener('error', handleError);
return () => {
console.error = originalConsoleError;
window.removeEventListener('error', handleError);
};
}, []);
// Refresh URL right before opening modal
const refreshUrl = () => {
setDiagnostics(prev => ({ ...prev, url: window.location.href }));
};
return { diagnostics, refreshUrl };
}
const CustomSelect = ({ label, value, options, onChange, name }) => {
const [isOpen, setIsOpen] = useState(false);
const selectedOption = options.find(opt => opt.value === value) || options[0];
return (
{label}
setIsOpen(!isOpen)}
className="w-full flex items-center justify-between px-4 py-2.5 rounded-xl border border-slate-200 bg-white hover:border-[#13ec80] transition-all text-sm text-slate-700 outline-none focus:ring-2 focus:ring-[#13ec80]/20"
>
{selectedOption.label}
{isOpen && (
<>
setIsOpen(false)}
/>
{options.map((option) => (
{
onChange({ target: { name, value: option.value } });
setIsOpen(false);
}}
className={`w-full flex items-center justify-between px-4 py-2.5 text-sm transition-colors
${value === option.value
? 'bg-[#13ec80]/10 text-slate-900 font-semibold'
: 'text-slate-600 hover:bg-slate-50'
}`}
>
{option.label}
{value === option.value && (
)}
))}
>
)}
);
};
const BugReportWidget = ({ advanced = false, customTrigger = null }) => {
const [isOpen, setIsOpen] = useState(false);
const { user } = useAuthStore();
const { showToast: addToast } = useToastStore();
const { diagnostics, refreshUrl } = useDiagnostics();
const [isSubmitting, setIsSubmitting] = useState(false);
const [lastSubmitTime, setLastSubmitTime] = useState(0);
const [screenshotData, setScreenshotData] = useState(null);
const [, setIsCapturing] = useState(false);
const [isSelectingRegion, setIsSelectingRegion] = useState(false);
const [selectionStart, setSelectionStart] = useState(null);
const [selectionRect, setSelectionRect] = useState(null);
const [formData, setFormData] = useState({
bug_title: '',
description: '',
steps_to_reproduce: '',
expected_result: '',
actual_result: '',
severity: 'Medium',
category: 'Functionality Broken',
contact_permission: false
});
const handleOpen = () => {
if (!isOpen) {
refreshUrl();
setIsOpen(true);
}
};
const handleClose = () => {
setIsOpen(false);
// Reset specific form fields, but keep severity/category
setFormData(prev => ({
...prev,
bug_title: '',
description: '',
steps_to_reproduce: '',
expected_result: '',
actual_result: ''
}));
setScreenshotData(null);
};
const handleCaptureScreenshot = () => {
// Step 1: Hide modal and enter selection mode
setIsOpen(false);
setSelectionRect(null);
setSelectionStart(null);
// Wait for modal animation to clear
setTimeout(() => {
setIsSelectingRegion(true);
addToast("Click and drag over the dashboard to select the bug area.", "info");
}, 500);
};
const handleMouseDown = (e) => {
if (!isSelectingRegion) return;
setSelectionStart({ x: e.clientX, y: e.clientY });
setSelectionRect({ left: e.clientX, top: e.clientY, width: 0, height: 0 });
};
const handleMouseMove = (e) => {
if (!isSelectingRegion || !selectionStart) return;
const left = Math.min(e.clientX, selectionStart.x);
const top = Math.min(e.clientY, selectionStart.y);
const width = Math.abs(e.clientX - selectionStart.x);
const height = Math.abs(e.clientY - selectionStart.y);
setSelectionRect({ left, top, width, height });
};
const handleMouseUp = async () => {
if (!isSelectingRegion || !selectionRect || selectionRect.width < 10) {
setSelectionStart(null);
setSelectionRect(null);
return;
}
setIsSelectingRegion(false);
setIsCapturing(true);
// Snap the region
try {
const { left, top, width, height } = selectionRect;
// html2canvas options for partial capture
const canvas = await html2canvas(document.body, {
useCORS: true,
logging: false,
x: left + window.scrollX,
y: top + window.scrollY,
width: width,
height: height,
scale: 2 // High quality
});
const base64Image = canvas.toDataURL('image/jpeg', 0.8);
setScreenshotData(base64Image);
addToast("Region captured successfully!", "success");
} catch (err) {
console.error("Capture failed:", err);
addToast("Failed to capture region.", "error");
} finally {
setIsCapturing(false);
setIsOpen(true);
setSelectionStart(null);
setSelectionRect(null);
}
};
const handleCancelSelection = () => {
setIsSelectingRegion(false);
setIsOpen(true);
setSelectionStart(null);
setSelectionRect(null);
};
const handleClearScreenshot = () => {
setScreenshotData(null);
};
useEffect(() => {
const handleKeyDown = (e) => {
if (e.key === 'Escape' && isSelectingRegion) {
handleCancelSelection();
}
};
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, [isSelectingRegion]);
const handleChange = (e) => {
const { name, value, type, checked } = e.target;
setFormData(prev => ({
...prev,
[name]: type === 'checkbox' ? checked : value
}));
};
const handleSubmit = async (e) => {
e.preventDefault();
// 10 second throttle
const now = Date.now();
if (now - lastSubmitTime < 10000) {
addToast("Please wait before submitting another report.", "error");
return;
}
const actualTitle = advanced ? formData.bug_title : (formData.description ? formData.description.slice(0, 40) + "..." : "");
if (!actualTitle || !formData.description) {
addToast(advanced ? "Please fill out the Title and Description fields." : "Please describe what happened.", "error");
return;
}
setIsSubmitting(true);
try {
// 1. Call AI Diagnostics
let probableCause = "Not analyzed";
try {
// Using standard fetch assuming backend is on port 8000
const aiResponse = await fetch(`${API_CONFIG.BACKEND_URL}/ai/analyze_bug`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
bug_title: formData.bug_title,
description: formData.description,
steps_to_reproduce: formData.steps_to_reproduce,
console_errors: diagnostics.consoleErrors
})
});
if (aiResponse.ok) {
const aiData = await aiResponse.json();
if (aiData.probable_cause) {
probableCause = aiData.probable_cause;
}
}
} catch (aiErr) {
console.error("AI Analysis failed silently:", aiErr);
}
// 2. Prepare payload
const payload = {
user_id: user ? user.id : null,
bug_title: actualTitle,
description: formData.description,
steps_to_reproduce: formData.steps_to_reproduce,
expected_result: formData.expected_result,
actual_result: formData.actual_result,
severity: formData.severity,
category: formData.category,
contact_permission: formData.contact_permission,
diagnostic_data: {
url: diagnostics.url,
browser: diagnostics.browser,
screen: diagnostics.screen,
console_errors: diagnostics.consoleErrors,
network_errors: diagnostics.networkErrors,
screenshot_base64: screenshotData, // Attached light-weight jpeg data
ai_probable_cause: probableCause, // Added AI Root Cause Analysis
timestamp: new Date().toISOString()
}
};
const { error } = await supabase
.from('bug_reports')
.insert([payload]);
if (error) {
console.error("Supabase Error:", error);
// Fallback for demo: if table doesn't exist yet, just mock success.
if (error.code === '42P01') {
// Relation does not exist
console.warn("bug_reports table doesn't exist yet. Mocking successful submission for UI preview.");
setTimeout(() => {
addToast("Diagnostic Report Sent (Mock). Please run the SQL script to persist to DB.", "success");
setLastSubmitTime(Date.now());
setIsSubmitting(false);
handleClose();
}, 1000);
return;
}
throw error;
}
addToast("Bug Report Sent. Our diagnostic engine is on it!", "success");
setLastSubmitTime(Date.now());
handleClose();
} catch (error) {
console.error('Error submitting bug:', error);
addToast("Failed to submit bug. Please try again later.", "error");
} finally {
setIsSubmitting(false);
}
};
return (
<>
{/* Floating Trigger Button */}
{/* Trigger Button */}
{!isOpen && !customTrigger && (
Report Bug
)}
{customTrigger && !isOpen && (
{customTrigger}
)}
{/* Modal Dialog */}
{isOpen && (
{/* Header */}
{/* Scrollable Content */}
{/* Info Box */}
Auto-captured Diagnosis
Page URL, browser info, console errors, and screen dimensions are automatically attached. You don't need to provide deep technical details—just describe what you experienced.
{/* Footer */}
Cancel
{isSubmitting ? 'Sending...' : 'Submit Bug Report'}
{!isSubmitting && }
)}
{/* Region Selection Overlay */}
{isSelectingRegion && (
Drag to Select Bug Area
ESC to Cancel
{selectionRect && (
)}
{ e.stopPropagation(); handleCancelSelection(); }}
className="fixed top-6 right-6 p-2 bg-white rounded-full shadow-lg text-slate-600 hover:text-red-500 transition-colors pointer-events-auto"
>
)}
{/* Global style overrides just for scrollbar inside modal if needed */}
>
);
};
export default BugReportWidget;