diff --git a/app/components/settings/SettingsWindow.tsx b/app/components/settings/SettingsWindow.tsx deleted file mode 100644 index 6d19c99f..00000000 --- a/app/components/settings/SettingsWindow.tsx +++ /dev/null @@ -1,300 +0,0 @@ -import * as RadixDialog from '@radix-ui/react-dialog'; -import { motion, AnimatePresence } from 'framer-motion'; -import { useState } from 'react'; -import { classNames } from '~/utils/classNames'; -import { DialogTitle } from '~/components/ui/Dialog'; -import type { SettingCategory, TabType } from './settings.types'; -import { categoryLabels, categoryIcons } from './settings.types'; -import ProfileTab from './profile/ProfileTab'; -import ProvidersTab from './providers/ProvidersTab'; -import { useSettings } from '~/lib/hooks/useSettings'; -import FeaturesTab from './features/FeaturesTab'; -import DebugTab from './debug/DebugTab'; -import EventLogsTab from './event-logs/EventLogsTab'; -import ConnectionsTab from './connections/ConnectionsTab'; -import DataTab from './data/DataTab'; - -interface SettingsProps { - open: boolean; - onClose: () => void; -} - -export const SettingsWindow = ({ open, onClose }: SettingsProps) => { - const { debug, eventLogs } = useSettings(); - const [searchQuery, setSearchQuery] = useState(''); - const [activeTab, setActiveTab] = useState(null); - - const settingItems = [ - { - id: 'profile' as const, - label: 'Profile Settings', - icon: 'i-ph:user-circle', - category: 'profile' as const, - description: 'Manage your personal information and preferences', - component: () => , - keywords: ['profile', 'account', 'avatar', 'email', 'name', 'theme', 'notifications'], - }, - - { - id: 'data' as const, - label: 'Data Management', - icon: 'i-ph:database', - category: 'file_sharing' as const, - description: 'Manage your chat history and application data', - component: () => , - keywords: ['data', 'export', 'import', 'backup', 'delete'], - }, - - { - id: 'providers' as const, - label: 'Providers', - icon: 'i-ph:key', - category: 'file_sharing' as const, - description: 'Configure AI providers and API keys', - component: () => , - keywords: ['api', 'keys', 'providers', 'configuration'], - }, - - { - id: 'connection' as const, - label: 'Connection', - icon: 'i-ph:link', - category: 'connectivity' as const, - description: 'Manage network and connection settings', - component: () => , - keywords: ['network', 'connection', 'proxy', 'ssl'], - }, - - { - id: 'features' as const, - label: 'Features', - icon: 'i-ph:star', - category: 'system' as const, - description: 'Configure application features and preferences', - component: () => , - keywords: ['features', 'settings', 'options'], - }, - ] as const; - - const debugItems = debug - ? [ - { - id: 'debug' as const, - label: 'Debug', - icon: 'i-ph:bug', - category: 'system' as const, - description: 'Advanced debugging tools and options', - component: () => , - keywords: ['debug', 'logs', 'developer'], - }, - ] - : []; - - const eventLogItems = eventLogs - ? [ - { - id: 'event-logs' as const, - label: 'Event Logs', - icon: 'i-ph:list-bullets', - category: 'system' as const, - description: 'View system events and application logs', - component: () => , - keywords: ['logs', 'events', 'history'], - }, - ] - : []; - - const allSettingItems = [...settingItems, ...debugItems, ...eventLogItems]; - - const filteredItems = allSettingItems.filter( - (item) => - item.label.toLowerCase().includes(searchQuery.toLowerCase()) || - item.description?.toLowerCase().includes(searchQuery.toLowerCase()) || - item.keywords?.some((keyword) => keyword.toLowerCase().includes(searchQuery.toLowerCase())), - ); - - const groupedItems = filteredItems.reduce( - (acc, item) => { - if (!acc[item.category]) { - acc[item.category] = allSettingItems.filter((i) => i.category === item.category); - } - - return acc; - }, - {} as Record, - ); - - const handleBackToDashboard = () => { - setActiveTab(null); - onClose(); - }; - - const activeTabItem = allSettingItems.find((item) => item.id === activeTab); - - return ( - - -
- - - - - - - {activeTab ? ( - -
-
- - -
|
- - {activeTabItem && ( -
-
-
-

- {activeTabItem.label} -

-

{activeTabItem.description}

-
-
- )} -
- - -
-
- {allSettingItems.find((item) => item.id === activeTab)?.component()} -
- - ) : ( - -
-
-
- - Bolt Control Panel - -
-
-
- setSearchQuery(e.target.value)} - className={classNames( - 'w-full h-10 pl-10 pr-4 rounded-lg text-sm', - 'bg-[#F8F8F8] dark:bg-[#1A1A1A]', - 'border border-[#E5E5E5] dark:border-[#333333]', - 'text-bolt-elements-textPrimary placeholder-bolt-elements-textTertiary', - 'focus:outline-none focus:ring-1 focus:ring-purple-500 transition-all', - )} - /> -
-
-
-
- -
-
- -
-
- {(Object.keys(groupedItems) as SettingCategory[]).map((category) => ( -
-
-
-

- {categoryLabels[category]} -

-
-
- {groupedItems[category].map((item) => ( - - ))} -
-
- ))} -
-
- - )} - - - -
- - - ); -}; diff --git a/app/components/settings/data/DataTab.tsx b/app/components/settings/data/DataTab.tsx index 98835070..08ec75aa 100644 --- a/app/components/settings/data/DataTab.tsx +++ b/app/components/settings/data/DataTab.tsx @@ -185,11 +185,15 @@ export default function DataTab() { localStorage.removeItem('bolt_settings'); localStorage.removeItem('bolt_chat_history'); - // Reload the page to apply reset + // Close the dialog first + setShowResetInlineConfirm(false); + + // Then reload and show success message window.location.reload(); toast.success('Settings reset successfully'); } catch (error) { console.error('Reset error:', error); + setShowResetInlineConfirm(false); toast.error('Failed to reset settings'); } finally { setIsResetting(false); @@ -202,9 +206,15 @@ export default function DataTab() { try { // Clear chat history localStorage.removeItem('bolt_chat_history'); + + // Close the dialog first + setShowDeleteInlineConfirm(false); + + // Then show the success message toast.success('Chat history deleted successfully'); } catch (error) { console.error('Delete error:', error); + setShowDeleteInlineConfirm(false); toast.error('Failed to delete chat history'); } finally { setIsDeleting(false); @@ -216,7 +226,7 @@ export default function DataTab() { {/* Reset Settings Dialog */} - +
@@ -252,7 +262,7 @@ export default function DataTab() { {/* Delete Confirmation Dialog */} - +
diff --git a/app/components/settings/debug/DebugTab.tsx b/app/components/settings/debug/DebugTab.tsx index e8e74e60..330e2cb4 100644 --- a/app/components/settings/debug/DebugTab.tsx +++ b/app/components/settings/debug/DebugTab.tsx @@ -1,727 +1,869 @@ -import React, { useCallback, useEffect, useState } from 'react'; -import { useSettings } from '~/lib/hooks/useSettings'; +import React, { useEffect, useState } from 'react'; import { toast } from 'react-toastify'; -import { providerBaseUrlEnvKeys } from '~/utils/constants'; -import { motion } from 'framer-motion'; import { classNames } from '~/utils/classNames'; -import { settingsStyles } from '~/components/settings/settings.styles'; +import { logStore } from '~/lib/stores/logs'; interface ProviderStatus { + id: string; name: string; - enabled: boolean; - isLocal: boolean; - isRunning: boolean | null; + status: 'online' | 'offline' | 'error'; 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; + arch: string; + platform: string; + cpus: string; + memory: { + total: string; + free: string; + used: string; + percentage: number; }; -} - -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]; + node: string; + browser: { + name: string; + version: string; + language: string; + userAgent: string; + cookiesEnabled: boolean; + online: boolean; + platform: string; + cores: number; }; - - 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}`; + screen: { + width: number; + height: number; + colorDepth: number; + pixelRatio: number; }; - - 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'; + time: { + timezone: string; + offset: number; + locale: string; }; - - 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 => { - 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, + performance: { + memory: { + jsHeapSizeLimit: number; + totalJSHeapSize: number; + usedJSHeapSize: number; + usagePercentage: number; }; - } - - 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, + timing: { + loadTime: number; + domReadyTime: number; + readyStart: number; + redirectTime: number; + appcacheTime: number; + unloadEventTime: number; + lookupDomainTime: number; + connectTime: number; + requestTime: number; + initDomTreeTime: number; + loadEventTime: number; }; - } 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, + 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; + }; +} export default function DebugTab() { - const { providers, isLatestBranch } = useSettings(); - const [activeProviders, setActiveProviders] = useState([]); - const [updateMessage, setUpdateMessage] = useState(''); - const [systemInfo] = useState(getSystemInfo()); - const [isCheckingUpdate, setIsCheckingUpdate] = useState(false); + const [providerStatuses, setProviderStatuses] = useState([]); + const [systemInfo, setSystemInfo] = useState(null); + const [loading, setLoading] = useState({ + systemInfo: false, + providers: false, + performance: false, + errors: false, + }); + const [errorLog, setErrorLog] = useState<{ + errors: any[]; + lastCheck: string | null; + }>({ + errors: [], + lastCheck: null, + }); - const updateProviderStatuses = async () => { - if (!providers) { - return; - } + // Fetch initial data + useEffect(() => { + checkProviderStatus(); + getSystemInfo(); + }, []); + // Set up error listeners when component mounts + useEffect(() => { + const errors: any[] = []; + + const handleError = (event: ErrorEvent) => { + errors.push({ + type: 'error', + message: event.message, + filename: event.filename, + lineNumber: event.lineno, + columnNumber: event.colno, + error: event.error, + timestamp: new Date().toISOString(), + }); + }; + + const handleRejection = (event: PromiseRejectionEvent) => { + errors.push({ + type: 'unhandledRejection', + reason: event.reason, + timestamp: new Date().toISOString(), + }); + }; + + window.addEventListener('error', handleError); + window.addEventListener('unhandledrejection', handleRejection); + + return () => { + window.removeEventListener('error', handleError); + window.removeEventListener('unhandledrejection', handleRejection); + }; + }, []); + + const checkProviderStatus = async () => { 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`; + setLoading((prev) => ({ ...prev, providers: true })); - // Access environment variables through import.meta.env - let settingsUrl = provider.settings.baseUrl; + // Fetch real provider statuses + const providers: ProviderStatus[] = []; - if (settingsUrl && settingsUrl.trim().length === 0) { - settingsUrl = undefined; - } + // Check OpenAI status + try { + const openaiResponse = await fetch('/api/providers/openai/status'); + providers.push({ + id: 'openai', + name: 'OpenAI', + status: openaiResponse.ok ? 'online' : 'error', + error: !openaiResponse.ok ? 'API Error' : undefined, + }); + } catch { + providers.push({ id: 'openai', name: 'OpenAI', status: 'offline' }); + } - const url = settingsUrl || import.meta.env[envVarName] || null; // Ensure baseUrl is used - console.log(`[Debug] Using URL for ${provider.name}:`, url, `(from ${envVarName})`); + // Check Anthropic status + try { + const anthropicResponse = await fetch('/api/providers/anthropic/status'); + providers.push({ + id: 'anthropic', + name: 'Anthropic', + status: anthropicResponse.ok ? 'online' : 'error', + error: !anthropicResponse.ok ? 'API Error' : undefined, + }); + } catch { + providers.push({ id: 'anthropic', name: 'Anthropic', status: 'offline' }); + } - const status = await checkProviderStatus(url, provider.name); + // Check Local Models status + try { + const localResponse = await fetch('/api/providers/local/status'); + providers.push({ + id: 'local', + name: 'Local Models', + status: localResponse.ok ? 'online' : 'error', + error: !localResponse.ok ? 'API Error' : undefined, + }); + } catch { + providers.push({ id: 'local', name: 'Local Models', status: 'offline' }); + } - return { - ...status, - enabled: provider.settings.enabled ?? false, - }; - }), - ); + // Check Ollama status + try { + const ollamaResponse = await fetch('/api/providers/ollama/status'); + providers.push({ + id: 'ollama', + name: 'Ollama', + status: ollamaResponse.ok ? 'online' : 'error', + error: !ollamaResponse.ok ? 'API Error' : undefined, + }); + } catch { + providers.push({ id: 'ollama', name: 'Ollama', status: 'offline' }); + } - setActiveProviders(statuses); + setProviderStatuses(providers); + toast.success('Provider status updated'); } catch (error) { - console.error('[Debug] Failed to update provider statuses:', error); + toast.error('Failed to check provider status'); + console.error('Failed to check provider status:', error); + } finally { + setLoading((prev) => ({ ...prev, providers: false })); } }; - useEffect(() => { - updateProviderStatuses(); + const getSystemInfo = async () => { + try { + setLoading((prev) => ({ ...prev, systemInfo: true })); - const interval = setInterval(updateProviderStatuses, 30000); + // Get browser info + const ua = navigator.userAgent; + const browserName = ua.includes('Firefox') + ? 'Firefox' + : ua.includes('Chrome') + ? 'Chrome' + : ua.includes('Safari') + ? 'Safari' + : ua.includes('Edge') + ? 'Edge' + : 'Unknown'; + const browserVersion = ua.match(/(Firefox|Chrome|Safari|Edge)\/([0-9.]+)/)?.[2] || 'Unknown'; - return () => clearInterval(interval); - }, [providers]); + // Get performance metrics + const memory = (performance as any).memory || {}; + const timing = performance.timing; + const navigation = performance.navigation; + const connection = (navigator as any).connection; - const handleCheckForUpdate = useCallback(async () => { - if (isCheckingUpdate) { + // 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: navigator.platform, + arch: navigator.userAgent.includes('x64') ? 'x64' : navigator.userAgent.includes('arm') ? 'arm' : 'unknown', + platform: navigator.platform, + 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, + 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: timing.loadEventEnd - timing.navigationStart, + domReadyTime: timing.domContentLoadedEventEnd - timing.navigationStart, + 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 + const formatBytes = (bytes: number) => { + const units = ['B', 'KB', 'MB', 'GB']; + let size = bytes; + let unitIndex = 0; + + while (size >= 1024 && unitIndex < units.length - 1) { + size /= 1024; + unitIndex++; + } + + return `${Math.round(size)} ${units[unitIndex]}`; + }; + + const handleLogSystemInfo = () => { + if (!systemInfo) { 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!'); + logStore.logSystem('System Information', { + os: systemInfo.os, + arch: systemInfo.arch, + cpus: systemInfo.cpus, + memory: systemInfo.memory, + node: systemInfo.node, }); - }, [activeProviders, systemInfo, isLatestBranch]); + toast.success('System information logged'); + }; + + 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 handleCheckErrors = () => { + try { + setLoading((prev) => ({ ...prev, errors: true })); + + // Get any errors from the performance entries + const resourceErrors = performance + .getEntriesByType('resource') + .filter((entry) => { + const failedEntry = entry as PerformanceResourceTiming; + return failedEntry.responseEnd - failedEntry.startTime === 0; + }) + .map((entry) => ({ + type: 'networkError', + resource: entry.name, + timestamp: new Date().toISOString(), + })); + + // Combine collected errors with resource errors + const allErrors = [...errorLog.errors, ...resourceErrors]; + + if (allErrors.length > 0) { + logStore.logError('JavaScript Errors Found', { + errors: allErrors, + timestamp: new Date().toISOString(), + }); + toast.error(`Found ${allErrors.length} error(s)`); + } else { + toast.success('No errors found'); + } + + // Update error log + setErrorLog({ + errors: allErrors, + lastCheck: new Date().toISOString(), + }); + } catch (error) { + toast.error('Failed to check for errors'); + console.error('Failed to check for errors:', error); + } finally { + setLoading((prev) => ({ ...prev, errors: false })); + } + }; return (
-
-
-
-

