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 (
{isOpen && ( <>
setIsOpen(false)} /> {options.map((option) => ( ))} )}
); }; 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 */}

Report a Bug

{/* 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.

{advanced && (
)} {/* What happened? */}
{advanced && ( <> {/* Steps to Reproduce */}
{/* Expected vs Actual */}
{/* Dropdowns */}
)} {advanced && ( <> {/* Permission */} {/* Diagnostics Read-Only View */}
Captured Diagnostics
Path: {diagnostics.url.split(window.location.host)[1] || diagnostics.url}
Browser: {diagnostics.browser.split(' ')[0]} {diagnostics.browser.split(' ')[diagnostics.browser.split(' ').length - 1]}
Screen: {diagnostics.screen}
Errors: 0 ? "text-amber-500 font-bold" : ""}>{diagnostics.consoleErrors.length}
Telemetry: Active
)} {/* Action Attachments: Screenshot */} {advanced && (

Advanced Attachments

Capture your screen to show exactly what's wrong.

{!screenshotData && ( )}
{screenshotData && ( Captured screen
)}
)}
{/* Footer */}
)}
{/* Region Selection Overlay */} {isSelectingRegion && (
Drag to Select Bug Area
ESC to Cancel
{selectionRect && (
)} )} {/* Global style overrides just for scrollbar inside modal if needed */}