From 7072600b509e7554188077e47d20b93c376fb144 Mon Sep 17 00:00:00 2001 From: Stijnus Date: Sun, 31 Aug 2025 15:44:33 +0200 Subject: [PATCH] feat: Redesign bug reporting and header actions - Remove BugReportTab component and move bug reporting to header - Add bug report icon to main header and profile dropdown - Add sync button to main header with deploy button styling - Remove duplicate sync/bug report buttons from workbench header - Clean up unused imports and code - Improve header button organization and visibility --- .../@settings/core/AvatarDropdown.tsx | 18 + .../@settings/core/ControlPanel.tsx | 4 +- app/components/@settings/core/constants.ts | 5 +- app/components/@settings/core/types.ts | 4 +- .../tabs/bug-report/BugReportTab.tsx | 896 ------------------ .../header/HeaderActionButtons.client.tsx | 80 +- app/components/workbench/Workbench.client.tsx | 49 - 7 files changed, 99 insertions(+), 957 deletions(-) delete mode 100644 app/components/@settings/tabs/bug-report/BugReportTab.tsx diff --git a/app/components/@settings/core/AvatarDropdown.tsx b/app/components/@settings/core/AvatarDropdown.tsx index c6c1845..f9f6eb7 100644 --- a/app/components/@settings/core/AvatarDropdown.tsx +++ b/app/components/@settings/core/AvatarDropdown.tsx @@ -133,6 +133,24 @@ export const AvatarDropdown = ({ onSelectTab }: AvatarDropdownProps) => { Service Status + + + window.open('https://github.com/stackblitz-labs/bolt.diy/issues/new?template=bug_report.yml', '_blank') + } + > +
+ Report Bug + diff --git a/app/components/@settings/core/ControlPanel.tsx b/app/components/@settings/core/ControlPanel.tsx index 456cfc2..276151b 100644 --- a/app/components/@settings/core/ControlPanel.tsx +++ b/app/components/@settings/core/ControlPanel.tsx @@ -26,7 +26,6 @@ import CloudProvidersTab from '~/components/@settings/tabs/providers/cloud/Cloud import ServiceStatusTab from '~/components/@settings/tabs/providers/status/ServiceStatusTab'; import LocalProvidersTab from '~/components/@settings/tabs/providers/local/LocalProvidersTab'; import McpTab from '~/components/@settings/tabs/mcp/McpTab'; -import BugReportTab from '~/components/@settings/tabs/bug-report/BugReportTab'; interface ControlPanelProps { open: boolean; @@ -143,8 +142,7 @@ export const ControlPanel = ({ open, onClose }: ControlPanelProps) => { return ; case 'mcp': return ; - case 'bug-report': - return ; + default: return null; } diff --git a/app/components/@settings/core/constants.ts b/app/components/@settings/core/constants.ts index 72312dd..fc7088a 100644 --- a/app/components/@settings/core/constants.ts +++ b/app/components/@settings/core/constants.ts @@ -12,7 +12,6 @@ export const TAB_ICONS: Record = { connection: 'i-ph:wifi-high', 'event-logs': 'i-ph:list-bullets', mcp: 'i-ph:wrench', - 'bug-report': 'i-ph:bug', }; export const TAB_LABELS: Record = { @@ -27,7 +26,6 @@ export const TAB_LABELS: Record = { connection: 'Connection', 'event-logs': 'Event Logs', mcp: 'MCP Servers', - 'bug-report': 'Bug Report', }; export const TAB_DESCRIPTIONS: Record = { @@ -42,7 +40,6 @@ export const TAB_DESCRIPTIONS: Record = { connection: 'Check connection status and settings', 'event-logs': 'View system events and logs', mcp: 'Configure MCP (Model Context Protocol) servers', - 'bug-report': 'Report bugs and issues directly to developers', }; export const DEFAULT_TAB_CONFIG = [ @@ -55,7 +52,7 @@ export const DEFAULT_TAB_CONFIG = [ { id: 'notifications', visible: true, window: 'user' as const, order: 5 }, { id: 'event-logs', visible: true, window: 'user' as const, order: 6 }, { id: 'mcp', visible: true, window: 'user' as const, order: 7 }, - { id: 'bug-report', visible: true, window: 'user' as const, order: 8 }, + { id: 'profile', visible: true, window: 'user' as const, order: 9 }, { id: 'service-status', visible: true, window: 'user' as const, order: 10 }, { id: 'settings', visible: true, window: 'user' as const, order: 11 }, diff --git a/app/components/@settings/core/types.ts b/app/components/@settings/core/types.ts index 78f43bd..d4a518f 100644 --- a/app/components/@settings/core/types.ts +++ b/app/components/@settings/core/types.ts @@ -13,8 +13,7 @@ export type TabType = | 'service-status' | 'connection' | 'event-logs' - | 'mcp' - | 'bug-report'; + | 'mcp'; export type WindowType = 'user' | 'developer'; @@ -75,7 +74,6 @@ export const TAB_LABELS: Record = { connection: 'Connections', 'event-logs': 'Event Logs', mcp: 'MCP Servers', - 'bug-report': 'Bug Report', }; export const categoryLabels: Record = { diff --git a/app/components/@settings/tabs/bug-report/BugReportTab.tsx b/app/components/@settings/tabs/bug-report/BugReportTab.tsx deleted file mode 100644 index e8c6a52..0000000 --- a/app/components/@settings/tabs/bug-report/BugReportTab.tsx +++ /dev/null @@ -1,896 +0,0 @@ -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 */} -
- -