Debug Information

+ {/* System Information */} +
+
+
+
+

System Information

+
+
+ + +
-
- -
- Copy Debug Info - - - {isCheckingUpdate ? ( - <> -
- Checking... - - ) : ( - <> -
- Check for Updates - + {systemInfo ? ( +
+
+
+
+ OS: + {systemInfo.os} +
+
+
+ Platform: + {systemInfo.platform} +
+
+
+ Architecture: + {systemInfo.arch} +
+
+
+ CPU Cores: + {systemInfo.cpus} +
+
+
+ Node Version: + {systemInfo.node} +
+
+
+ Network Type: + + {systemInfo.network.type} ({systemInfo.network.effectiveType}) + +
+
+
+ Network Speed: + + {systemInfo.network.downlink}Mbps (RTT: {systemInfo.network.rtt}ms) + +
+ {systemInfo.battery && ( +
+
+ Battery: + + {systemInfo.battery.level.toFixed(1)}% {systemInfo.battery.charging ? '(Charging)' : ''} + +
+ )} +
+
+ Storage: + + {(systemInfo.storage.usage / (1024 * 1024 * 1024)).toFixed(2)}GB /{' '} + {(systemInfo.storage.quota / (1024 * 1024 * 1024)).toFixed(2)}GB + +
+
+
+
+
+ Memory Usage: + + {systemInfo.memory.used} / {systemInfo.memory.total} ({systemInfo.memory.percentage}%) + +
+
+
+ Browser: + + {systemInfo.browser.name} {systemInfo.browser.version} + +
+
+
+ Screen: + + {systemInfo.screen.width}x{systemInfo.screen.height} ({systemInfo.screen.pixelRatio}x) + +
+
+
+ Timezone: + {systemInfo.time.timezone} +
+
+
+ Language: + {systemInfo.browser.language} +
+
+
+ JS Heap: + + {(systemInfo.performance.memory.usedJSHeapSize / (1024 * 1024)).toFixed(1)}MB /{' '} + {(systemInfo.performance.memory.totalJSHeapSize / (1024 * 1024)).toFixed(1)}MB ( + {systemInfo.performance.memory.usagePercentage.toFixed(1)}%) + +
+
+
+ Page Load: + + {(systemInfo.performance.timing.loadTime / 1000).toFixed(2)}s + +
+
+
+ DOM Ready: + + {(systemInfo.performance.timing.domReadyTime / 1000).toFixed(2)}s + +
+
+
+ ) : ( +
Loading system information...
+ )} +
+ + {/* Provider Status */} +
+
+
+
+

Provider Status

+
+ +
+
+ {providerStatuses.map((provider) => ( +
+
+
+ {provider.name} +
+ {provider.status} +
+ ))}
- {updateMessage && ( - -
-
-
-

{updateMessage}

- {updateMessage.includes('Update available') && ( -
-

To update:

-
    -
  1. -
    -
    - Pull the latest changes:{' '} - - git pull upstream main - -
    -
  2. -
  3. -
    -
    - Install any new dependencies:{' '} - - pnpm install - -
    -
  4. -
  5. -
    -
    - Restart the application -
    -
  6. -
-
- )} -
+ {/* Performance Metrics */} +
+
+
+
+

Performance Metrics

- - )} - -
- -
-
-

System Information

-
- -
-
-
-
-

Operating System

-
-

{systemInfo.os}

-
-
-
-
-

Device Type

-
-

{systemInfo.deviceType}

-
-
-
-
-

Browser

-
-

{systemInfo.browser}

-
-
-
-
-

Display

-
-

- {systemInfo.screen} ({systemInfo.colorDepth}) @{systemInfo.pixelRatio}x -

-
-
-
-
-

Connection

-
-
- - - {systemInfo.online ? 'Online' : 'Offline'} - -
-
-
-
-
-

Language

-
-

{systemInfo.language}

-
-
-
-
-

Timezone

-
-

{systemInfo.timezone}

-
-
-
-
-

CPU Cores

-
-

{systemInfo.cores}

-
-
-
-
-
-

Version

-
-

- {connitJson.commit.slice(0, 7)} - - (v{versionTag || '0.0.1'}) - {isLatestBranch ? 'nightly' : 'stable'} + +

+ {systemInfo && ( +
+
+
+ Page Load Time: + + {(systemInfo.performance.timing.loadTime / 1000).toFixed(2)}s -

+
+
+ DOM Ready Time: + + {(systemInfo.performance.timing.domReadyTime / 1000).toFixed(2)}s + +
+
+ Request Time: + + {(systemInfo.performance.timing.requestTime / 1000).toFixed(2)}s + +
+
+ Redirect Time: + + {(systemInfo.performance.timing.redirectTime / 1000).toFixed(2)}s + +
+
+
+
+ JS Heap Usage: + + {(systemInfo.performance.memory.usedJSHeapSize / (1024 * 1024)).toFixed(1)}MB /{' '} + {(systemInfo.performance.memory.totalJSHeapSize / (1024 * 1024)).toFixed(1)}MB + +
+
+ Heap Utilization: + + {systemInfo.performance.memory.usagePercentage.toFixed(1)}% + +
+
+ Navigation Type: + + {systemInfo.performance.navigation.type === 0 + ? 'Navigate' + : systemInfo.performance.navigation.type === 1 + ? 'Reload' + : systemInfo.performance.navigation.type === 2 + ? 'Back/Forward' + : 'Other'} + +
+
+ Redirects: + + {systemInfo.performance.navigation.redirectCount} + +
- - - - -
-
-

Local LLM Status

- -
- {activeProviders.map((provider) => ( -
-
-
-
-
-
-
-

{provider.name}

- {provider.url && ( -

- {provider.url} -

- )} -
-
-
- - {provider.enabled ? 'Enabled' : 'Disabled'} - - {provider.enabled && ( - - {provider.isRunning ? 'Running' : 'Not Running'} - - )} -
+ )} +
+ + {/* Error Check */} +
+
+
+
+

Error Check

+
+ +
+
+
+ Checks for: +
    +
  • Unhandled JavaScript errors
  • +
  • Unhandled Promise rejections
  • +
  • Runtime exceptions
  • +
  • Network errors
  • +
+
+
+ Last Check: + + {loading.errors + ? 'Checking...' + : errorLog.lastCheck + ? `Last checked ${new Date(errorLog.lastCheck).toLocaleString()} (${errorLog.errors.length} errors found)` + : 'Click to check for errors'} + +
+ {errorLog.errors.length > 0 && ( +
+
Recent Errors:
+
+ {errorLog.errors.slice(0, 3).map((error, index) => ( +
+ {error.type === 'error' && `${error.message} (${error.filename}:${error.lineNumber})`} + {error.type === 'unhandledRejection' && `Unhandled Promise Rejection: ${error.reason}`} + {error.type === 'networkError' && `Network Error: Failed to load ${error.resource}`}
- -
-
- - Last checked: {new Date(provider.lastChecked).toLocaleTimeString()} - - {provider.responseTime && ( - - Response time: {Math.round(provider.responseTime)}ms - - )} -
- - {provider.error && ( -
- Error: {provider.error} -
- )} - - {provider.url && ( -
- Endpoints checked: -
    -
  • {provider.url} (root)
  • -
  • {provider.url}/api/health
  • -
  • {provider.url}/v1/models
  • -
-
- )} + ))} + {errorLog.errors.length > 3 && ( +
+ And {errorLog.errors.length - 3} more errors...
-
- ))} - {activeProviders.length === 0 && ( -
No local LLMs configured
- )} + )} +
- - -
+ )} +
+
); } diff --git a/app/components/settings/developer/DeveloperWindow.tsx b/app/components/settings/developer/DeveloperWindow.tsx new file mode 100644 index 00000000..d0467bbf --- /dev/null +++ b/app/components/settings/developer/DeveloperWindow.tsx @@ -0,0 +1,378 @@ +import * as RadixDialog from '@radix-ui/react-dialog'; +import { motion } from 'framer-motion'; +import { useState } from 'react'; +import { classNames } from '~/utils/classNames'; +import { TabManagement } from './TabManagement'; +import { TabTile } from '~/components/settings/shared/TabTile'; +import type { TabType, TabVisibilityConfig } from '~/components/settings/settings.types'; +import { tabConfigurationStore, updateTabConfiguration } from '~/lib/stores/settings'; +import { useStore } from '@nanostores/react'; +import { DndProvider, useDrag, useDrop } from 'react-dnd'; +import { HTML5Backend } from 'react-dnd-html5-backend'; +import DebugTab from '~/components/settings/debug/DebugTab'; +import { EventLogsTab } from '~/components/settings/event-logs/EventLogsTab'; +import UpdateTab from '~/components/settings/update/UpdateTab'; +import { ProvidersTab } from '~/components/settings/providers/ProvidersTab'; +import DataTab from '~/components/settings/data/DataTab'; +import FeaturesTab from '~/components/settings/features/FeaturesTab'; +import NotificationsTab from '~/components/settings/notifications/NotificationsTab'; +import SettingsTab from '~/components/settings/settings/SettingsTab'; +import ProfileTab from '~/components/settings/profile/ProfileTab'; +import ConnectionsTab from '~/components/settings/connections/ConnectionsTab'; +import { useUpdateCheck, useFeatures, useNotifications, useConnectionStatus, useDebugStatus } from '~/lib/hooks'; + +interface DraggableTabTileProps { + tab: TabVisibilityConfig; + index: number; + moveTab: (dragIndex: number, hoverIndex: number) => void; + onClick: () => void; + isActive: boolean; + hasUpdate: boolean; + statusMessage: string; + description: string; + isLoading?: boolean; +} + +const TAB_DESCRIPTIONS: Record = { + profile: 'Manage your profile and account settings', + settings: 'Configure application preferences', + notifications: 'View and manage your notifications', + features: 'Explore new and upcoming features', + data: 'Manage your data and storage', + providers: 'Configure AI providers and models', + connection: 'Check connection status and settings', + debug: 'Debug tools and system information', + 'event-logs': 'View system events and logs', + update: 'Check for updates and release notes', +}; + +const DraggableTabTile = ({ + tab, + index, + moveTab, + onClick, + isActive, + hasUpdate, + statusMessage, + description, + isLoading, +}: DraggableTabTileProps) => { + const [{ isDragging }, drag] = useDrag({ + type: 'tab', + item: { index }, + collect: (monitor) => ({ + isDragging: monitor.isDragging(), + }), + }); + + const [, drop] = useDrop({ + accept: 'tab', + hover: (item: { index: number }) => { + if (item.index === index) { + return; + } + + moveTab(item.index, index); + item.index = index; + }, + }); + + return ( +
drag(drop(node))} style={{ opacity: isDragging ? 0.5 : 1 }}> + +
+ ); +}; + +interface DeveloperWindowProps { + open: boolean; + onClose: () => void; +} + +export const DeveloperWindow = ({ open, onClose }: DeveloperWindowProps) => { + const tabConfiguration = useStore(tabConfigurationStore); + const [activeTab, setActiveTab] = useState(null); + const [showTabManagement, setShowTabManagement] = useState(false); + const [loadingTab, setLoadingTab] = useState(null); + + // Status hooks + const { hasUpdate, currentVersion, acknowledgeUpdate } = useUpdateCheck(); + const { hasNewFeatures, unviewedFeatures, acknowledgeAllFeatures } = useFeatures(); + const { hasUnreadNotifications, unreadNotifications, markAllAsRead } = useNotifications(); + const { hasConnectionIssues, currentIssue, acknowledgeIssue } = useConnectionStatus(); + const { hasActiveWarnings, activeIssues, acknowledgeAllIssues } = useDebugStatus(); + + const handleBack = () => { + if (showTabManagement) { + setShowTabManagement(false); + } else if (activeTab) { + setActiveTab(null); + } + }; + + // Only show tabs that are assigned to the developer window AND are visible + const visibleDeveloperTabs = tabConfiguration.developerTabs + .filter((tab: TabVisibilityConfig) => tab.window === 'developer' && tab.visible) + .sort((a: TabVisibilityConfig, b: TabVisibilityConfig) => (a.order || 0) - (b.order || 0)); + + const moveTab = (dragIndex: number, hoverIndex: number) => { + const draggedTab = visibleDeveloperTabs[dragIndex]; + const targetTab = visibleDeveloperTabs[hoverIndex]; + + // Update the order of the dragged and target tabs + const updatedDraggedTab = { ...draggedTab, order: targetTab.order }; + const updatedTargetTab = { ...targetTab, order: draggedTab.order }; + + // Update both tabs in the store + updateTabConfiguration(updatedDraggedTab); + updateTabConfiguration(updatedTargetTab); + }; + + const handleTabClick = async (tabId: TabType) => { + setLoadingTab(tabId); + setActiveTab(tabId); + + // Acknowledge the status based on tab type + switch (tabId) { + case 'update': + await acknowledgeUpdate(); + break; + case 'features': + await acknowledgeAllFeatures(); + break; + case 'notifications': + await markAllAsRead(); + break; + case 'connection': + acknowledgeIssue(); + break; + case 'debug': + await acknowledgeAllIssues(); + break; + } + + // Simulate loading time (remove this in production) + await new Promise((resolve) => setTimeout(resolve, 1000)); + setLoadingTab(null); + }; + + const getTabComponent = () => { + switch (activeTab) { + case 'profile': + return ; + case 'settings': + return ; + case 'notifications': + return ; + case 'features': + return ; + case 'data': + return ; + case 'providers': + return ; + case 'connection': + return ; + case 'debug': + return ; + case 'event-logs': + return ; + case 'update': + return ; + default: + return null; + } + }; + + const getTabUpdateStatus = (tabId: TabType): boolean => { + switch (tabId) { + case 'update': + return hasUpdate; + case 'features': + return hasNewFeatures; + case 'notifications': + return hasUnreadNotifications; + case 'connection': + return hasConnectionIssues; + case 'debug': + return hasActiveWarnings; + default: + return false; + } + }; + + const getStatusMessage = (tabId: TabType): string => { + switch (tabId) { + case 'update': + return `New update available (v${currentVersion})`; + case 'features': + return `${unviewedFeatures.length} new feature${unviewedFeatures.length === 1 ? '' : 's'} to explore`; + case 'notifications': + return `${unreadNotifications.length} unread notification${unreadNotifications.length === 1 ? '' : 's'}`; + case 'connection': + return currentIssue === 'disconnected' + ? 'Connection lost' + : currentIssue === 'high-latency' + ? 'High latency detected' + : 'Connection issues detected'; + case 'debug': { + const warnings = activeIssues.filter((i) => i.type === 'warning').length; + const errors = activeIssues.filter((i) => i.type === 'error').length; + + return `${warnings} warning${warnings === 1 ? '' : 's'}, ${errors} error${errors === 1 ? '' : 's'}`; + } + default: + return ''; + } + }; + + return ( + + + +
+ + + + + + {/* Header */} +
+
+ {(activeTab || showTabManagement) && ( + +
+ + )} +
+ +

+ {showTabManagement ? 'Tab Management' : activeTab ? 'Developer Tools' : 'Developer Dashboard'} +

+
+
+
+ {!showTabManagement && !activeTab && ( + setShowTabManagement(true)} + className={classNames( + 'px-3 py-1.5 rounded-lg text-sm', + 'bg-purple-500/10 text-purple-500', + 'hover:bg-purple-500/20', + 'transition-colors duration-200', + )} + whileHover={{ scale: 1.02 }} + whileTap={{ scale: 0.98 }} + > + Manage Tabs + + )} + +
+ +
+
+ + {/* Content */} +
+ + {showTabManagement ? ( + + ) : activeTab ? ( + getTabComponent() + ) : ( +
+ {visibleDeveloperTabs.map((tab: TabVisibilityConfig, index: number) => ( + handleTabClick(tab.id)} + isActive={activeTab === tab.id} + hasUpdate={getTabUpdateStatus(tab.id)} + statusMessage={getStatusMessage(tab.id)} + description={TAB_DESCRIPTIONS[tab.id]} + isLoading={loadingTab === tab.id} + /> + ))} +
+ )} +
+
+ + +
+ + + + ); +}; diff --git a/app/components/settings/developer/TabManagement.tsx b/app/components/settings/developer/TabManagement.tsx new file mode 100644 index 00000000..3ec23a4c --- /dev/null +++ b/app/components/settings/developer/TabManagement.tsx @@ -0,0 +1,315 @@ +import { motion } from 'framer-motion'; +import { useState } from 'react'; +import { classNames } from '~/utils/classNames'; +import { tabConfigurationStore, updateTabConfiguration, resetTabConfiguration } from '~/lib/stores/settings'; +import { useStore } from '@nanostores/react'; +import { TAB_LABELS, type TabType, type TabVisibilityConfig } from '~/components/settings/settings.types'; +import { toast } from 'react-toastify'; + +// Define icons for each tab type +const TAB_ICONS: Record = { + profile: 'i-ph:user-circle-fill', + settings: 'i-ph:gear-six-fill', + notifications: 'i-ph:bell-fill', + features: 'i-ph:sparkle-fill', + data: 'i-ph:database-fill', + providers: 'i-ph:robot-fill', + connection: 'i-ph:plug-fill', + debug: 'i-ph:bug-fill', + 'event-logs': 'i-ph:list-bullets-fill', + update: 'i-ph:arrow-clockwise-fill', +}; + +interface TabGroupProps { + title: string; + description?: string; + tabs: TabVisibilityConfig[]; + onVisibilityChange: (tabId: TabType, enabled: boolean) => void; + targetWindow: 'user' | 'developer'; + standardTabs: TabType[]; +} + +const TabGroup = ({ title, description, tabs, onVisibilityChange, targetWindow }: TabGroupProps) => { + // Split tabs into visible and hidden + const visibleTabs = tabs.filter((tab) => tab.visible).sort((a, b) => (a.order || 0) - (b.order || 0)); + const hiddenTabs = tabs.filter((tab) => !tab.visible).sort((a, b) => (a.order || 0) - (b.order || 0)); + + return ( +
+
+

+ + {title} +

+ {description &&

{description}

} +
+ +
+ + {visibleTabs.map((tab) => ( + +
+
+ + {TAB_LABELS[tab.id]} + + {tab.id === 'profile' && targetWindow === 'user' && ( + + Standard + + )} +
+
+ {targetWindow === 'user' ? ( +