bolt.diy/app/components/@settings/tabs/debug/DebugTab.tsx
Stijnus b86fd63700
feat: bolt dyi datatab (#1570)
* Update DataTab.tsx

## API Key Import Fix

We identified and fixed an issue with the API key import functionality in the DataTab component. The problem was that API keys were being stored in localStorage instead of cookies, and the key format was being incorrectly processed.

### Changes Made:

1. **Updated `handleImportAPIKeys` function**:
   - Changed to store API keys in cookies instead of localStorage
   - Modified to use provider names directly as keys (e.g., "OpenAI", "Google")
   - Added logic to skip comment fields (keys starting with "_")
   - Added page reload after successful import to apply changes immediately

2. **Updated `handleDownloadTemplate` function**:
   - Changed template format to use provider names as keys
   - Added explanatory comment in the template
   - Removed URL-related keys that weren't being used properly

3. **Fixed template format**:
   - Template now uses the correct format with provider names as keys
   - Added support for all available providers including Hyperbolic

These changes ensure that when users download the template, fill it with their API keys, and import it back, the keys are properly stored in cookies with the correct format that the application expects.

* backwards compatible old import template

* Update the export / import settings

Settings Export/Import Improvements
We've completely redesigned the settings export and import functionality to ensure all application settings are properly backed up and restored:
Key Improvements
Comprehensive Export Format: Now captures ALL settings from both localStorage and cookies, organized into logical categories (core, providers, features, UI, connections, debug, updates)
Robust Import System: Automatically detects format version and handles both new and legacy formats with detailed error handling
Complete Settings Coverage: Properly exports and imports settings from ALL tabs including:
Local provider configurations (Ollama, LMStudio, etc.)
Cloud provider API keys (OpenAI, Anthropic, etc.)
Feature toggles and preferences
UI configurations and tab settings
Connection settings (GitHub, Netlify)
Debug configurations and logs
Technical Details
Added version tracking to export files for better compatibility
Implemented fallback mechanisms if primary import methods fail
Added detailed logging for troubleshooting import/export issues
Created helper functions for safer data handling
Maintained backward compatibility with older export formats

Feature Settings:
Feature flags and viewed features
Developer mode settings
Energy saver mode configurations
User Preferences:
User profile information
Theme settings
Tab configurations
Connection Settings:
Netlify connections
Git authentication credentials
Any other service connections
Debug and System Settings:
Debug flags and acknowledged issues
Error logs and event logs
Update settings and preferences

* Update DataTab.tsx

* Update GithubConnection.tsx

revert the code back as asked

* feat: enhance style to match the project

* feat:small improvements

* feat: add major improvements

* Update Dialog.tsx

* Delete DataTab.tsx.bak

* feat: small updates

* Update DataVisualization.tsx

* feat: dark mode fix
2025-03-29 20:43:07 +01:00

2111 lines
82 KiB
TypeScript

import React, { useEffect, useState, useMemo, useCallback } from 'react';
import { toast } from 'react-toastify';
import { classNames } from '~/utils/classNames';
import { logStore, type LogEntry } from '~/lib/stores/logs';
import { useStore } from '@nanostores/react';
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '~/components/ui/Collapsible';
import { Progress } from '~/components/ui/Progress';
import { ScrollArea } from '~/components/ui/ScrollArea';
import { Badge } from '~/components/ui/Badge';
import { Dialog, DialogRoot, DialogTitle } from '~/components/ui/Dialog';
import { jsPDF } from 'jspdf';
import { useSettings } from '~/lib/hooks/useSettings';
interface SystemInfo {
os: string;
arch: string;
platform: string;
cpus: string;
memory: {
total: string;
free: string;
used: string;
percentage: number;
};
node: string;
browser: {
name: string;
version: string;
language: string;
userAgent: string;
cookiesEnabled: boolean;
online: boolean;
platform: string;
cores: number;
};
screen: {
width: number;
height: number;
colorDepth: number;
pixelRatio: number;
};
time: {
timezone: string;
offset: number;
locale: string;
};
performance: {
memory: {
jsHeapSizeLimit: number;
totalJSHeapSize: number;
usedJSHeapSize: number;
usagePercentage: number;
};
timing: {
loadTime: number;
domReadyTime: number;
readyStart: number;
redirectTime: number;
appcacheTime: number;
unloadEventTime: number;
lookupDomainTime: number;
connectTime: number;
requestTime: number;
initDomTreeTime: number;
loadEventTime: number;
};
navigation: {
type: number;
redirectCount: number;
};
};
network: {
downlink: number;
effectiveType: string;
rtt: number;
saveData: boolean;
type: string;
};
battery?: {
charging: boolean;
chargingTime: number;
dischargingTime: number;
level: number;
};
storage: {
quota: number;
usage: number;
persistent: boolean;
temporary: boolean;
};
}
interface GitHubRepoInfo {
fullName: string;
defaultBranch: string;
stars: number;
forks: number;
openIssues?: number;
}
interface GitInfo {
local: {
commitHash: string;
branch: string;
commitTime: string;
author: string;
email: string;
remoteUrl: string;
repoName: string;
};
github?: {
currentRepo: GitHubRepoInfo;
upstream?: GitHubRepoInfo;
};
isForked?: boolean;
}
interface WebAppInfo {
name: string;
version: string;
description: string;
license: string;
environment: string;
timestamp: string;
runtimeInfo: {
nodeVersion: string;
};
dependencies: {
production: Array<{ name: string; version: string; type: string }>;
development: Array<{ name: string; version: string; type: string }>;
peer: Array<{ name: string; version: string; type: string }>;
optional: Array<{ name: string; version: string; type: string }>;
};
gitInfo: GitInfo;
}
// Add Ollama service status interface
interface OllamaServiceStatus {
isRunning: boolean;
lastChecked: Date;
error?: string;
models?: Array<{
name: string;
size: string;
quantization: string;
}>;
}
interface ExportFormat {
id: string;
label: string;
icon: string;
handler: () => void;
}
const DependencySection = ({
title,
deps,
}: {
title: string;
deps: Array<{ name: string; version: string; type: string }>;
}) => {
const [isOpen, setIsOpen] = useState(false);
if (deps.length === 0) {
return null;
}
return (
<Collapsible open={isOpen} onOpenChange={setIsOpen}>
<CollapsibleTrigger
className={classNames(
'flex w-full items-center justify-between p-4',
'bg-white dark:bg-[#0A0A0A]',
'hover:bg-purple-50/50 dark:hover:bg-[#1a1a1a]',
'border-b border-[#E5E5E5] dark:border-[#1A1A1A]',
'transition-colors duration-200',
'first:rounded-t-lg last:rounded-b-lg',
{ 'hover:rounded-lg': !isOpen },
)}
>
<div className="flex items-center gap-3">
<div className="i-ph:package text-bolt-elements-textSecondary w-4 h-4" />
<span className="text-base text-bolt-elements-textPrimary">
{title} Dependencies ({deps.length})
</span>
</div>
<div className="flex items-center gap-2">
<span className="text-sm text-bolt-elements-textSecondary">{isOpen ? 'Hide' : 'Show'}</span>
<div
className={classNames(
'i-ph:caret-down w-4 h-4 transform transition-transform duration-200 text-bolt-elements-textSecondary',
isOpen ? 'rotate-180' : '',
)}
/>
</div>
</CollapsibleTrigger>
<CollapsibleContent>
<ScrollArea
className={classNames(
'h-[200px] w-full',
'bg-white dark:bg-[#0A0A0A]',
'border-b border-[#E5E5E5] dark:border-[#1A1A1A]',
'last:rounded-b-lg last:border-b-0',
)}
>
<div className="space-y-2 p-4">
{deps.map((dep) => (
<div key={dep.name} className="flex items-center justify-between text-sm">
<span className="text-bolt-elements-textPrimary">{dep.name}</span>
<span className="text-bolt-elements-textSecondary">{dep.version}</span>
</div>
))}
</div>
</ScrollArea>
</CollapsibleContent>
</Collapsible>
);
};
export default function DebugTab() {
const [systemInfo, setSystemInfo] = useState<SystemInfo | null>(null);
const [webAppInfo, setWebAppInfo] = useState<WebAppInfo | null>(null);
const [ollamaStatus, setOllamaStatus] = useState<OllamaServiceStatus>({
isRunning: false,
lastChecked: new Date(),
});
const [loading, setLoading] = useState({
systemInfo: false,
webAppInfo: false,
errors: false,
performance: false,
});
const [openSections, setOpenSections] = useState({
system: false,
webapp: false,
errors: false,
performance: false,
});
const { providers } = useSettings();
// Subscribe to logStore updates
const logs = useStore(logStore.logs);
const errorLogs = useMemo(() => {
return Object.values(logs).filter(
(log): log is LogEntry => typeof log === 'object' && log !== null && 'level' in log && log.level === 'error',
);
}, [logs]);
// Set up error listeners when component mounts
useEffect(() => {
const handleError = (event: ErrorEvent) => {
logStore.logError(event.message, event.error, {
filename: event.filename,
lineNumber: event.lineno,
columnNumber: event.colno,
});
};
const handleRejection = (event: PromiseRejectionEvent) => {
logStore.logError('Unhandled Promise Rejection', event.reason);
};
window.addEventListener('error', handleError);
window.addEventListener('unhandledrejection', handleRejection);
return () => {
window.removeEventListener('error', handleError);
window.removeEventListener('unhandledrejection', handleRejection);
};
}, []);
// Check for errors when the errors section is opened
useEffect(() => {
if (openSections.errors) {
checkErrors();
}
}, [openSections.errors]);
// Load initial data when component mounts
useEffect(() => {
const loadInitialData = async () => {
await Promise.all([getSystemInfo(), getWebAppInfo()]);
};
loadInitialData();
}, []);
// Refresh data when sections are opened
useEffect(() => {
if (openSections.system) {
getSystemInfo();
}
if (openSections.webapp) {
getWebAppInfo();
}
}, [openSections.system, openSections.webapp]);
// Add periodic refresh of git info
useEffect(() => {
if (!openSections.webapp) {
return undefined;
}
// Initial fetch
const fetchGitInfo = async () => {
try {
const response = await fetch('/api/system/git-info');
const updatedGitInfo = (await response.json()) as GitInfo;
setWebAppInfo((prev) => {
if (!prev) {
return null;
}
// Only update if the data has changed
if (JSON.stringify(prev.gitInfo) === JSON.stringify(updatedGitInfo)) {
return prev;
}
return {
...prev,
gitInfo: updatedGitInfo,
};
});
} catch (error) {
console.error('Failed to fetch git info:', error);
}
};
fetchGitInfo();
// Refresh every 5 minutes instead of every second
const interval = setInterval(fetchGitInfo, 5 * 60 * 1000);
return () => clearInterval(interval);
}, [openSections.webapp]);
const getSystemInfo = async () => {
try {
setLoading((prev) => ({ ...prev, systemInfo: true }));
// Get better OS detection
const userAgent = navigator.userAgent;
let detectedOS = 'Unknown';
let detectedArch = 'unknown';
// Improved OS detection
if (userAgent.indexOf('Win') !== -1) {
detectedOS = 'Windows';
} else if (userAgent.indexOf('Mac') !== -1) {
detectedOS = 'macOS';
} else if (userAgent.indexOf('Linux') !== -1) {
detectedOS = 'Linux';
} else if (userAgent.indexOf('Android') !== -1) {
detectedOS = 'Android';
} else if (/iPhone|iPad|iPod/.test(userAgent)) {
detectedOS = 'iOS';
}
// Better architecture detection
if (userAgent.indexOf('x86_64') !== -1 || userAgent.indexOf('x64') !== -1 || userAgent.indexOf('WOW64') !== -1) {
detectedArch = 'x64';
} else if (userAgent.indexOf('x86') !== -1 || userAgent.indexOf('i686') !== -1) {
detectedArch = 'x86';
} else if (userAgent.indexOf('arm64') !== -1 || userAgent.indexOf('aarch64') !== -1) {
detectedArch = 'arm64';
} else if (userAgent.indexOf('arm') !== -1) {
detectedArch = 'arm';
}
// Get browser info with improved detection
const browserName = (() => {
if (userAgent.indexOf('Edge') !== -1 || userAgent.indexOf('Edg/') !== -1) {
return 'Edge';
}
if (userAgent.indexOf('Chrome') !== -1) {
return 'Chrome';
}
if (userAgent.indexOf('Firefox') !== -1) {
return 'Firefox';
}
if (userAgent.indexOf('Safari') !== -1) {
return 'Safari';
}
return 'Unknown';
})();
const browserVersionMatch = userAgent.match(/(Edge|Edg|Chrome|Firefox|Safari)[\s/](\d+(\.\d+)*)/);
const browserVersion = browserVersionMatch ? browserVersionMatch[2] : 'Unknown';
// Get performance metrics
const memory = (performance as any).memory || {};
const timing = performance.timing;
const navigation = performance.navigation;
const connection = (navigator as any).connection || {};
// Try to use Navigation Timing API Level 2 when available
let loadTime = 0;
let domReadyTime = 0;
try {
const navEntries = performance.getEntriesByType('navigation');
if (navEntries.length > 0) {
const navTiming = navEntries[0] as PerformanceNavigationTiming;
loadTime = navTiming.loadEventEnd - navTiming.startTime;
domReadyTime = navTiming.domContentLoadedEventEnd - navTiming.startTime;
} else {
// Fall back to older API
loadTime = timing.loadEventEnd - timing.navigationStart;
domReadyTime = timing.domContentLoadedEventEnd - timing.navigationStart;
}
} catch {
// Fall back to older API if Navigation Timing API Level 2 is not available
loadTime = timing.loadEventEnd - timing.navigationStart;
domReadyTime = timing.domContentLoadedEventEnd - timing.navigationStart;
}
// Get battery info
let batteryInfo;
try {
const battery = await (navigator as any).getBattery();
batteryInfo = {
charging: battery.charging,
chargingTime: battery.chargingTime,
dischargingTime: battery.dischargingTime,
level: battery.level * 100,
};
} catch {
console.log('Battery API not supported');
}
// Get storage info
let storageInfo = {
quota: 0,
usage: 0,
persistent: false,
temporary: false,
};
try {
const storage = await navigator.storage.estimate();
const persistent = await navigator.storage.persist();
storageInfo = {
quota: storage.quota || 0,
usage: storage.usage || 0,
persistent,
temporary: !persistent,
};
} catch {
console.log('Storage API not supported');
}
// Get memory info from browser performance API
const performanceMemory = (performance as any).memory || {};
const totalMemory = performanceMemory.jsHeapSizeLimit || 0;
const usedMemory = performanceMemory.usedJSHeapSize || 0;
const freeMemory = totalMemory - usedMemory;
const memoryPercentage = totalMemory ? (usedMemory / totalMemory) * 100 : 0;
const systemInfo: SystemInfo = {
os: detectedOS,
arch: detectedArch,
platform: navigator.platform || 'unknown',
cpus: navigator.hardwareConcurrency + ' cores',
memory: {
total: formatBytes(totalMemory),
free: formatBytes(freeMemory),
used: formatBytes(usedMemory),
percentage: Math.round(memoryPercentage),
},
node: 'browser',
browser: {
name: browserName,
version: browserVersion,
language: navigator.language,
userAgent: navigator.userAgent,
cookiesEnabled: navigator.cookieEnabled,
online: navigator.onLine,
platform: navigator.platform || 'unknown',
cores: navigator.hardwareConcurrency,
},
screen: {
width: window.screen.width,
height: window.screen.height,
colorDepth: window.screen.colorDepth,
pixelRatio: window.devicePixelRatio,
},
time: {
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
offset: new Date().getTimezoneOffset(),
locale: navigator.language,
},
performance: {
memory: {
jsHeapSizeLimit: memory.jsHeapSizeLimit || 0,
totalJSHeapSize: memory.totalJSHeapSize || 0,
usedJSHeapSize: memory.usedJSHeapSize || 0,
usagePercentage: memory.totalJSHeapSize ? (memory.usedJSHeapSize / memory.totalJSHeapSize) * 100 : 0,
},
timing: {
loadTime,
domReadyTime,
readyStart: timing.fetchStart - timing.navigationStart,
redirectTime: timing.redirectEnd - timing.redirectStart,
appcacheTime: timing.domainLookupStart - timing.fetchStart,
unloadEventTime: timing.unloadEventEnd - timing.unloadEventStart,
lookupDomainTime: timing.domainLookupEnd - timing.domainLookupStart,
connectTime: timing.connectEnd - timing.connectStart,
requestTime: timing.responseEnd - timing.requestStart,
initDomTreeTime: timing.domInteractive - timing.responseEnd,
loadEventTime: timing.loadEventEnd - timing.loadEventStart,
},
navigation: {
type: navigation.type,
redirectCount: navigation.redirectCount,
},
},
network: {
downlink: connection?.downlink || 0,
effectiveType: connection?.effectiveType || 'unknown',
rtt: connection?.rtt || 0,
saveData: connection?.saveData || false,
type: connection?.type || 'unknown',
},
battery: batteryInfo,
storage: storageInfo,
};
setSystemInfo(systemInfo);
toast.success('System information updated');
} catch (error) {
toast.error('Failed to get system information');
console.error('Failed to get system information:', error);
} finally {
setLoading((prev) => ({ ...prev, systemInfo: false }));
}
};
// Helper function to format bytes to human readable format with better precision
const formatBytes = (bytes: number) => {
if (bytes === 0) {
return '0 B';
}
const units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB'];
const i = Math.floor(Math.log(bytes) / Math.log(1024));
// Return with proper precision based on unit size
if (i === 0) {
return `${bytes} ${units[i]}`;
}
return `${(bytes / Math.pow(1024, i)).toFixed(2)} ${units[i]}`;
};
const getWebAppInfo = async () => {
try {
setLoading((prev) => ({ ...prev, webAppInfo: true }));
const [appResponse, gitResponse] = await Promise.all([
fetch('/api/system/app-info'),
fetch('/api/system/git-info'),
]);
if (!appResponse.ok || !gitResponse.ok) {
throw new Error('Failed to fetch webapp info');
}
const appData = (await appResponse.json()) as Omit<WebAppInfo, 'gitInfo'>;
const gitData = (await gitResponse.json()) as GitInfo;
console.log('Git Info Response:', gitData); // Add logging to debug
setWebAppInfo({
...appData,
gitInfo: gitData,
});
toast.success('WebApp information updated');
return true;
} catch (error) {
console.error('Failed to fetch webapp info:', error);
toast.error('Failed to fetch webapp information');
setWebAppInfo(null);
return false;
} finally {
setLoading((prev) => ({ ...prev, webAppInfo: false }));
}
};
const handleLogPerformance = () => {
try {
setLoading((prev) => ({ ...prev, performance: true }));
// Get performance metrics using modern Performance API
const performanceEntries = performance.getEntriesByType('navigation')[0] as PerformanceNavigationTiming;
const memory = (performance as any).memory;
// Calculate timing metrics
const timingMetrics = {
loadTime: performanceEntries.loadEventEnd - performanceEntries.startTime,
domReadyTime: performanceEntries.domContentLoadedEventEnd - performanceEntries.startTime,
fetchTime: performanceEntries.responseEnd - performanceEntries.fetchStart,
redirectTime: performanceEntries.redirectEnd - performanceEntries.redirectStart,
dnsTime: performanceEntries.domainLookupEnd - performanceEntries.domainLookupStart,
tcpTime: performanceEntries.connectEnd - performanceEntries.connectStart,
ttfb: performanceEntries.responseStart - performanceEntries.requestStart,
processingTime: performanceEntries.loadEventEnd - performanceEntries.responseEnd,
};
// Get resource timing data
const resourceEntries = performance.getEntriesByType('resource');
const resourceStats = {
totalResources: resourceEntries.length,
totalSize: resourceEntries.reduce((total, entry) => total + ((entry as any).transferSize || 0), 0),
totalTime: Math.max(...resourceEntries.map((entry) => entry.duration)),
};
// Get memory metrics
const memoryMetrics = memory
? {
jsHeapSizeLimit: memory.jsHeapSizeLimit,
totalJSHeapSize: memory.totalJSHeapSize,
usedJSHeapSize: memory.usedJSHeapSize,
heapUtilization: (memory.usedJSHeapSize / memory.totalJSHeapSize) * 100,
}
: null;
// Get frame rate metrics
let fps = 0;
if ('requestAnimationFrame' in window) {
const times: number[] = [];
function calculateFPS(now: number) {
times.push(now);
if (times.length > 10) {
const fps = Math.round((1000 * 10) / (now - times[0]));
times.shift();
return fps;
}
requestAnimationFrame(calculateFPS);
return 0;
}
fps = calculateFPS(performance.now());
}
// Log all performance metrics
logStore.logSystem('Performance Metrics', {
timing: timingMetrics,
resources: resourceStats,
memory: memoryMetrics,
fps,
timestamp: new Date().toISOString(),
navigationEntry: {
type: performanceEntries.type,
redirectCount: performanceEntries.redirectCount,
},
});
toast.success('Performance metrics logged');
} catch (error) {
toast.error('Failed to log performance metrics');
console.error('Failed to log performance metrics:', error);
} finally {
setLoading((prev) => ({ ...prev, performance: false }));
}
};
const checkErrors = async () => {
try {
setLoading((prev) => ({ ...prev, errors: true }));
// Get errors from log store
const storedErrors = errorLogs;
if (storedErrors.length === 0) {
toast.success('No errors found');
} else {
toast.warning(`Found ${storedErrors.length} error(s)`);
}
} catch (error) {
toast.error('Failed to check errors');
console.error('Failed to check errors:', error);
} finally {
setLoading((prev) => ({ ...prev, errors: false }));
}
};
const exportDebugInfo = () => {
try {
const debugData = {
timestamp: new Date().toISOString(),
system: systemInfo,
webApp: webAppInfo,
errors: logStore.getLogs().filter((log: LogEntry) => log.level === 'error'),
performance: {
memory: (performance as any).memory || {},
timing: performance.timing,
navigation: performance.navigation,
},
};
const blob = new Blob([JSON.stringify(debugData, null, 2)], { type: 'application/json' });
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `bolt-debug-info-${new Date().toISOString()}.json`;
document.body.appendChild(a);
a.click();
window.URL.revokeObjectURL(url);
document.body.removeChild(a);
toast.success('Debug information exported successfully');
} catch (error) {
console.error('Failed to export debug info:', error);
toast.error('Failed to export debug information');
}
};
const exportAsCSV = () => {
try {
const debugData = {
system: systemInfo,
webApp: webAppInfo,
errors: logStore.getLogs().filter((log: LogEntry) => log.level === 'error'),
performance: {
memory: (performance as any).memory || {},
timing: performance.timing,
navigation: performance.navigation,
},
};
// Convert the data to CSV format
const csvData = [
['Category', 'Key', 'Value'],
...Object.entries(debugData).flatMap(([category, data]) =>
Object.entries(data || {}).map(([key, value]) => [
category,
key,
typeof value === 'object' ? JSON.stringify(value) : String(value),
]),
),
];
// Create CSV content
const csvContent = csvData.map((row) => row.join(',')).join('\n');
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `bolt-debug-info-${new Date().toISOString()}.csv`;
document.body.appendChild(a);
a.click();
window.URL.revokeObjectURL(url);
document.body.removeChild(a);
toast.success('Debug information exported as CSV');
} catch (error) {
console.error('Failed to export CSV:', error);
toast.error('Failed to export debug information as CSV');
}
};
const exportAsPDF = () => {
try {
const debugData = {
system: systemInfo,
webApp: webAppInfo,
errors: logStore.getLogs().filter((log: LogEntry) => log.level === 'error'),
performance: {
memory: (performance as any).memory || {},
timing: performance.timing,
navigation: performance.navigation,
},
};
// Create new PDF document
const doc = new jsPDF();
const lineHeight = 7;
let yPos = 20;
const margin = 20;
const pageWidth = doc.internal.pageSize.getWidth();
const maxLineWidth = pageWidth - 2 * margin;
// Add key-value pair with better formatting
const addKeyValue = (key: string, value: any, indent = 0) => {
// Check if we need a new page
if (yPos > doc.internal.pageSize.getHeight() - 20) {
doc.addPage();
yPos = margin;
}
doc.setFontSize(10);
doc.setTextColor('#374151');
doc.setFont('helvetica', 'bold');
// Format the key with proper spacing
const formattedKey = key.replace(/([A-Z])/g, ' $1').trim();
doc.text(formattedKey + ':', margin + indent, yPos);
doc.setFont('helvetica', 'normal');
doc.setTextColor('#6B7280');
let valueText;
if (typeof value === 'object' && value !== null) {
// Skip rendering if value is empty object
if (Object.keys(value).length === 0) {
return;
}
yPos += lineHeight;
Object.entries(value).forEach(([subKey, subValue]) => {
// Check for page break before each sub-item
if (yPos > doc.internal.pageSize.getHeight() - 20) {
doc.addPage();
yPos = margin;
}
const formattedSubKey = subKey.replace(/([A-Z])/g, ' $1').trim();
addKeyValue(formattedSubKey, subValue, indent + 10);
});
return;
} else {
valueText = String(value);
}
const valueX = margin + indent + doc.getTextWidth(formattedKey + ': ');
const maxValueWidth = maxLineWidth - indent - doc.getTextWidth(formattedKey + ': ');
const lines = doc.splitTextToSize(valueText, maxValueWidth);
// Check if we need a new page for the value
if (yPos + lines.length * lineHeight > doc.internal.pageSize.getHeight() - 20) {
doc.addPage();
yPos = margin;
}
doc.text(lines, valueX, yPos);
yPos += lines.length * lineHeight;
};
// Add section header with page break check
const addSectionHeader = (title: string) => {
// Check if we need a new page
if (yPos + 20 > doc.internal.pageSize.getHeight() - 20) {
doc.addPage();
yPos = margin;
}
yPos += lineHeight;
doc.setFillColor('#F3F4F6');
doc.rect(margin - 2, yPos - 5, pageWidth - 2 * (margin - 2), lineHeight + 6, 'F');
doc.setFont('helvetica', 'bold');
doc.setTextColor('#111827');
doc.setFontSize(12);
doc.text(title.toUpperCase(), margin, yPos);
doc.setFont('helvetica', 'normal');
yPos += lineHeight * 1.5;
};
// Add horizontal line with page break check
const addHorizontalLine = () => {
// Check if we need a new page
if (yPos + 10 > doc.internal.pageSize.getHeight() - 20) {
doc.addPage();
yPos = margin;
return; // Skip drawing line if we just started a new page
}
doc.setDrawColor('#E5E5E5');
doc.line(margin, yPos, pageWidth - margin, yPos);
yPos += lineHeight;
};
// Helper function to add footer to all pages
const addFooters = () => {
const totalPages = doc.internal.pages.length - 1;
for (let i = 1; i <= totalPages; i++) {
doc.setPage(i);
doc.setFontSize(8);
doc.setTextColor('#9CA3AF');
doc.text(`Page ${i} of ${totalPages}`, pageWidth / 2, doc.internal.pageSize.getHeight() - 10, {
align: 'center',
});
}
};
// Title and Header (first page only)
doc.setFillColor('#6366F1');
doc.rect(0, 0, pageWidth, 40, 'F');
doc.setTextColor('#FFFFFF');
doc.setFontSize(24);
doc.setFont('helvetica', 'bold');
doc.text('Debug Information Report', margin, 25);
yPos = 50;
// Timestamp and metadata
doc.setTextColor('#6B7280');
doc.setFontSize(10);
doc.setFont('helvetica', 'normal');
const timestamp = new Date().toLocaleString(undefined, {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
});
doc.text(`Generated: ${timestamp}`, margin, yPos);
yPos += lineHeight * 2;
// System Information Section
if (debugData.system) {
addSectionHeader('System Information');
// OS and Architecture
addKeyValue('Operating System', debugData.system.os);
addKeyValue('Architecture', debugData.system.arch);
addKeyValue('Platform', debugData.system.platform);
addKeyValue('CPU Cores', debugData.system.cpus);
// Memory
const memory = debugData.system.memory;
addKeyValue('Memory', {
'Total Memory': memory.total,
'Used Memory': memory.used,
'Free Memory': memory.free,
Usage: memory.percentage + '%',
});
// Browser Information
const browser = debugData.system.browser;
addKeyValue('Browser', {
Name: browser.name,
Version: browser.version,
Language: browser.language,
Platform: browser.platform,
'Cookies Enabled': browser.cookiesEnabled ? 'Yes' : 'No',
'Online Status': browser.online ? 'Online' : 'Offline',
});
// Screen Information
const screen = debugData.system.screen;
addKeyValue('Screen', {
Resolution: `${screen.width}x${screen.height}`,
'Color Depth': screen.colorDepth + ' bit',
'Pixel Ratio': screen.pixelRatio + 'x',
});
// Time Information
const time = debugData.system.time;
addKeyValue('Time Settings', {
Timezone: time.timezone,
'UTC Offset': time.offset / 60 + ' hours',
Locale: time.locale,
});
addHorizontalLine();
}
// Web App Information Section
if (debugData.webApp) {
addSectionHeader('Web App Information');
// Basic Info
addKeyValue('Application', {
Name: debugData.webApp.name,
Version: debugData.webApp.version,
Environment: debugData.webApp.environment,
'Node Version': debugData.webApp.runtimeInfo.nodeVersion,
});
// Git Information
if (debugData.webApp.gitInfo) {
const gitInfo = debugData.webApp.gitInfo.local;
addKeyValue('Git Information', {
Branch: gitInfo.branch,
Commit: gitInfo.commitHash,
Author: gitInfo.author,
'Commit Time': gitInfo.commitTime,
Repository: gitInfo.repoName,
});
if (debugData.webApp.gitInfo.github) {
const githubInfo = debugData.webApp.gitInfo.github.currentRepo;
addKeyValue('GitHub Information', {
Repository: githubInfo.fullName,
'Default Branch': githubInfo.defaultBranch,
Stars: githubInfo.stars,
Forks: githubInfo.forks,
'Open Issues': githubInfo.openIssues || 0,
});
}
}
addHorizontalLine();
}
// Performance Section
if (debugData.performance) {
addSectionHeader('Performance Metrics');
// Memory Usage
const memory = debugData.performance.memory || {};
const totalHeap = memory.totalJSHeapSize || 0;
const usedHeap = memory.usedJSHeapSize || 0;
const usagePercentage = memory.usagePercentage || 0;
addKeyValue('Memory Usage', {
'Total Heap Size': formatBytes(totalHeap),
'Used Heap Size': formatBytes(usedHeap),
Usage: usagePercentage.toFixed(1) + '%',
});
// Timing Metrics
const timing = debugData.performance.timing || {};
const navigationStart = timing.navigationStart || 0;
const loadEventEnd = timing.loadEventEnd || 0;
const domContentLoadedEventEnd = timing.domContentLoadedEventEnd || 0;
const responseEnd = timing.responseEnd || 0;
const requestStart = timing.requestStart || 0;
const loadTime = loadEventEnd > navigationStart ? loadEventEnd - navigationStart : 0;
const domReadyTime =
domContentLoadedEventEnd > navigationStart ? domContentLoadedEventEnd - navigationStart : 0;
const requestTime = responseEnd > requestStart ? responseEnd - requestStart : 0;
addKeyValue('Page Load Metrics', {
'Total Load Time': (loadTime / 1000).toFixed(2) + ' seconds',
'DOM Ready Time': (domReadyTime / 1000).toFixed(2) + ' seconds',
'Request Time': (requestTime / 1000).toFixed(2) + ' seconds',
});
// Network Information
if (debugData.system?.network) {
const network = debugData.system.network;
addKeyValue('Network Information', {
'Connection Type': network.type || 'Unknown',
'Effective Type': network.effectiveType || 'Unknown',
'Download Speed': (network.downlink || 0) + ' Mbps',
'Latency (RTT)': (network.rtt || 0) + ' ms',
'Data Saver': network.saveData ? 'Enabled' : 'Disabled',
});
}
addHorizontalLine();
}
// Errors Section
if (debugData.errors && debugData.errors.length > 0) {
addSectionHeader('Error Log');
debugData.errors.forEach((error: LogEntry, index: number) => {
doc.setTextColor('#DC2626');
doc.setFontSize(10);
doc.setFont('helvetica', 'bold');
doc.text(`Error ${index + 1}:`, margin, yPos);
yPos += lineHeight;
doc.setFont('helvetica', 'normal');
doc.setTextColor('#6B7280');
addKeyValue('Message', error.message, 10);
if (error.stack) {
addKeyValue('Stack', error.stack, 10);
}
if (error.source) {
addKeyValue('Source', error.source, 10);
}
yPos += lineHeight;
});
}
// Add footers to all pages at the end
addFooters();
// Save the PDF
doc.save(`bolt-debug-info-${new Date().toISOString()}.pdf`);
toast.success('Debug information exported as PDF');
} catch (error) {
console.error('Failed to export PDF:', error);
toast.error('Failed to export debug information as PDF');
}
};
const exportAsText = () => {
try {
const debugData = {
system: systemInfo,
webApp: webAppInfo,
errors: logStore.getLogs().filter((log: LogEntry) => log.level === 'error'),
performance: {
memory: (performance as any).memory || {},
timing: performance.timing,
navigation: performance.navigation,
},
};
const textContent = Object.entries(debugData)
.map(([category, data]) => {
return `${category.toUpperCase()}\n${'-'.repeat(30)}\n${JSON.stringify(data, null, 2)}\n\n`;
})
.join('\n');
const blob = new Blob([textContent], { type: 'text/plain' });
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `bolt-debug-info-${new Date().toISOString()}.txt`;
document.body.appendChild(a);
a.click();
window.URL.revokeObjectURL(url);
document.body.removeChild(a);
toast.success('Debug information exported as text file');
} catch (error) {
console.error('Failed to export text file:', error);
toast.error('Failed to export debug information as text file');
}
};
const exportFormats: ExportFormat[] = [
{
id: 'json',
label: 'Export as JSON',
icon: 'i-ph:file-json',
handler: exportDebugInfo,
},
{
id: 'csv',
label: 'Export as CSV',
icon: 'i-ph:file-csv',
handler: exportAsCSV,
},
{
id: 'pdf',
label: 'Export as PDF',
icon: 'i-ph:file-pdf',
handler: exportAsPDF,
},
{
id: 'txt',
label: 'Export as Text',
icon: 'i-ph:file-text',
handler: exportAsText,
},
];
// Add Ollama health check function
const checkOllamaStatus = useCallback(async () => {
try {
const ollamaProvider = providers?.Ollama;
const baseUrl = ollamaProvider?.settings?.baseUrl || 'http://127.0.0.1:11434';
// First check if service is running
const versionResponse = await fetch(`${baseUrl}/api/version`);
if (!versionResponse.ok) {
throw new Error('Service not running');
}
// Then fetch installed models
const modelsResponse = await fetch(`${baseUrl}/api/tags`);
const modelsData = (await modelsResponse.json()) as {
models: Array<{ name: string; size: string; quantization: string }>;
};
setOllamaStatus({
isRunning: true,
lastChecked: new Date(),
models: modelsData.models,
});
} catch {
setOllamaStatus({
isRunning: false,
error: 'Connection failed',
lastChecked: new Date(),
models: undefined,
});
}
}, [providers]);
// Monitor Ollama provider status and check periodically
useEffect(() => {
const ollamaProvider = providers?.Ollama;
if (ollamaProvider?.settings?.enabled) {
// Check immediately when provider is enabled
checkOllamaStatus();
// Set up periodic checks every 10 seconds
const intervalId = setInterval(checkOllamaStatus, 10000);
return () => clearInterval(intervalId);
}
return undefined;
}, [providers, checkOllamaStatus]);
// Replace the existing export button with this new component
const ExportButton = () => {
const [isOpen, setIsOpen] = useState(false);
const handleOpenChange = useCallback((open: boolean) => {
setIsOpen(open);
}, []);
const handleFormatClick = useCallback((handler: () => void) => {
handler();
setIsOpen(false);
}, []);
return (
<DialogRoot open={isOpen} onOpenChange={handleOpenChange}>
<button
onClick={() => setIsOpen(true)}
className={classNames(
'group flex items-center gap-2',
'rounded-lg px-3 py-1.5',
'text-sm text-gray-900 dark:text-white',
'bg-[#FAFAFA] dark:bg-[#0A0A0A]',
'border border-[#E5E5E5] dark:border-[#1A1A1A]',
'hover:bg-purple-500/10 dark:hover:bg-purple-500/20',
'transition-all duration-200',
)}
>
<span className="i-ph:download text-lg text-gray-500 dark:text-gray-400 group-hover:text-purple-500 transition-colors" />
Export
</button>
<Dialog showCloseButton>
<div className="p-6">
<DialogTitle className="flex items-center gap-2">
<div className="i-ph:download w-5 h-5" />
Export Debug Information
</DialogTitle>
<div className="mt-4 flex flex-col gap-2">
{exportFormats.map((format) => (
<button
key={format.id}
onClick={() => handleFormatClick(format.handler)}
className={classNames(
'flex items-center gap-3 px-4 py-3 text-sm rounded-lg transition-colors w-full text-left',
'bg-white dark:bg-[#0A0A0A]',
'border border-[#E5E5E5] dark:border-[#1A1A1A]',
'hover:bg-purple-50 dark:hover:bg-[#1a1a1a]',
'hover:border-purple-200 dark:hover:border-purple-900/30',
'text-bolt-elements-textPrimary',
)}
>
<div className={classNames(format.icon, 'w-5 h-5')} />
<div>
<div className="font-medium">{format.label}</div>
<div className="text-xs text-bolt-elements-textSecondary mt-0.5">
{format.id === 'json' && 'Export as a structured JSON file'}
{format.id === 'csv' && 'Export as a CSV spreadsheet'}
{format.id === 'pdf' && 'Export as a formatted PDF document'}
{format.id === 'txt' && 'Export as a formatted text file'}
</div>
</div>
</button>
))}
</div>
</div>
</Dialog>
</DialogRoot>
);
};
// Add helper function to get Ollama status text and color
const getOllamaStatus = () => {
const ollamaProvider = providers?.Ollama;
const isOllamaEnabled = ollamaProvider?.settings?.enabled;
if (!isOllamaEnabled) {
return {
status: 'Disabled',
color: 'text-red-500',
bgColor: 'bg-red-500',
message: 'Ollama provider is disabled in settings',
};
}
if (!ollamaStatus.isRunning) {
return {
status: 'Not Running',
color: 'text-red-500',
bgColor: 'bg-red-500',
message: ollamaStatus.error || 'Ollama service is not running',
};
}
const modelCount = ollamaStatus.models?.length ?? 0;
return {
status: 'Running',
color: 'text-green-500',
bgColor: 'bg-green-500',
message: `Ollama service is running with ${modelCount} installed models (Provider: Enabled)`,
};
};
// Add type for status result
type StatusResult = {
status: string;
color: string;
bgColor: string;
message: string;
};
const status = getOllamaStatus() as StatusResult;
return (
<div className="flex flex-col gap-6 max-w-7xl mx-auto p-4">
{/* Quick Stats Banner */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
{/* Errors Card */}
<div className="p-4 rounded-xl bg-white dark:bg-[#0A0A0A] border border-[#E5E5E5] dark:border-[#1A1A1A] hover:border-purple-500/30 transition-all duration-200 h-[180px] flex flex-col">
<div className="flex items-center gap-2">
<div className="i-ph:warning-octagon text-purple-500 w-4 h-4" />
<div className="text-sm text-bolt-elements-textSecondary">Errors</div>
</div>
<div className="flex items-center gap-2 mt-2">
<span
className={classNames('text-2xl font-semibold', errorLogs.length > 0 ? 'text-red-500' : 'text-green-500')}
>
{errorLogs.length}
</span>
</div>
<div className="text-xs text-bolt-elements-textSecondary mt-2 flex items-center gap-1.5">
<div
className={classNames(
'w-3.5 h-3.5',
errorLogs.length > 0 ? 'i-ph:warning text-red-500' : 'i-ph:check-circle text-green-500',
)}
/>
{errorLogs.length > 0 ? 'Errors detected' : 'No errors detected'}
</div>
</div>
{/* Memory Usage Card */}
<div className="p-4 rounded-xl bg-white dark:bg-[#0A0A0A] border border-[#E5E5E5] dark:border-[#1A1A1A] hover:border-purple-500/30 transition-all duration-200 h-[180px] flex flex-col">
<div className="flex items-center gap-2">
<div className="i-ph:cpu text-purple-500 w-4 h-4" />
<div className="text-sm text-bolt-elements-textSecondary">Memory Usage</div>
</div>
<div className="flex items-center gap-2 mt-2">
<span
className={classNames(
'text-2xl font-semibold',
(systemInfo?.memory?.percentage ?? 0) > 80
? 'text-red-500'
: (systemInfo?.memory?.percentage ?? 0) > 60
? 'text-yellow-500'
: 'text-green-500',
)}
>
{systemInfo?.memory?.percentage ?? 0}%
</span>
</div>
<Progress
value={systemInfo?.memory?.percentage ?? 0}
className={classNames(
'mt-2',
(systemInfo?.memory?.percentage ?? 0) > 80
? '[&>div]:bg-red-500'
: (systemInfo?.memory?.percentage ?? 0) > 60
? '[&>div]:bg-yellow-500'
: '[&>div]:bg-green-500',
)}
/>
<div className="text-xs text-bolt-elements-textSecondary mt-2 flex items-center gap-1.5">
<div className="i-ph:info w-3.5 h-3.5 text-purple-500" />
Used: {systemInfo?.memory.used ?? '0 GB'} / {systemInfo?.memory.total ?? '0 GB'}
</div>
</div>
{/* Page Load Time Card */}
<div className="p-4 rounded-xl bg-white dark:bg-[#0A0A0A] border border-[#E5E5E5] dark:border-[#1A1A1A] hover:border-purple-500/30 transition-all duration-200 h-[180px] flex flex-col">
<div className="flex items-center gap-2">
<div className="i-ph:timer text-purple-500 w-4 h-4" />
<div className="text-sm text-bolt-elements-textSecondary">Page Load Time</div>
</div>
<div className="flex items-center gap-2 mt-2">
<span
className={classNames(
'text-2xl font-semibold',
(systemInfo?.performance.timing.loadTime ?? 0) > 2000
? 'text-red-500'
: (systemInfo?.performance.timing.loadTime ?? 0) > 1000
? 'text-yellow-500'
: 'text-green-500',
)}
>
{systemInfo ? (systemInfo.performance.timing.loadTime / 1000).toFixed(2) : '-'}s
</span>
</div>
<div className="text-xs text-bolt-elements-textSecondary mt-2 flex items-center gap-1.5">
<div className="i-ph:code w-3.5 h-3.5 text-purple-500" />
DOM Ready: {systemInfo ? (systemInfo.performance.timing.domReadyTime / 1000).toFixed(2) : '-'}s
</div>
</div>
{/* Network Speed Card */}
<div className="p-4 rounded-xl bg-white dark:bg-[#0A0A0A] border border-[#E5E5E5] dark:border-[#1A1A1A] hover:border-purple-500/30 transition-all duration-200 h-[180px] flex flex-col">
<div className="flex items-center gap-2">
<div className="i-ph:wifi-high text-purple-500 w-4 h-4" />
<div className="text-sm text-bolt-elements-textSecondary">Network Speed</div>
</div>
<div className="flex items-center gap-2 mt-2">
<span
className={classNames(
'text-2xl font-semibold',
(systemInfo?.network.downlink ?? 0) < 5
? 'text-red-500'
: (systemInfo?.network.downlink ?? 0) < 10
? 'text-yellow-500'
: 'text-green-500',
)}
>
{systemInfo?.network.downlink ?? '-'} Mbps
</span>
</div>
<div className="text-xs text-bolt-elements-textSecondary mt-2 flex items-center gap-1.5">
<div className="i-ph:activity w-3.5 h-3.5 text-purple-500" />
RTT: {systemInfo?.network.rtt ?? '-'} ms
</div>
</div>
{/* Ollama Service Card - Now spans all 4 columns */}
<div className="md:col-span-4 p-6 rounded-xl bg-white dark:bg-[#0A0A0A] border border-[#E5E5E5] dark:border-[#1A1A1A] hover:border-purple-500/30 transition-all duration-200 h-[260px] flex flex-col">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="i-ph:robot text-purple-500 w-5 h-5" />
<div>
<div className="text-base font-medium text-bolt-elements-textPrimary">Ollama Service</div>
<div className="text-xs text-bolt-elements-textSecondary mt-0.5">{status.message}</div>
</div>
</div>
<div className="flex items-center gap-3">
<div className="flex items-center gap-2 px-2.5 py-1 rounded-full bg-bolt-elements-background-depth-3">
<div
className={classNames('w-2 h-2 rounded-full animate-pulse', status.bgColor, {
'shadow-lg shadow-green-500/20': status.status === 'Running',
'shadow-lg shadow-red-500/20': status.status === 'Not Running',
})}
/>
<span className={classNames('text-xs font-medium flex items-center gap-1', status.color)}>
{status.status}
</span>
</div>
<div className="text-[10px] text-bolt-elements-textTertiary flex items-center gap-1.5">
<div className="i-ph:clock w-3 h-3" />
{ollamaStatus.lastChecked.toLocaleTimeString()}
</div>
</div>
</div>
<div className="mt-6 flex-1 min-h-0 flex flex-col">
{status.status === 'Running' && ollamaStatus.models && ollamaStatus.models.length > 0 ? (
<>
<div className="text-xs font-medium text-bolt-elements-textSecondary flex items-center justify-between mb-3">
<div className="flex items-center gap-2">
<div className="i-ph:cube-duotone w-4 h-4 text-purple-500" />
<span>Installed Models</span>
<Badge variant="secondary" className="ml-1">
{ollamaStatus.models.length}
</Badge>
</div>
</div>
<div className="overflow-y-auto flex-1 scrollbar-thin scrollbar-thumb-gray-300 dark:scrollbar-thumb-gray-700 scrollbar-track-transparent hover:scrollbar-thumb-gray-400 dark:hover:scrollbar-thumb-gray-600">
<div className="grid grid-cols-2 gap-3 pr-2">
{ollamaStatus.models.map((model) => (
<div
key={model.name}
className="text-sm bg-bolt-elements-background-depth-3 hover:bg-bolt-elements-background-depth-4 rounded-lg px-4 py-3 flex items-center justify-between transition-colors group"
>
<div className="flex items-center gap-2 text-bolt-elements-textSecondary">
<div className="i-ph:cube w-4 h-4 text-purple-500/70 group-hover:text-purple-500 transition-colors" />
<span className="font-mono truncate">{model.name}</span>
</div>
<Badge variant="outline" className="ml-2 text-xs font-mono">
{Math.round(parseInt(model.size) / 1024 / 1024)}MB
</Badge>
</div>
))}
</div>
</div>
</>
) : (
<div className="flex-1 flex items-center justify-center">
<div className="flex flex-col items-center gap-3 max-w-[280px] text-center">
<div
className={classNames('w-12 h-12', {
'i-ph:warning-circle text-red-500/80':
status.status === 'Not Running' || status.status === 'Disabled',
'i-ph:cube-duotone text-purple-500/80': status.status === 'Running',
})}
/>
<span className="text-sm text-bolt-elements-textSecondary">{status.message}</span>
</div>
</div>
)}
</div>
</div>
</div>
{/* Action Buttons */}
<div className="flex flex-wrap gap-4">
<button
onClick={getSystemInfo}
disabled={loading.systemInfo}
className={classNames(
'flex items-center gap-2 px-4 py-2 text-sm font-medium rounded-lg transition-colors',
'bg-white dark:bg-[#0A0A0A]',
'border border-[#E5E5E5] dark:border-[#1A1A1A]',
'hover:bg-purple-50 dark:hover:bg-[#1a1a1a]',
'hover:border-purple-200 dark:hover:border-purple-900/30',
'text-bolt-elements-textPrimary',
{ 'opacity-50 cursor-not-allowed': loading.systemInfo },
)}
>
{loading.systemInfo ? (
<div className="i-ph:spinner-gap w-4 h-4 animate-spin" />
) : (
<div className="i-ph:gear w-4 h-4" />
)}
Update System Info
</button>
<button
onClick={handleLogPerformance}
disabled={loading.performance}
className={classNames(
'flex items-center gap-2 px-4 py-2 text-sm font-medium rounded-lg transition-colors',
'bg-white dark:bg-[#0A0A0A]',
'border border-[#E5E5E5] dark:border-[#1A1A1A]',
'hover:bg-purple-50 dark:hover:bg-[#1a1a1a]',
'hover:border-purple-200 dark:hover:border-purple-900/30',
'text-bolt-elements-textPrimary',
{ 'opacity-50 cursor-not-allowed': loading.performance },
)}
>
{loading.performance ? (
<div className="i-ph:spinner-gap w-4 h-4 animate-spin" />
) : (
<div className="i-ph:chart-bar w-4 h-4" />
)}
Log Performance
</button>
<button
onClick={checkErrors}
disabled={loading.errors}
className={classNames(
'flex items-center gap-2 px-4 py-2 text-sm font-medium rounded-lg transition-colors',
'bg-white dark:bg-[#0A0A0A]',
'border border-[#E5E5E5] dark:border-[#1A1A1A]',
'hover:bg-purple-50 dark:hover:bg-[#1a1a1a]',
'hover:border-purple-200 dark:hover:border-purple-900/30',
'text-bolt-elements-textPrimary',
{ 'opacity-50 cursor-not-allowed': loading.errors },
)}
>
{loading.errors ? (
<div className="i-ph:spinner-gap w-4 h-4 animate-spin" />
) : (
<div className="i-ph:warning w-4 h-4" />
)}
Check Errors
</button>
<button
onClick={getWebAppInfo}
disabled={loading.webAppInfo}
className={classNames(
'flex items-center gap-2 px-4 py-2 text-sm font-medium rounded-lg transition-colors',
'bg-white dark:bg-[#0A0A0A]',
'border border-[#E5E5E5] dark:border-[#1A1A1A]',
'hover:bg-purple-50 dark:hover:bg-[#1a1a1a]',
'hover:border-purple-200 dark:hover:border-purple-900/30',
'text-bolt-elements-textPrimary',
{ 'opacity-50 cursor-not-allowed': loading.webAppInfo },
)}
>
{loading.webAppInfo ? (
<div className="i-ph:spinner-gap w-4 h-4 animate-spin" />
) : (
<div className="i-ph:info w-4 h-4" />
)}
Fetch WebApp Info
</button>
<ExportButton />
</div>
{/* System Information */}
<Collapsible
open={openSections.system}
onOpenChange={(open: boolean) => setOpenSections((prev) => ({ ...prev, system: open }))}
className="w-full"
>
<CollapsibleTrigger className="w-full">
<div className="flex items-center justify-between p-6 rounded-xl bg-white dark:bg-[#0A0A0A] border border-[#E5E5E5] dark:border-[#1A1A1A]">
<div className="flex items-center gap-3">
<div className="i-ph:cpu text-purple-500 w-5 h-5" />
<h3 className="text-base font-medium text-bolt-elements-textPrimary">System Information</h3>
</div>
<div
className={classNames(
'i-ph:caret-down w-4 h-4 transform transition-transform duration-200',
openSections.system ? 'rotate-180' : '',
)}
/>
</div>
</CollapsibleTrigger>
<CollapsibleContent>
<div className="p-6 mt-2 rounded-xl bg-white dark:bg-[#0A0A0A] border border-[#E5E5E5] dark:border-[#1A1A1A]">
{systemInfo ? (
<div className="grid grid-cols-2 gap-6">
<div className="space-y-2">
<div className="text-sm flex items-center gap-2">
<div className="i-ph:desktop text-bolt-elements-textSecondary w-4 h-4" />
<span className="text-bolt-elements-textSecondary">OS: </span>
<span className="text-bolt-elements-textPrimary">{systemInfo.os}</span>
</div>
<div className="text-sm flex items-center gap-2">
<div className="i-ph:device-mobile text-bolt-elements-textSecondary w-4 h-4" />
<span className="text-bolt-elements-textSecondary">Platform: </span>
<span className="text-bolt-elements-textPrimary">{systemInfo.platform}</span>
</div>
<div className="text-sm flex items-center gap-2">
<div className="i-ph:microchip text-bolt-elements-textSecondary w-4 h-4" />
<span className="text-bolt-elements-textSecondary">Architecture: </span>
<span className="text-bolt-elements-textPrimary">{systemInfo.arch}</span>
</div>
<div className="text-sm flex items-center gap-2">
<div className="i-ph:cpu text-bolt-elements-textSecondary w-4 h-4" />
<span className="text-bolt-elements-textSecondary">CPU Cores: </span>
<span className="text-bolt-elements-textPrimary">{systemInfo.cpus}</span>
</div>
<div className="text-sm flex items-center gap-2">
<div className="i-ph:node text-bolt-elements-textSecondary w-4 h-4" />
<span className="text-bolt-elements-textSecondary">Node Version: </span>
<span className="text-bolt-elements-textPrimary">{systemInfo.node}</span>
</div>
<div className="text-sm flex items-center gap-2">
<div className="i-ph:wifi-high text-bolt-elements-textSecondary w-4 h-4" />
<span className="text-bolt-elements-textSecondary">Network Type: </span>
<span className="text-bolt-elements-textPrimary">
{systemInfo.network.type} ({systemInfo.network.effectiveType})
</span>
</div>
<div className="text-sm flex items-center gap-2">
<div className="i-ph:gauge text-bolt-elements-textSecondary w-4 h-4" />
<span className="text-bolt-elements-textSecondary">Network Speed: </span>
<span className="text-bolt-elements-textPrimary">
{systemInfo.network.downlink}Mbps (RTT: {systemInfo.network.rtt}ms)
</span>
</div>
{systemInfo.battery && (
<div className="text-sm flex items-center gap-2">
<div className="i-ph:battery-charging text-bolt-elements-textSecondary w-4 h-4" />
<span className="text-bolt-elements-textSecondary">Battery: </span>
<span className="text-bolt-elements-textPrimary">
{systemInfo.battery.level.toFixed(1)}% {systemInfo.battery.charging ? '(Charging)' : ''}
</span>
</div>
)}
<div className="text-sm flex items-center gap-2">
<div className="i-ph:hard-drive text-bolt-elements-textSecondary w-4 h-4" />
<span className="text-bolt-elements-textSecondary">Storage: </span>
<span className="text-bolt-elements-textPrimary">
{(systemInfo.storage.usage / (1024 * 1024 * 1024)).toFixed(2)}GB /{' '}
{(systemInfo.storage.quota / (1024 * 1024 * 1024)).toFixed(2)}GB
</span>
</div>
</div>
<div className="space-y-2">
<div className="text-sm flex items-center gap-2">
<div className="i-ph:database text-bolt-elements-textSecondary w-4 h-4" />
<span className="text-bolt-elements-textSecondary">Memory Usage: </span>
<span className="text-bolt-elements-textPrimary">
{systemInfo.memory.used} / {systemInfo.memory.total} ({systemInfo.memory.percentage}%)
</span>
</div>
<div className="text-sm flex items-center gap-2">
<div className="i-ph:browser text-bolt-elements-textSecondary w-4 h-4" />
<span className="text-bolt-elements-textSecondary">Browser: </span>
<span className="text-bolt-elements-textPrimary">
{systemInfo.browser.name} {systemInfo.browser.version}
</span>
</div>
<div className="text-sm flex items-center gap-2">
<div className="i-ph:monitor text-bolt-elements-textSecondary w-4 h-4" />
<span className="text-bolt-elements-textSecondary">Screen: </span>
<span className="text-bolt-elements-textPrimary">
{systemInfo.screen.width}x{systemInfo.screen.height} ({systemInfo.screen.pixelRatio}x)
</span>
</div>
<div className="text-sm flex items-center gap-2">
<div className="i-ph:clock text-bolt-elements-textSecondary w-4 h-4" />
<span className="text-bolt-elements-textSecondary">Timezone: </span>
<span className="text-bolt-elements-textPrimary">{systemInfo.time.timezone}</span>
</div>
<div className="text-sm flex items-center gap-2">
<div className="i-ph:translate text-bolt-elements-textSecondary w-4 h-4" />
<span className="text-bolt-elements-textSecondary">Language: </span>
<span className="text-bolt-elements-textPrimary">{systemInfo.browser.language}</span>
</div>
<div className="text-sm flex items-center gap-2">
<div className="i-ph:chart-pie text-bolt-elements-textSecondary w-4 h-4" />
<span className="text-bolt-elements-textSecondary">JS Heap: </span>
<span className="text-bolt-elements-textPrimary">
{(systemInfo.performance.memory.usedJSHeapSize / (1024 * 1024)).toFixed(1)}MB /{' '}
{(systemInfo.performance.memory.totalJSHeapSize / (1024 * 1024)).toFixed(1)}MB (
{systemInfo.performance.memory.usagePercentage.toFixed(1)}%)
</span>
</div>
<div className="text-sm flex items-center gap-2">
<div className="i-ph:timer text-bolt-elements-textSecondary w-4 h-4" />
<span className="text-bolt-elements-textSecondary">Page Load: </span>
<span className="text-bolt-elements-textPrimary">
{(systemInfo.performance.timing.loadTime / 1000).toFixed(2)}s
</span>
</div>
<div className="text-sm flex items-center gap-2">
<div className="i-ph:code text-bolt-elements-textSecondary w-4 h-4" />
<span className="text-bolt-elements-textSecondary">DOM Ready: </span>
<span className="text-bolt-elements-textPrimary">
{(systemInfo.performance.timing.domReadyTime / 1000).toFixed(2)}s
</span>
</div>
</div>
</div>
) : (
<div className="text-sm text-bolt-elements-textSecondary">Loading system information...</div>
)}
</div>
</CollapsibleContent>
</Collapsible>
{/* Performance Metrics */}
<Collapsible
open={openSections.performance}
onOpenChange={(open: boolean) => setOpenSections((prev) => ({ ...prev, performance: open }))}
className="w-full"
>
<CollapsibleTrigger className="w-full">
<div className="flex items-center justify-between p-6 rounded-xl bg-white dark:bg-[#0A0A0A] border border-[#E5E5E5] dark:border-[#1A1A1A]">
<div className="flex items-center gap-3">
<div className="i-ph:chart-line text-purple-500 w-5 h-5" />
<h3 className="text-base font-medium text-bolt-elements-textPrimary">Performance Metrics</h3>
</div>
<div
className={classNames(
'i-ph:caret-down w-4 h-4 transform transition-transform duration-200',
openSections.performance ? 'rotate-180' : '',
)}
/>
</div>
</CollapsibleTrigger>
<CollapsibleContent>
<div className="p-6 mt-2 rounded-xl bg-white dark:bg-[#0A0A0A] border border-[#E5E5E5] dark:border-[#1A1A1A]">
{systemInfo && (
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<div className="text-sm">
<span className="text-bolt-elements-textSecondary">Page Load Time: </span>
<span className="text-bolt-elements-textPrimary">
{(systemInfo.performance.timing.loadTime / 1000).toFixed(2)}s
</span>
</div>
<div className="text-sm">
<span className="text-bolt-elements-textSecondary">DOM Ready Time: </span>
<span className="text-bolt-elements-textPrimary">
{(systemInfo.performance.timing.domReadyTime / 1000).toFixed(2)}s
</span>
</div>
<div className="text-sm">
<span className="text-bolt-elements-textSecondary">Request Time: </span>
<span className="text-bolt-elements-textPrimary">
{(systemInfo.performance.timing.requestTime / 1000).toFixed(2)}s
</span>
</div>
<div className="text-sm">
<span className="text-bolt-elements-textSecondary">Redirect Time: </span>
<span className="text-bolt-elements-textPrimary">
{(systemInfo.performance.timing.redirectTime / 1000).toFixed(2)}s
</span>
</div>
</div>
<div className="space-y-2">
<div className="text-sm">
<span className="text-bolt-elements-textSecondary">JS Heap Usage: </span>
<span className="text-bolt-elements-textPrimary">
{(systemInfo.performance.memory.usedJSHeapSize / (1024 * 1024)).toFixed(1)}MB /{' '}
{(systemInfo.performance.memory.totalJSHeapSize / (1024 * 1024)).toFixed(1)}MB
</span>
</div>
<div className="text-sm">
<span className="text-bolt-elements-textSecondary">Heap Utilization: </span>
<span className="text-bolt-elements-textPrimary">
{systemInfo.performance.memory.usagePercentage.toFixed(1)}%
</span>
</div>
<div className="text-sm">
<span className="text-bolt-elements-textSecondary">Navigation Type: </span>
<span className="text-bolt-elements-textPrimary">
{systemInfo.performance.navigation.type === 0
? 'Navigate'
: systemInfo.performance.navigation.type === 1
? 'Reload'
: systemInfo.performance.navigation.type === 2
? 'Back/Forward'
: 'Other'}
</span>
</div>
<div className="text-sm">
<span className="text-bolt-elements-textSecondary">Redirects: </span>
<span className="text-bolt-elements-textPrimary">
{systemInfo.performance.navigation.redirectCount}
</span>
</div>
</div>
</div>
)}
</div>
</CollapsibleContent>
</Collapsible>
{/* WebApp Information */}
<Collapsible
open={openSections.webapp}
onOpenChange={(open) => setOpenSections((prev) => ({ ...prev, webapp: open }))}
className="w-full"
>
<CollapsibleTrigger className="w-full">
<div className="flex items-center justify-between p-6 rounded-xl bg-white dark:bg-[#0A0A0A] border border-[#E5E5E5] dark:border-[#1A1A1A]">
<div className="flex items-center gap-3">
<div className="i-ph:info text-blue-500 w-5 h-5" />
<h3 className="text-base font-medium text-bolt-elements-textPrimary">WebApp Information</h3>
{loading.webAppInfo && <span className="loading loading-spinner loading-sm" />}
</div>
<div
className={classNames(
'i-ph:caret-down w-4 h-4 transform transition-transform duration-200',
openSections.webapp ? 'rotate-180' : '',
)}
/>
</div>
</CollapsibleTrigger>
<CollapsibleContent>
<div className="p-6 mt-2 rounded-xl bg-white dark:bg-[#0A0A0A] border border-[#E5E5E5] dark:border-[#1A1A1A]">
{loading.webAppInfo ? (
<div className="flex items-center justify-center p-8">
<span className="loading loading-spinner loading-lg" />
</div>
) : !webAppInfo ? (
<div className="flex flex-col items-center justify-center p-8 text-bolt-elements-textSecondary">
<div className="i-ph:warning-circle w-8 h-8 mb-2" />
<p>Failed to load WebApp information</p>
<button
onClick={() => getWebAppInfo()}
className="mt-4 px-4 py-2 text-sm bg-blue-500 text-white rounded-lg hover:bg-blue-600 transition-colors"
>
Retry
</button>
</div>
) : (
<div className="grid grid-cols-2 gap-6">
<div>
<h3 className="mb-4 text-base font-medium text-bolt-elements-textPrimary">Basic Information</h3>
<div className="space-y-3">
<div className="text-sm flex items-center gap-2">
<div className="i-ph:app-window text-bolt-elements-textSecondary w-4 h-4" />
<span className="text-bolt-elements-textSecondary">Name:</span>
<span className="text-bolt-elements-textPrimary">{webAppInfo.name}</span>
</div>
<div className="text-sm flex items-center gap-2">
<div className="i-ph:tag text-bolt-elements-textSecondary w-4 h-4" />
<span className="text-bolt-elements-textSecondary">Version:</span>
<span className="text-bolt-elements-textPrimary">{webAppInfo.version}</span>
</div>
<div className="text-sm flex items-center gap-2">
<div className="i-ph:certificate text-bolt-elements-textSecondary w-4 h-4" />
<span className="text-bolt-elements-textSecondary">License:</span>
<span className="text-bolt-elements-textPrimary">{webAppInfo.license}</span>
</div>
<div className="text-sm flex items-center gap-2">
<div className="i-ph:cloud text-bolt-elements-textSecondary w-4 h-4" />
<span className="text-bolt-elements-textSecondary">Environment:</span>
<span className="text-bolt-elements-textPrimary">{webAppInfo.environment}</span>
</div>
<div className="text-sm flex items-center gap-2">
<div className="i-ph:node text-bolt-elements-textSecondary w-4 h-4" />
<span className="text-bolt-elements-textSecondary">Node Version:</span>
<span className="text-bolt-elements-textPrimary">{webAppInfo.runtimeInfo.nodeVersion}</span>
</div>
</div>
</div>
<div>
<h3 className="mb-4 text-base font-medium text-bolt-elements-textPrimary">Git Information</h3>
<div className="space-y-3">
<div className="text-sm flex items-center gap-2">
<div className="i-ph:git-branch text-bolt-elements-textSecondary w-4 h-4" />
<span className="text-bolt-elements-textSecondary">Branch:</span>
<span className="text-bolt-elements-textPrimary">{webAppInfo.gitInfo.local.branch}</span>
</div>
<div className="text-sm flex items-center gap-2">
<div className="i-ph:git-commit text-bolt-elements-textSecondary w-4 h-4" />
<span className="text-bolt-elements-textSecondary">Commit:</span>
<span className="text-bolt-elements-textPrimary">{webAppInfo.gitInfo.local.commitHash}</span>
</div>
<div className="text-sm flex items-center gap-2">
<div className="i-ph:user text-bolt-elements-textSecondary w-4 h-4" />
<span className="text-bolt-elements-textSecondary">Author:</span>
<span className="text-bolt-elements-textPrimary">{webAppInfo.gitInfo.local.author}</span>
</div>
<div className="text-sm flex items-center gap-2">
<div className="i-ph:clock text-bolt-elements-textSecondary w-4 h-4" />
<span className="text-bolt-elements-textSecondary">Commit Time:</span>
<span className="text-bolt-elements-textPrimary">{webAppInfo.gitInfo.local.commitTime}</span>
</div>
{webAppInfo.gitInfo.github && (
<>
<div className="mt-4 pt-4 border-t border-gray-200 dark:border-gray-800">
<div className="text-sm flex items-center gap-2">
<div className="i-ph:git-repository text-bolt-elements-textSecondary w-4 h-4" />
<span className="text-bolt-elements-textSecondary">Repository:</span>
<span className="text-bolt-elements-textPrimary">
{webAppInfo.gitInfo.github.currentRepo.fullName}
{webAppInfo.gitInfo.isForked && ' (fork)'}
</span>
</div>
<div className="mt-2 flex items-center gap-4 text-sm">
<div className="flex items-center gap-1">
<div className="i-ph:star text-yellow-500 w-4 h-4" />
<span className="text-bolt-elements-textSecondary">
{webAppInfo.gitInfo.github.currentRepo.stars}
</span>
</div>
<div className="flex items-center gap-1">
<div className="i-ph:git-fork text-blue-500 w-4 h-4" />
<span className="text-bolt-elements-textSecondary">
{webAppInfo.gitInfo.github.currentRepo.forks}
</span>
</div>
<div className="flex items-center gap-1">
<div className="i-ph:warning-circle text-red-500 w-4 h-4" />
<span className="text-bolt-elements-textSecondary">
{webAppInfo.gitInfo.github.currentRepo.openIssues}
</span>
</div>
</div>
</div>
{webAppInfo.gitInfo.github.upstream && (
<div className="mt-2">
<div className="text-sm flex items-center gap-2">
<div className="i-ph:git-fork text-bolt-elements-textSecondary w-4 h-4" />
<span className="text-bolt-elements-textSecondary">Upstream:</span>
<span className="text-bolt-elements-textPrimary">
{webAppInfo.gitInfo.github.upstream.fullName}
</span>
</div>
<div className="mt-2 flex items-center gap-4 text-sm">
<div className="flex items-center gap-1">
<div className="i-ph:star text-yellow-500 w-4 h-4" />
<span className="text-bolt-elements-textSecondary">
{webAppInfo.gitInfo.github.upstream.stars}
</span>
</div>
<div className="flex items-center gap-1">
<div className="i-ph:git-fork text-blue-500 w-4 h-4" />
<span className="text-bolt-elements-textSecondary">
{webAppInfo.gitInfo.github.upstream.forks}
</span>
</div>
</div>
</div>
)}
</>
)}
</div>
</div>
</div>
)}
{webAppInfo && (
<div className="mt-6">
<h3 className="mb-4 text-base font-medium text-bolt-elements-textPrimary">Dependencies</h3>
<div className="bg-white dark:bg-[#0A0A0A] border border-[#E5E5E5] dark:border-[#1A1A1A] rounded-lg divide-y divide-[#E5E5E5] dark:divide-[#1A1A1A]">
<DependencySection title="Production" deps={webAppInfo.dependencies.production} />
<DependencySection title="Development" deps={webAppInfo.dependencies.development} />
<DependencySection title="Peer" deps={webAppInfo.dependencies.peer} />
<DependencySection title="Optional" deps={webAppInfo.dependencies.optional} />
</div>
</div>
)}
</div>
</CollapsibleContent>
</Collapsible>
{/* Error Check */}
<Collapsible
open={openSections.errors}
onOpenChange={(open) => setOpenSections((prev) => ({ ...prev, errors: open }))}
className="w-full"
>
<CollapsibleTrigger className="w-full">
<div className="flex items-center justify-between p-6 rounded-xl bg-white dark:bg-[#0A0A0A] border border-[#E5E5E5] dark:border-[#1A1A1A]">
<div className="flex items-center gap-3">
<div className="i-ph:warning text-red-500 w-5 h-5" />
<h3 className="text-base font-medium text-bolt-elements-textPrimary">Error Check</h3>
{errorLogs.length > 0 && (
<Badge variant="destructive" className="ml-2">
{errorLogs.length} Errors
</Badge>
)}
</div>
<div
className={classNames(
'i-ph:caret-down w-4 h-4 transform transition-transform duration-200',
openSections.errors ? 'rotate-180' : '',
)}
/>
</div>
</CollapsibleTrigger>
<CollapsibleContent>
<div className="p-6 mt-2 rounded-xl bg-white dark:bg-[#0A0A0A] border border-[#E5E5E5] dark:border-[#1A1A1A]">
<ScrollArea className="h-[300px]">
<div className="space-y-4">
<div className="text-sm text-bolt-elements-textSecondary">
Checks for:
<ul className="list-disc list-inside mt-2 space-y-1">
<li>Unhandled JavaScript errors</li>
<li>Unhandled Promise rejections</li>
<li>Runtime exceptions</li>
<li>Network errors</li>
</ul>
</div>
<div className="text-sm">
<span className="text-bolt-elements-textSecondary">Status: </span>
<span className="text-bolt-elements-textPrimary">
{loading.errors
? 'Checking...'
: errorLogs.length > 0
? `${errorLogs.length} errors found`
: 'No errors found'}
</span>
</div>
{errorLogs.length > 0 && (
<div className="mt-4">
<div className="text-sm font-medium text-bolt-elements-textPrimary mb-2">Recent Errors:</div>
<div className="space-y-2">
{errorLogs.map((error) => (
<div key={error.id} className="text-sm text-red-500 dark:text-red-400 p-2 rounded bg-red-500/5">
<div className="font-medium">{error.message}</div>
{error.source && (
<div className="text-xs mt-1 text-red-400">
Source: {error.source}
{error.details?.lineNumber && `:${error.details.lineNumber}`}
</div>
)}
{error.stack && (
<div className="text-xs mt-1 text-red-400 font-mono whitespace-pre-wrap">{error.stack}</div>
)}
</div>
))}
</div>
</div>
)}
</div>
</ScrollArea>
</div>
</CollapsibleContent>
</Collapsible>
</div>
);
}