import { useState, useCallback, useEffect } from 'react'; import { useFetcher } from '@remix-run/react'; import { toast } from 'react-toastify'; import { motion } from 'framer-motion'; import { classNames } from '~/utils/classNames'; import { Button } from '~/components/ui/Button'; import { Input } from '~/components/ui/Input'; import { getApiKeysFromCookies } from '~/components/chat/APIKeyManager'; import Cookies from 'js-cookie'; interface BugReportFormData { title: string; description: string; stepsToReproduce: string; expectedBehavior: string; contactEmail: string; includeEnvironmentInfo: boolean; } interface FormErrors { title?: string; description?: string; stepsToReproduce?: string; expectedBehavior?: string; contactEmail?: string; includeEnvironmentInfo?: string; } interface EnvironmentInfo { browser: string; os: string; screenResolution: string; boltVersion: string; aiProviders: string; projectType: string; currentModel: string; } const BugReportTab = () => { const fetcher = useFetcher(); const [formData, setFormData] = useState({ title: '', description: '', stepsToReproduce: '', expectedBehavior: '', contactEmail: '', includeEnvironmentInfo: false, }); const [environmentInfo, setEnvironmentInfo] = useState({ browser: '', os: '', screenResolution: '', boltVersion: '1.0.0', aiProviders: '', projectType: '', currentModel: '', }); const [showPreview, setShowPreview] = useState(false); const [isSubmitting, setIsSubmitting] = useState(false); const [errors, setErrors] = useState({}); const [showInfoPanel, setShowInfoPanel] = useState(false); // Auto-detect environment info with real data useEffect(() => { const detectEnvironment = () => { const userAgent = navigator.userAgent; let browser = 'Unknown'; let os = 'Unknown'; // Detect browser if (userAgent.includes('Chrome') && !userAgent.includes('Edg')) { browser = `Chrome ${userAgent.match(/Chrome\/(\d+\.\d+)/)?.[1] || 'Unknown'}`; } else if (userAgent.includes('Firefox')) { browser = `Firefox ${userAgent.match(/Firefox\/(\d+\.\d+)/)?.[1] || 'Unknown'}`; } else if (userAgent.includes('Safari') && !userAgent.includes('Chrome')) { browser = `Safari ${userAgent.match(/Version\/(\d+\.\d+)/)?.[1] || 'Unknown'}`; } else if (userAgent.includes('Edg')) { browser = `Edge ${userAgent.match(/Edg\/(\d+\.\d+)/)?.[1] || 'Unknown'}`; } // Detect OS if (userAgent.includes('Windows NT 10.0')) { os = 'Windows 10/11'; } else if (userAgent.includes('Windows NT 6.3')) { os = 'Windows 8.1'; } else if (userAgent.includes('Windows NT 6.1')) { os = 'Windows 7'; } else if (userAgent.includes('Windows')) { os = 'Windows'; } else if (userAgent.includes('Mac OS X')) { const version = userAgent.match(/Mac OS X (\d+_\d+(?:_\d+)?)/)?.[1]?.replace(/_/g, '.'); os = version ? `macOS ${version}` : 'macOS'; } else if (userAgent.includes('Linux')) { os = 'Linux'; } const screenResolution = `${screen.width}x${screen.height}`; // Get real AI provider information const getActiveProviders = () => { const apiKeys = getApiKeysFromCookies(); const activeProviders: string[] = []; // Check which providers have API keys if (apiKeys.OPENAI_API_KEY) { activeProviders.push('OpenAI'); } if (apiKeys.ANTHROPIC_API_KEY) { activeProviders.push('Anthropic'); } if (apiKeys.GOOGLE_GENERATIVE_AI_API_KEY) { activeProviders.push('Google'); } if (apiKeys.GROQ_API_KEY) { activeProviders.push('Groq'); } if (apiKeys.MISTRAL_API_KEY) { activeProviders.push('Mistral'); } if (apiKeys.COHERE_API_KEY) { activeProviders.push('Cohere'); } if (apiKeys.DEEPSEEK_API_KEY) { activeProviders.push('DeepSeek'); } if (apiKeys.XAI_API_KEY) { activeProviders.push('xAI'); } if (apiKeys.OPEN_ROUTER_API_KEY) { activeProviders.push('OpenRouter'); } if (apiKeys.TOGETHER_API_KEY) { activeProviders.push('Together'); } if (apiKeys.PERPLEXITY_API_KEY) { activeProviders.push('Perplexity'); } if (apiKeys.OLLAMA_API_BASE_URL) { activeProviders.push('Ollama'); } if (apiKeys.LMSTUDIO_API_BASE_URL) { activeProviders.push('LMStudio'); } if (apiKeys.OPENAI_LIKE_API_BASE_URL) { activeProviders.push('OpenAI-Compatible'); } return activeProviders.length > 0 ? activeProviders.join(', ') : 'None configured'; }; // Get current model and provider from cookies const getCurrentModel = () => { try { const savedModel = Cookies.get('selectedModel'); const savedProvider = Cookies.get('selectedProvider'); if (savedModel && savedProvider) { const provider = JSON.parse(savedProvider); return `${savedModel} (${provider.name})`; } } catch (error) { console.debug('Could not parse model/provider from cookies:', error); } return 'Default model'; }; // Detect project type based on current context const getProjectType = () => { const url = window.location.href; if (url.includes('/chat/')) { return 'AI Chat Session'; } if (url.includes('/git')) { return 'Git Repository'; } return 'Web Application'; }; // Get bolt.diy version const getBoltVersion = () => { // Try to get version from meta tags or global variables const metaVersion = document.querySelector('meta[name="version"]')?.getAttribute('content'); return metaVersion || '1.0.0'; }; setEnvironmentInfo({ browser, os, screenResolution, boltVersion: getBoltVersion(), aiProviders: getActiveProviders(), projectType: getProjectType(), currentModel: getCurrentModel(), }); }; // Initial detection detectEnvironment(); // Listen for storage changes to update when model/provider changes const handleStorageChange = () => { detectEnvironment(); }; // Listen for cookie changes (model/provider selection) const originalSetItem = Storage.prototype.setItem; Storage.prototype.setItem = function (key, value) { originalSetItem.apply(this, [key, value]); if (key === 'selectedModel' || key === 'selectedProvider') { setTimeout(detectEnvironment, 100); } }; window.addEventListener('storage', handleStorageChange); // Detect changes every 30 seconds to catch any updates const interval = setInterval(detectEnvironment, 30000); return () => { window.removeEventListener('storage', handleStorageChange); clearInterval(interval); Storage.prototype.setItem = originalSetItem; }; }, []); // Handle form submission response useEffect(() => { if (fetcher.data) { const data = fetcher.data as any; if (data.success) { toast.success(`Bug report submitted successfully! Issue #${data.issueNumber} created.`); // Reset form setFormData({ title: '', description: '', stepsToReproduce: '', expectedBehavior: '', contactEmail: '', includeEnvironmentInfo: false, }); setShowPreview(false); } else if (data.error) { toast.error(data.error); } setIsSubmitting(false); } }, [fetcher.data]); // Validation functions const validateField = ( field: keyof BugReportFormData, value: string | boolean | File[] | undefined, ): string | undefined => { switch (field) { case 'title': { const titleValue = value as string; if (!titleValue.trim()) { return 'Title is required'; } if (titleValue.length < 5) { return 'Title must be at least 5 characters long'; } if (titleValue.length > 100) { return 'Title must be 100 characters or less'; } if (!/^[a-zA-Z0-9\s\-_.,!?()[\]{}]+$/.test(titleValue)) { return 'Title contains invalid characters. Please use only letters, numbers, spaces, and basic punctuation'; } return undefined; } case 'description': { const descValue = value as string; if (!descValue.trim()) { return 'Description is required'; } if (descValue.length < 10) { return 'Description must be at least 10 characters long'; } if (descValue.length > 2000) { return 'Description must be 2000 characters or less'; } if (descValue.trim().split(/\s+/).length < 3) { return 'Please provide more details in the description (at least 3 words)'; } return undefined; } case 'stepsToReproduce': { const stepsValue = value as string; if (stepsValue.length > 1000) { return 'Steps to reproduce must be 1000 characters or less'; } return undefined; } case 'expectedBehavior': { const behaviorValue = value as string; if (behaviorValue.length > 1000) { return 'Expected behavior must be 1000 characters or less'; } return undefined; } case 'contactEmail': { const emailValue = value as string; if (emailValue && emailValue.trim()) { const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; if (!emailRegex.test(emailValue)) { return 'Please enter a valid email address'; } } return undefined; } default: return undefined; } }; const validateForm = (): FormErrors => { const newErrors: FormErrors = {}; // Only validate required fields const requiredFields: (keyof BugReportFormData)[] = ['title', 'description']; requiredFields.forEach((field) => { const error = validateField(field, formData[field]); if (error) { newErrors[field] = error; } }); // Validate optional fields only if they have values if (formData.stepsToReproduce.trim()) { const error = validateField('stepsToReproduce', formData.stepsToReproduce); if (error) { newErrors.stepsToReproduce = error; } } if (formData.expectedBehavior.trim()) { const error = validateField('expectedBehavior', formData.expectedBehavior); if (error) { newErrors.expectedBehavior = error; } } if (formData.contactEmail.trim()) { const error = validateField('contactEmail', formData.contactEmail); if (error) { newErrors.contactEmail = error; } } return newErrors; }; // Re-validate form when form data changes to ensure errors are cleared useEffect(() => { const newErrors = validateForm(); setErrors(newErrors); }, [formData]); const handleInputChange = useCallback((field: keyof BugReportFormData, value: string | boolean) => { setFormData((prev) => ({ ...prev, [field]: value })); // Real-time validation for text fields (only for fields that can have errors) if (typeof value === 'string' && field !== 'includeEnvironmentInfo') { const error = validateField(field, value); setErrors((prev) => { const newErrors = { ...prev }; if (error) { newErrors[field as keyof FormErrors] = error; } else { delete newErrors[field as keyof FormErrors]; // Clear the error if validation passes } return newErrors; }); } }, []); const handleSubmit = useCallback( (e: React.FormEvent) => { e.preventDefault(); // Validate entire form const formErrors = validateForm(); setErrors(formErrors); // Check if there are any errors if (Object.keys(formErrors).length > 0) { const errorMessages = Object.values(formErrors).join(', '); toast.error(`Please fix the following errors: ${errorMessages}`); return; } setIsSubmitting(true); const submitData = new FormData(); submitData.append('title', formData.title); submitData.append('description', formData.description); submitData.append('stepsToReproduce', formData.stepsToReproduce); submitData.append('expectedBehavior', formData.expectedBehavior); submitData.append('contactEmail', formData.contactEmail); submitData.append('includeEnvironmentInfo', formData.includeEnvironmentInfo.toString()); if (formData.includeEnvironmentInfo) { submitData.append('environmentInfo', JSON.stringify(environmentInfo)); } fetcher.submit(submitData, { method: 'post', action: '/api/bug-report', }); }, [formData, environmentInfo, fetcher], ); const generatePreview = () => { let preview = `**Bug Report**\n\n`; preview += `**Title:** ${formData.title}\n\n`; preview += `**Description:**\n${formData.description}\n\n`; if (formData.stepsToReproduce) { preview += `**Steps to Reproduce:**\n${formData.stepsToReproduce}\n\n`; } if (formData.expectedBehavior) { preview += `**Expected Behavior:**\n${formData.expectedBehavior}\n\n`; } if (formData.includeEnvironmentInfo) { preview += `**Environment Info:**\n`; preview += `- Browser: ${environmentInfo.browser}\n`; preview += `- OS: ${environmentInfo.os}\n`; preview += `- Screen: ${environmentInfo.screenResolution}\n`; preview += `- bolt.diy: ${environmentInfo.boltVersion}\n`; preview += `- Current Model: ${environmentInfo.currentModel}\n`; preview += `- AI Providers: ${environmentInfo.aiProviders}\n`; preview += `- Project Type: ${environmentInfo.projectType}\n\n`; } if (formData.contactEmail) { preview += `**Contact:** ${formData.contactEmail}\n\n`; } return preview; }; const InfoPanel = () => (

Important Information About Bug Reporting

🔧 Administrator Setup Required:

Bug reporting requires server-side configuration of GitHub API tokens. If you see a "not properly configured" error, please contact your administrator to set up the following environment variables:

GITHUB_BUG_REPORT_TOKEN=ghp_xxxxxxxx
BUG_REPORT_REPO=owner/repository
🔒 Your Privacy:

We never collect your personal data. Environment information is only shared if you explicitly opt-in.

⚡ Rate Limits:

To prevent spam, you can submit up to 5 bug reports per hour.

📝 Good Bug Reports Include:
  • Clear, descriptive title (5-100 characters)
  • Detailed description of what happened
  • Steps to reproduce the issue
  • What you expected to happen instead
  • Environment info (browser, OS) if relevant
); return (

Bug Report

Help us improve bolt.diy by reporting bugs and issues. Your report will be automatically submitted to our GitHub repository.

{showInfoPanel && } {!showPreview ? ( {/* Title */}
handleInputChange('title', e.target.value)} placeholder="Brief, clear description of the bug (e.g., 'Login button not working on mobile')" maxLength={100} className={classNames( 'w-full', errors.title ? 'border-red-500 focus:border-red-500 focus:ring-red-500' : '', )} required />
{errors.title && (

{errors.title}

)}

90 ? 'text-orange-500' : 'text-gray-500', )} > {formData.title.length}/100

{/* Description */}