Spaces:
Running
Running
| import React, { useCallback, useEffect, useState } from 'react'; | |
| import { useSettings } from '~/lib/hooks/useSettings'; | |
| import { toast } from 'react-toastify'; | |
| import { providerBaseUrlEnvKeys } from '~/utils/constants'; | |
| interface ProviderStatus { | |
| name: string; | |
| enabled: boolean; | |
| isLocal: boolean; | |
| isRunning: boolean | null; | |
| error?: string; | |
| lastChecked: Date; | |
| responseTime?: number; | |
| url: string | null; | |
| } | |
| interface SystemInfo { | |
| os: string; | |
| browser: string; | |
| screen: string; | |
| language: string; | |
| timezone: string; | |
| memory: string; | |
| cores: number; | |
| deviceType: string; | |
| colorDepth: string; | |
| pixelRatio: number; | |
| online: boolean; | |
| cookiesEnabled: boolean; | |
| doNotTrack: boolean; | |
| } | |
| interface IProviderConfig { | |
| name: string; | |
| settings: { | |
| enabled: boolean; | |
| baseUrl?: string; | |
| }; | |
| } | |
| interface CommitData { | |
| commit: string; | |
| version?: string; | |
| } | |
| const connitJson: CommitData = { | |
| commit: __COMMIT_HASH, | |
| version: __APP_VERSION, | |
| }; | |
| const LOCAL_PROVIDERS = ['Ollama', 'LMStudio', 'OpenAILike']; | |
| const versionHash = connitJson.commit; | |
| const versionTag = connitJson.version; | |
| const GITHUB_URLS = { | |
| original: 'https://api.github.com/repos/stackblitz-labs/bolt.diy/commits/main', | |
| fork: 'https://api.github.com/repos/Stijnus/bolt.new-any-llm/commits/main', | |
| commitJson: async (branch: string) => { | |
| try { | |
| const response = await fetch(`https://api.github.com/repos/stackblitz-labs/bolt.diy/commits/${branch}`); | |
| const data: { sha: string } = await response.json(); | |
| const packageJsonResp = await fetch( | |
| `https://raw.githubusercontent.com/stackblitz-labs/bolt.diy/${branch}/package.json`, | |
| ); | |
| const packageJson: { version: string } = await packageJsonResp.json(); | |
| return { | |
| commit: data.sha.slice(0, 7), | |
| version: packageJson.version, | |
| }; | |
| } catch (error) { | |
| console.log('Failed to fetch local commit info:', error); | |
| throw new Error('Failed to fetch local commit info'); | |
| } | |
| }, | |
| }; | |
| function getSystemInfo(): SystemInfo { | |
| const formatBytes = (bytes: number): string => { | |
| if (bytes === 0) { | |
| return '0 Bytes'; | |
| } | |
| const k = 1024; | |
| const sizes = ['Bytes', 'KB', 'MB', 'GB']; | |
| const i = Math.floor(Math.log(bytes) / Math.log(k)); | |
| return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; | |
| }; | |
| const getBrowserInfo = (): string => { | |
| const ua = navigator.userAgent; | |
| let browser = 'Unknown'; | |
| if (ua.includes('Firefox/')) { | |
| browser = 'Firefox'; | |
| } else if (ua.includes('Chrome/')) { | |
| if (ua.includes('Edg/')) { | |
| browser = 'Edge'; | |
| } else if (ua.includes('OPR/')) { | |
| browser = 'Opera'; | |
| } else { | |
| browser = 'Chrome'; | |
| } | |
| } else if (ua.includes('Safari/')) { | |
| if (!ua.includes('Chrome')) { | |
| browser = 'Safari'; | |
| } | |
| } | |
| // Extract version number | |
| const match = ua.match(new RegExp(`${browser}\\/([\\d.]+)`)); | |
| const version = match ? ` ${match[1]}` : ''; | |
| return `${browser}${version}`; | |
| }; | |
| const getOperatingSystem = (): string => { | |
| const ua = navigator.userAgent; | |
| const platform = navigator.platform; | |
| if (ua.includes('Win')) { | |
| return 'Windows'; | |
| } | |
| if (ua.includes('Mac')) { | |
| if (ua.includes('iPhone') || ua.includes('iPad')) { | |
| return 'iOS'; | |
| } | |
| return 'macOS'; | |
| } | |
| if (ua.includes('Linux')) { | |
| return 'Linux'; | |
| } | |
| if (ua.includes('Android')) { | |
| return 'Android'; | |
| } | |
| return platform || 'Unknown'; | |
| }; | |
| const getDeviceType = (): string => { | |
| const ua = navigator.userAgent; | |
| if (ua.includes('Mobile')) { | |
| return 'Mobile'; | |
| } | |
| if (ua.includes('Tablet')) { | |
| return 'Tablet'; | |
| } | |
| return 'Desktop'; | |
| }; | |
| // Get more detailed memory info if available | |
| const getMemoryInfo = (): string => { | |
| if ('memory' in performance) { | |
| const memory = (performance as any).memory; | |
| return `${formatBytes(memory.jsHeapSizeLimit)} (Used: ${formatBytes(memory.usedJSHeapSize)})`; | |
| } | |
| return 'Not available'; | |
| }; | |
| return { | |
| os: getOperatingSystem(), | |
| browser: getBrowserInfo(), | |
| screen: `${window.screen.width}x${window.screen.height}`, | |
| language: navigator.language, | |
| timezone: Intl.DateTimeFormat().resolvedOptions().timeZone, | |
| memory: getMemoryInfo(), | |
| cores: navigator.hardwareConcurrency || 0, | |
| deviceType: getDeviceType(), | |
| // Add new fields | |
| colorDepth: `${window.screen.colorDepth}-bit`, | |
| pixelRatio: window.devicePixelRatio, | |
| online: navigator.onLine, | |
| cookiesEnabled: navigator.cookieEnabled, | |
| doNotTrack: navigator.doNotTrack === '1', | |
| }; | |
| } | |
| const checkProviderStatus = async (url: string | null, providerName: string): Promise<ProviderStatus> => { | |
| if (!url) { | |
| console.log(`[Debug] No URL provided for ${providerName}`); | |
| return { | |
| name: providerName, | |
| enabled: false, | |
| isLocal: true, | |
| isRunning: false, | |
| error: 'No URL configured', | |
| lastChecked: new Date(), | |
| url: null, | |
| }; | |
| } | |
| console.log(`[Debug] Checking status for ${providerName} at ${url}`); | |
| const startTime = performance.now(); | |
| try { | |
| if (providerName.toLowerCase() === 'ollama') { | |
| // Special check for Ollama root endpoint | |
| try { | |
| console.log(`[Debug] Checking Ollama root endpoint: ${url}`); | |
| const controller = new AbortController(); | |
| const timeoutId = setTimeout(() => controller.abort(), 5000); // 5 second timeout | |
| const response = await fetch(url, { | |
| signal: controller.signal, | |
| headers: { | |
| Accept: 'text/plain,application/json', | |
| }, | |
| }); | |
| clearTimeout(timeoutId); | |
| const text = await response.text(); | |
| console.log(`[Debug] Ollama root response:`, text); | |
| if (text.includes('Ollama is running')) { | |
| console.log(`[Debug] Ollama running confirmed via root endpoint`); | |
| return { | |
| name: providerName, | |
| enabled: false, | |
| isLocal: true, | |
| isRunning: true, | |
| lastChecked: new Date(), | |
| responseTime: performance.now() - startTime, | |
| url, | |
| }; | |
| } | |
| } catch (error) { | |
| console.log(`[Debug] Ollama root check failed:`, error); | |
| const errorMessage = error instanceof Error ? error.message : 'Unknown error'; | |
| if (errorMessage.includes('aborted')) { | |
| return { | |
| name: providerName, | |
| enabled: false, | |
| isLocal: true, | |
| isRunning: false, | |
| error: 'Connection timeout', | |
| lastChecked: new Date(), | |
| responseTime: performance.now() - startTime, | |
| url, | |
| }; | |
| } | |
| } | |
| } | |
| // Try different endpoints based on provider | |
| const checkUrls = [`${url}/api/health`, url.endsWith('v1') ? `${url}/models` : `${url}/v1/models`]; | |
| console.log(`[Debug] Checking additional endpoints:`, checkUrls); | |
| const results = await Promise.all( | |
| checkUrls.map(async (checkUrl) => { | |
| try { | |
| console.log(`[Debug] Trying endpoint: ${checkUrl}`); | |
| const controller = new AbortController(); | |
| const timeoutId = setTimeout(() => controller.abort(), 5000); | |
| const response = await fetch(checkUrl, { | |
| signal: controller.signal, | |
| headers: { | |
| Accept: 'application/json', | |
| }, | |
| }); | |
| clearTimeout(timeoutId); | |
| const ok = response.ok; | |
| console.log(`[Debug] Endpoint ${checkUrl} response:`, ok); | |
| if (ok) { | |
| try { | |
| const data = await response.json(); | |
| console.log(`[Debug] Endpoint ${checkUrl} data:`, data); | |
| } catch { | |
| console.log(`[Debug] Could not parse JSON from ${checkUrl}`); | |
| } | |
| } | |
| return ok; | |
| } catch (error) { | |
| console.log(`[Debug] Endpoint ${checkUrl} failed:`, error); | |
| return false; | |
| } | |
| }), | |
| ); | |
| const isRunning = results.some((result) => result); | |
| console.log(`[Debug] Final status for ${providerName}:`, isRunning); | |
| return { | |
| name: providerName, | |
| enabled: false, | |
| isLocal: true, | |
| isRunning, | |
| lastChecked: new Date(), | |
| responseTime: performance.now() - startTime, | |
| url, | |
| }; | |
| } catch (error) { | |
| console.log(`[Debug] Provider check failed for ${providerName}:`, error); | |
| return { | |
| name: providerName, | |
| enabled: false, | |
| isLocal: true, | |
| isRunning: false, | |
| error: error instanceof Error ? error.message : 'Unknown error', | |
| lastChecked: new Date(), | |
| responseTime: performance.now() - startTime, | |
| url, | |
| }; | |
| } | |
| }; | |
| export default function DebugTab() { | |
| const { providers, isLatestBranch } = useSettings(); | |
| const [activeProviders, setActiveProviders] = useState<ProviderStatus[]>([]); | |
| const [updateMessage, setUpdateMessage] = useState<string>(''); | |
| const [systemInfo] = useState<SystemInfo>(getSystemInfo()); | |
| const [isCheckingUpdate, setIsCheckingUpdate] = useState(false); | |
| const updateProviderStatuses = async () => { | |
| if (!providers) { | |
| return; | |
| } | |
| try { | |
| const entries = Object.entries(providers) as [string, IProviderConfig][]; | |
| const statuses = await Promise.all( | |
| entries | |
| .filter(([, provider]) => LOCAL_PROVIDERS.includes(provider.name)) | |
| .map(async ([, provider]) => { | |
| const envVarName = | |
| providerBaseUrlEnvKeys[provider.name].baseUrlKey || `REACT_APP_${provider.name.toUpperCase()}_URL`; | |
| // Access environment variables through import.meta.env | |
| let settingsUrl = provider.settings.baseUrl; | |
| if (settingsUrl && settingsUrl.trim().length === 0) { | |
| settingsUrl = undefined; | |
| } | |
| const url = settingsUrl || import.meta.env[envVarName] || null; // Ensure baseUrl is used | |
| console.log(`[Debug] Using URL for ${provider.name}:`, url, `(from ${envVarName})`); | |
| const status = await checkProviderStatus(url, provider.name); | |
| return { | |
| ...status, | |
| enabled: provider.settings.enabled ?? false, | |
| }; | |
| }), | |
| ); | |
| setActiveProviders(statuses); | |
| } catch (error) { | |
| console.error('[Debug] Failed to update provider statuses:', error); | |
| } | |
| }; | |
| useEffect(() => { | |
| updateProviderStatuses(); | |
| const interval = setInterval(updateProviderStatuses, 30000); | |
| return () => clearInterval(interval); | |
| }, [providers]); | |
| const handleCheckForUpdate = useCallback(async () => { | |
| if (isCheckingUpdate) { | |
| return; | |
| } | |
| try { | |
| setIsCheckingUpdate(true); | |
| setUpdateMessage('Checking for updates...'); | |
| const branchToCheck = isLatestBranch ? 'main' : 'stable'; | |
| console.log(`[Debug] Checking for updates against ${branchToCheck} branch`); | |
| const latestCommitResp = await GITHUB_URLS.commitJson(branchToCheck); | |
| const remoteCommitHash = latestCommitResp.commit; | |
| const currentCommitHash = versionHash; | |
| if (remoteCommitHash !== currentCommitHash) { | |
| setUpdateMessage( | |
| `Update available from ${branchToCheck} branch!\n` + | |
| `Current: ${currentCommitHash.slice(0, 7)}\n` + | |
| `Latest: ${remoteCommitHash.slice(0, 7)}`, | |
| ); | |
| } else { | |
| setUpdateMessage(`You are on the latest version from the ${branchToCheck} branch`); | |
| } | |
| } catch (error) { | |
| setUpdateMessage('Failed to check for updates'); | |
| console.error('[Debug] Failed to check for updates:', error); | |
| } finally { | |
| setIsCheckingUpdate(false); | |
| } | |
| }, [isCheckingUpdate, isLatestBranch]); | |
| const handleCopyToClipboard = useCallback(() => { | |
| const debugInfo = { | |
| System: systemInfo, | |
| Providers: activeProviders.map((provider) => ({ | |
| name: provider.name, | |
| enabled: provider.enabled, | |
| isLocal: provider.isLocal, | |
| running: provider.isRunning, | |
| error: provider.error, | |
| lastChecked: provider.lastChecked, | |
| responseTime: provider.responseTime, | |
| url: provider.url, | |
| })), | |
| Version: { | |
| hash: versionHash.slice(0, 7), | |
| branch: isLatestBranch ? 'main' : 'stable', | |
| }, | |
| Timestamp: new Date().toISOString(), | |
| }; | |
| navigator.clipboard.writeText(JSON.stringify(debugInfo, null, 2)).then(() => { | |
| toast.success('Debug information copied to clipboard!'); | |
| }); | |
| }, [activeProviders, systemInfo, isLatestBranch]); | |
| return ( | |
| <div className="p-4 space-y-6"> | |
| <div className="flex items-center justify-between"> | |
| <h3 className="text-lg font-medium text-bolt-elements-textPrimary">Debug Information</h3> | |
| <div className="flex gap-2"> | |
| <button | |
| onClick={handleCopyToClipboard} | |
| className="bg-bolt-elements-button-primary-background rounded-lg px-4 py-2 transition-colors duration-200 hover:bg-bolt-elements-button-primary-backgroundHover text-bolt-elements-button-primary-text" | |
| > | |
| Copy Debug Info | |
| </button> | |
| <button | |
| onClick={handleCheckForUpdate} | |
| disabled={isCheckingUpdate} | |
| className={`bg-bolt-elements-button-primary-background rounded-lg px-4 py-2 transition-colors duration-200 | |
| ${!isCheckingUpdate ? 'hover:bg-bolt-elements-button-primary-backgroundHover' : 'opacity-75 cursor-not-allowed'} | |
| text-bolt-elements-button-primary-text`} | |
| > | |
| {isCheckingUpdate ? 'Checking...' : 'Check for Updates'} | |
| </button> | |
| </div> | |
| </div> | |
| {updateMessage && ( | |
| <div | |
| className={`bg-bolt-elements-surface rounded-lg p-3 ${ | |
| updateMessage.includes('Update available') ? 'border-l-4 border-yellow-400' : '' | |
| }`} | |
| > | |
| <p className="text-bolt-elements-textSecondary whitespace-pre-line">{updateMessage}</p> | |
| {updateMessage.includes('Update available') && ( | |
| <div className="mt-3 text-sm"> | |
| <p className="font-medium text-bolt-elements-textPrimary">To update:</p> | |
| <ol className="list-decimal ml-4 mt-1 text-bolt-elements-textSecondary"> | |
| <li> | |
| Pull the latest changes:{' '} | |
| <code className="bg-bolt-elements-surface-hover px-1 rounded">git pull upstream main</code> | |
| </li> | |
| <li> | |
| Install any new dependencies:{' '} | |
| <code className="bg-bolt-elements-surface-hover px-1 rounded">pnpm install</code> | |
| </li> | |
| <li>Restart the application</li> | |
| </ol> | |
| </div> | |
| )} | |
| </div> | |
| )} | |
| <section className="space-y-4"> | |
| <div> | |
| <h4 className="text-md font-medium text-bolt-elements-textPrimary mb-2">System Information</h4> | |
| <div className="bg-bolt-elements-surface rounded-lg p-4"> | |
| <div className="grid grid-cols-2 md:grid-cols-3 gap-4"> | |
| <div> | |
| <p className="text-xs text-bolt-elements-textSecondary">Operating System</p> | |
| <p className="text-sm font-medium text-bolt-elements-textPrimary">{systemInfo.os}</p> | |
| </div> | |
| <div> | |
| <p className="text-xs text-bolt-elements-textSecondary">Device Type</p> | |
| <p className="text-sm font-medium text-bolt-elements-textPrimary">{systemInfo.deviceType}</p> | |
| </div> | |
| <div> | |
| <p className="text-xs text-bolt-elements-textSecondary">Browser</p> | |
| <p className="text-sm font-medium text-bolt-elements-textPrimary">{systemInfo.browser}</p> | |
| </div> | |
| <div> | |
| <p className="text-xs text-bolt-elements-textSecondary">Display</p> | |
| <p className="text-sm font-medium text-bolt-elements-textPrimary"> | |
| {systemInfo.screen} ({systemInfo.colorDepth}) @{systemInfo.pixelRatio}x | |
| </p> | |
| </div> | |
| <div> | |
| <p className="text-xs text-bolt-elements-textSecondary">Connection</p> | |
| <p className="text-sm font-medium flex items-center gap-2"> | |
| <span | |
| className={`inline-block w-2 h-2 rounded-full ${systemInfo.online ? 'bg-green-500' : 'bg-red-500'}`} | |
| /> | |
| <span className={`${systemInfo.online ? 'text-green-600' : 'text-red-600'}`}> | |
| {systemInfo.online ? 'Online' : 'Offline'} | |
| </span> | |
| </p> | |
| </div> | |
| <div> | |
| <p className="text-xs text-bolt-elements-textSecondary">Screen Resolution</p> | |
| <p className="text-sm font-medium text-bolt-elements-textPrimary">{systemInfo.screen}</p> | |
| </div> | |
| <div> | |
| <p className="text-xs text-bolt-elements-textSecondary">Language</p> | |
| <p className="text-sm font-medium text-bolt-elements-textPrimary">{systemInfo.language}</p> | |
| </div> | |
| <div> | |
| <p className="text-xs text-bolt-elements-textSecondary">Timezone</p> | |
| <p className="text-sm font-medium text-bolt-elements-textPrimary">{systemInfo.timezone}</p> | |
| </div> | |
| <div> | |
| <p className="text-xs text-bolt-elements-textSecondary">CPU Cores</p> | |
| <p className="text-sm font-medium text-bolt-elements-textPrimary">{systemInfo.cores}</p> | |
| </div> | |
| </div> | |
| <div className="mt-3 pt-3 border-t border-bolt-elements-surface-hover"> | |
| <p className="text-xs text-bolt-elements-textSecondary">Version</p> | |
| <p className="text-sm font-medium text-bolt-elements-textPrimary font-mono"> | |
| {connitJson.commit.slice(0, 7)} | |
| <span className="ml-2 text-xs text-bolt-elements-textSecondary"> | |
| (v{versionTag || '0.0.1'}) - {isLatestBranch ? 'nightly' : 'stable'} | |
| </span> | |
| </p> | |
| </div> | |
| </div> | |
| </div> | |
| <div> | |
| <h4 className="text-md font-medium text-bolt-elements-textPrimary mb-2">Local LLM Status</h4> | |
| <div className="bg-bolt-elements-surface rounded-lg"> | |
| <div className="grid grid-cols-1 divide-y"> | |
| {activeProviders.map((provider) => ( | |
| <div key={provider.name} className="p-3 flex flex-col space-y-2"> | |
| <div className="flex items-center justify-between"> | |
| <div className="flex items-center gap-3"> | |
| <div className="flex-shrink-0"> | |
| <div | |
| className={`w-2 h-2 rounded-full ${ | |
| !provider.enabled ? 'bg-gray-300' : provider.isRunning ? 'bg-green-400' : 'bg-red-400' | |
| }`} | |
| /> | |
| </div> | |
| <div> | |
| <p className="text-sm font-medium text-bolt-elements-textPrimary">{provider.name}</p> | |
| {provider.url && ( | |
| <p className="text-xs text-bolt-elements-textSecondary truncate max-w-[300px]"> | |
| {provider.url} | |
| </p> | |
| )} | |
| </div> | |
| </div> | |
| <div className="flex items-center gap-2"> | |
| <span | |
| className={`px-2 py-0.5 text-xs rounded-full ${ | |
| provider.enabled ? 'bg-green-100 text-green-800' : 'bg-gray-100 text-gray-800' | |
| }`} | |
| > | |
| {provider.enabled ? 'Enabled' : 'Disabled'} | |
| </span> | |
| {provider.enabled && ( | |
| <span | |
| className={`px-2 py-0.5 text-xs rounded-full ${ | |
| provider.isRunning ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800' | |
| }`} | |
| > | |
| {provider.isRunning ? 'Running' : 'Not Running'} | |
| </span> | |
| )} | |
| </div> | |
| </div> | |
| <div className="pl-5 flex flex-col space-y-1 text-xs"> | |
| {/* Status Details */} | |
| <div className="flex flex-wrap gap-2"> | |
| <span className="text-bolt-elements-textSecondary"> | |
| Last checked: {new Date(provider.lastChecked).toLocaleTimeString()} | |
| </span> | |
| {provider.responseTime && ( | |
| <span className="text-bolt-elements-textSecondary"> | |
| Response time: {Math.round(provider.responseTime)}ms | |
| </span> | |
| )} | |
| </div> | |
| {/* Error Message */} | |
| {provider.error && ( | |
| <div className="mt-1 text-red-600 bg-red-50 rounded-md p-2"> | |
| <span className="font-medium">Error:</span> {provider.error} | |
| </div> | |
| )} | |
| {/* Connection Info */} | |
| {provider.url && ( | |
| <div className="text-bolt-elements-textSecondary"> | |
| <span className="font-medium">Endpoints checked:</span> | |
| <ul className="list-disc list-inside pl-2 mt-1"> | |
| <li>{provider.url} (root)</li> | |
| <li>{provider.url}/api/health</li> | |
| <li>{provider.url}/v1/models</li> | |
| </ul> | |
| </div> | |
| )} | |
| </div> | |
| </div> | |
| ))} | |
| {activeProviders.length === 0 && ( | |
| <div className="p-4 text-center text-bolt-elements-textSecondary">No local LLMs configured</div> | |
| )} | |
| </div> | |
| </div> | |
| </div> | |
| </section> | |
| </div> | |
| ); | |
| } | |