diff --git a/.gitignore b/.gitignore index 064ad973f1..909a59e8eb 100644 --- a/.gitignore +++ b/.gitignore @@ -44,3 +44,4 @@ changelogUI.md docs/instructions/Roadmap.md .cursorrules .cursorrules +*.md diff --git a/app/components/@settings/core/AvatarDropdown.tsx b/app/components/@settings/core/AvatarDropdown.tsx new file mode 100644 index 0000000000..b9a3b76b46 --- /dev/null +++ b/app/components/@settings/core/AvatarDropdown.tsx @@ -0,0 +1,181 @@ +import * as DropdownMenu from '@radix-ui/react-dropdown-menu'; +import { motion } from 'framer-motion'; +import { useStore } from '@nanostores/react'; +import { classNames } from '~/utils/classNames'; +import { profileStore } from '~/lib/stores/profile'; +import type { TabType, Profile } from './types'; + +interface AvatarDropdownProps { + onSelectTab: (tab: TabType) => void; +} + +export const AvatarDropdown = ({ onSelectTab }: AvatarDropdownProps) => { + const profile = useStore(profileStore) as Profile; + + return ( + + + +
+ {profile?.avatar ? ( +
+ {profile?.username +
+
+ ) : ( +
+ )} +
+ + + + + +
+
+ {profile?.avatar ? ( + {profile?.username + ) : ( +
+
+
+ )} +
+
+
+ {profile?.username || 'Guest User'} +
+ {profile?.bio &&
{profile.bio}
} +
+
+ + onSelectTab('profile')} + > +
+ Edit Profile + + + onSelectTab('settings')} + > +
+ Settings + + +
+ + onSelectTab('task-manager')} + > +
+ Task Manager + + + onSelectTab('service-status')} + > +
+ Service Status + + + + + ); +}; diff --git a/app/components/@settings/core/ControlPanel.tsx b/app/components/@settings/core/ControlPanel.tsx new file mode 100644 index 0000000000..93ed3db672 --- /dev/null +++ b/app/components/@settings/core/ControlPanel.tsx @@ -0,0 +1,459 @@ +import { useState, useEffect, useMemo } from 'react'; +import { motion, AnimatePresence } from 'framer-motion'; +import { useStore } from '@nanostores/react'; +import { Switch } from '@radix-ui/react-switch'; +import * as RadixDialog from '@radix-ui/react-dialog'; +import { classNames } from '~/utils/classNames'; +import { TabManagement } from '~/components/@settings/shared/components/TabManagement'; +import { TabTile } from '~/components/@settings/shared/components/TabTile'; +import { useUpdateCheck } from '~/lib/hooks/useUpdateCheck'; +import { useFeatures } from '~/lib/hooks/useFeatures'; +import { useNotifications } from '~/lib/hooks/useNotifications'; +import { useConnectionStatus } from '~/lib/hooks/useConnectionStatus'; +import { useDebugStatus } from '~/lib/hooks/useDebugStatus'; +import { tabConfigurationStore, developerModeStore, setDeveloperMode } from '~/lib/stores/settings'; +import { profileStore } from '~/lib/stores/profile'; +import type { TabType, TabVisibilityConfig, DevTabConfig, Profile } from './types'; +import { TAB_LABELS, DEFAULT_TAB_CONFIG } from './constants'; +import { resetTabConfiguration } from '~/lib/stores/settings'; +import { DialogTitle } from '~/components/ui/Dialog'; +import { AvatarDropdown } from './AvatarDropdown'; + +// Import all tab components +import ProfileTab from '~/components/@settings/tabs/profile/ProfileTab'; +import SettingsTab from '~/components/@settings/tabs/settings/SettingsTab'; +import NotificationsTab from '~/components/@settings/tabs/notifications/NotificationsTab'; +import FeaturesTab from '~/components/@settings/tabs/features/FeaturesTab'; +import DataTab from '~/components/@settings/tabs/data/DataTab'; +import DebugTab from '~/components/@settings/tabs/debug/DebugTab'; +import { EventLogsTab } from '~/components/@settings/tabs/event-logs/EventLogsTab'; +import UpdateTab from '~/components/@settings/tabs/update/UpdateTab'; +import ConnectionsTab from '~/components/@settings/tabs/connections/ConnectionsTab'; +import CloudProvidersTab from '~/components/@settings/tabs/providers/cloud/CloudProvidersTab'; +import ServiceStatusTab from '~/components/@settings/tabs/providers/status/ServiceStatusTab'; +import LocalProvidersTab from '~/components/@settings/tabs/providers/local/LocalProvidersTab'; +import TaskManagerTab from '~/components/@settings/tabs/task-manager/TaskManagerTab'; + +interface ControlPanelProps { + open: boolean; + onClose: () => void; +} + +interface TabWithDevType extends TabVisibilityConfig { + isExtraDevTab?: 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', + 'cloud-providers': 'Configure cloud AI providers and models', + 'local-providers': 'Configure local AI providers and models', + 'service-status': 'Monitor cloud LLM service status', + 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', + 'task-manager': 'Monitor system resources and processes', + 'tab-management': 'Configure visible tabs and their order', +}; + +export const ControlPanel = ({ open, onClose }: ControlPanelProps) => { + // State + const [activeTab, setActiveTab] = useState(null); + const [loadingTab, setLoadingTab] = useState(null); + const [showTabManagement, setShowTabManagement] = useState(false); + + // Store values + const tabConfiguration = useStore(tabConfigurationStore); + const developerMode = useStore(developerModeStore); + const profile = useStore(profileStore) as Profile; + + // 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(); + + // Add visibleTabs logic using useMemo + const visibleTabs = useMemo(() => { + if (!tabConfiguration?.userTabs || !Array.isArray(tabConfiguration.userTabs)) { + console.warn('Invalid tab configuration, resetting to defaults'); + resetTabConfiguration(); + + return []; + } + + // In developer mode, show ALL tabs without restrictions + if (developerMode) { + // Combine all unique tabs from both user and developer configurations + const allTabs = new Set([ + ...DEFAULT_TAB_CONFIG.map((tab) => tab.id), + ...tabConfiguration.userTabs.map((tab) => tab.id), + ...(tabConfiguration.developerTabs || []).map((tab) => tab.id), + ]); + + // Create a complete tab list with all tabs visible + const devTabs = Array.from(allTabs).map((tabId) => { + // Try to find existing configuration for this tab + const existingTab = + tabConfiguration.developerTabs?.find((t) => t.id === tabId) || + tabConfiguration.userTabs?.find((t) => t.id === tabId) || + DEFAULT_TAB_CONFIG.find((t) => t.id === tabId); + + return { + id: tabId, + visible: true, + window: 'developer' as const, + order: existingTab?.order || DEFAULT_TAB_CONFIG.findIndex((t) => t.id === tabId), + }; + }); + + // Add Tab Management tile for developer mode + const tabManagementConfig: DevTabConfig = { + id: 'tab-management', + visible: true, + window: 'developer', + order: devTabs.length, + isExtraDevTab: true, + }; + devTabs.push(tabManagementConfig); + + return devTabs.sort((a, b) => a.order - b.order); + } + + // In user mode, only show visible user tabs + const notificationsDisabled = profile?.preferences?.notifications === false; + + return tabConfiguration.userTabs + .filter((tab) => { + if (!tab || typeof tab.id !== 'string') { + console.warn('Invalid tab entry:', tab); + return false; + } + + // Hide notifications tab if notifications are disabled in user preferences + if (tab.id === 'notifications' && notificationsDisabled) { + return false; + } + + // Only show tabs that are explicitly visible and assigned to the user window + return tab.visible && tab.window === 'user'; + }) + .sort((a, b) => a.order - b.order); + }, [tabConfiguration, developerMode, profile?.preferences?.notifications]); + + // Handlers + const handleBack = () => { + if (showTabManagement) { + setShowTabManagement(false); + } else if (activeTab) { + setActiveTab(null); + } + }; + + const handleDeveloperModeChange = (checked: boolean) => { + console.log('Developer mode changed:', checked); + setDeveloperMode(checked); + }; + + // Add effect to log developer mode changes + useEffect(() => { + console.log('Current developer mode:', developerMode); + }, [developerMode]); + + const getTabComponent = (tabId: TabType | 'tab-management') => { + if (tabId === 'tab-management') { + return ; + } + + switch (tabId) { + case 'profile': + return ; + case 'settings': + return ; + case 'notifications': + return ; + case 'features': + return ; + case 'data': + return ; + case 'cloud-providers': + return ; + case 'local-providers': + return ; + case 'connection': + return ; + case 'debug': + return ; + case 'event-logs': + return ; + case 'update': + return ; + case 'task-manager': + return ; + case 'service-status': + 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 ''; + } + }; + + const handleTabClick = (tabId: TabType) => { + setLoadingTab(tabId); + setActiveTab(tabId); + setShowTabManagement(false); + + // Acknowledge notifications based on tab + switch (tabId) { + case 'update': + acknowledgeUpdate(); + break; + case 'features': + acknowledgeAllFeatures(); + break; + case 'notifications': + markAllAsRead(); + break; + case 'connection': + acknowledgeIssue(); + break; + case 'debug': + acknowledgeAllIssues(); + break; + } + + // Clear loading state after a delay + setTimeout(() => setLoadingTab(null), 500); + }; + + return ( + + +
+ + + + + + + {/* Header */} +
+
+ {activeTab || showTabManagement ? ( + + ) : ( + +
+
+
+ + )} + + {showTabManagement ? 'Tab Management' : activeTab ? TAB_LABELS[activeTab] : 'Control Panel'} + +
+ +
+ {/* Developer Mode Controls */} +
+ {/* Mode Toggle */} +
+ + Toggle developer mode + + +
+ +
+
+
+ + {/* Avatar and Dropdown */} +
+ +
+ + {/* Close Button */} + +
+
+ + {/* Content */} +
+ + {showTabManagement ? ( + + ) : activeTab ? ( + getTabComponent(activeTab) + ) : ( + + + {(visibleTabs as TabWithDevType[]).map((tab: TabWithDevType) => ( + + handleTabClick(tab.id as TabType)} + isActive={activeTab === tab.id} + hasUpdate={getTabUpdateStatus(tab.id)} + statusMessage={getStatusMessage(tab.id)} + description={TAB_DESCRIPTIONS[tab.id]} + isLoading={loadingTab === tab.id} + className="h-full" + /> + + ))} + + + )} + +
+ + +
+ + + ); +}; diff --git a/app/components/@settings/core/constants.ts b/app/components/@settings/core/constants.ts new file mode 100644 index 0000000000..ff72a2746f --- /dev/null +++ b/app/components/@settings/core/constants.ts @@ -0,0 +1,88 @@ +import type { TabType } from './types'; + +export const TAB_ICONS: Record = { + profile: 'i-ph:user-circle-fill', + settings: 'i-ph:gear-six-fill', + notifications: 'i-ph:bell-fill', + features: 'i-ph:star-fill', + data: 'i-ph:database-fill', + 'cloud-providers': 'i-ph:cloud-fill', + 'local-providers': 'i-ph:desktop-fill', + 'service-status': 'i-ph:activity-bold', + connection: 'i-ph:wifi-high-fill', + debug: 'i-ph:bug-fill', + 'event-logs': 'i-ph:list-bullets-fill', + update: 'i-ph:arrow-clockwise-fill', + 'task-manager': 'i-ph:chart-line-fill', + 'tab-management': 'i-ph:squares-four-fill', +}; + +export const TAB_LABELS: Record = { + profile: 'Profile', + settings: 'Settings', + notifications: 'Notifications', + features: 'Features', + data: 'Data Management', + 'cloud-providers': 'Cloud Providers', + 'local-providers': 'Local Providers', + 'service-status': 'Service Status', + connection: 'Connection', + debug: 'Debug', + 'event-logs': 'Event Logs', + update: 'Updates', + 'task-manager': 'Task Manager', + 'tab-management': 'Tab Management', +}; + +export 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', + 'cloud-providers': 'Configure cloud AI providers and models', + 'local-providers': 'Configure local AI providers and models', + 'service-status': 'Monitor cloud LLM service status', + 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', + 'task-manager': 'Monitor system resources and processes', + 'tab-management': 'Configure visible tabs and their order', +}; + +export const DEFAULT_TAB_CONFIG = [ + // User Window Tabs (Always visible by default) + { id: 'features', visible: true, window: 'user' as const, order: 0 }, + { id: 'data', visible: true, window: 'user' as const, order: 1 }, + { id: 'cloud-providers', visible: true, window: 'user' as const, order: 2 }, + { id: 'local-providers', visible: true, window: 'user' as const, order: 3 }, + { id: 'connection', visible: true, window: 'user' as const, order: 4 }, + { id: 'notifications', visible: true, window: 'user' as const, order: 5 }, + { id: 'event-logs', visible: true, window: 'user' as const, order: 6 }, + + // User Window Tabs (In dropdown, initially hidden) + { id: 'profile', visible: false, window: 'user' as const, order: 7 }, + { id: 'settings', visible: false, window: 'user' as const, order: 8 }, + { id: 'task-manager', visible: false, window: 'user' as const, order: 9 }, + { id: 'service-status', visible: false, window: 'user' as const, order: 10 }, + + // User Window Tabs (Hidden, controlled by TaskManagerTab) + { id: 'debug', visible: false, window: 'user' as const, order: 11 }, + { id: 'update', visible: false, window: 'user' as const, order: 12 }, + + // Developer Window Tabs (All visible by default) + { id: 'features', visible: true, window: 'developer' as const, order: 0 }, + { id: 'data', visible: true, window: 'developer' as const, order: 1 }, + { id: 'cloud-providers', visible: true, window: 'developer' as const, order: 2 }, + { id: 'local-providers', visible: true, window: 'developer' as const, order: 3 }, + { id: 'connection', visible: true, window: 'developer' as const, order: 4 }, + { id: 'notifications', visible: true, window: 'developer' as const, order: 5 }, + { id: 'event-logs', visible: true, window: 'developer' as const, order: 6 }, + { id: 'profile', visible: true, window: 'developer' as const, order: 7 }, + { id: 'settings', visible: true, window: 'developer' as const, order: 8 }, + { id: 'task-manager', visible: true, window: 'developer' as const, order: 9 }, + { id: 'service-status', visible: true, window: 'developer' as const, order: 10 }, + { id: 'debug', visible: true, window: 'developer' as const, order: 11 }, + { id: 'update', visible: true, window: 'developer' as const, order: 12 }, +]; diff --git a/app/components/settings/settings.types.ts b/app/components/@settings/core/types.ts similarity index 50% rename from app/components/settings/settings.types.ts rename to app/components/@settings/core/types.ts index 604c6e2d96..97d4d3606b 100644 --- a/app/components/settings/settings.types.ts +++ b/app/components/@settings/core/types.ts @@ -10,12 +10,13 @@ export type TabType = | 'data' | 'cloud-providers' | 'local-providers' + | 'service-status' | 'connection' | 'debug' | 'event-logs' | 'update' | 'task-manager' - | 'service-status'; + | 'tab-management'; export type WindowType = 'user' | 'developer'; @@ -46,14 +47,23 @@ export interface SettingItem { export interface TabVisibilityConfig { id: TabType; visible: boolean; - window: 'user' | 'developer'; + window: WindowType; order: number; + isExtraDevTab?: boolean; locked?: boolean; } +export interface DevTabConfig extends TabVisibilityConfig { + window: 'developer'; +} + +export interface UserTabConfig extends TabVisibilityConfig { + window: 'user'; +} + export interface TabWindowConfig { - userTabs: TabVisibilityConfig[]; - developerTabs: TabVisibilityConfig[]; + userTabs: UserTabConfig[]; + developerTabs: DevTabConfig[]; } export const TAB_LABELS: Record = { @@ -61,47 +71,18 @@ export const TAB_LABELS: Record = { settings: 'Settings', notifications: 'Notifications', features: 'Features', - data: 'Data', + data: 'Data Management', 'cloud-providers': 'Cloud Providers', 'local-providers': 'Local Providers', - connection: 'Connection', + 'service-status': 'Service Status', + connection: 'Connections', debug: 'Debug', 'event-logs': 'Event Logs', - update: 'Update', + update: 'Updates', 'task-manager': 'Task Manager', - 'service-status': 'Service Status', + 'tab-management': 'Tab Management', }; -export const DEFAULT_TAB_CONFIG: TabVisibilityConfig[] = [ - // User Window Tabs (Visible by default) - { id: 'features', visible: true, window: 'user', order: 0 }, - { id: 'data', visible: true, window: 'user', order: 1 }, - { id: 'cloud-providers', visible: true, window: 'user', order: 2 }, - { id: 'local-providers', visible: true, window: 'user', order: 3 }, - { id: 'connection', visible: true, window: 'user', order: 4 }, - { id: 'debug', visible: true, window: 'user', order: 5 }, - - // User Window Tabs (Hidden by default) - { id: 'profile', visible: false, window: 'user', order: 6 }, - { id: 'settings', visible: false, window: 'user', order: 7 }, - { id: 'notifications', visible: false, window: 'user', order: 8 }, - { id: 'event-logs', visible: false, window: 'user', order: 9 }, - { id: 'update', visible: false, window: 'user', order: 10 }, - { id: 'service-status', visible: false, window: 'user', order: 11 }, - - // Developer Window Tabs (All visible by default) - { id: 'features', visible: true, window: 'developer', order: 0 }, - { id: 'data', visible: true, window: 'developer', order: 1 }, - { id: 'cloud-providers', visible: true, window: 'developer', order: 2 }, - { id: 'local-providers', visible: true, window: 'developer', order: 3 }, - { id: 'connection', visible: true, window: 'developer', order: 4 }, - { id: 'debug', visible: true, window: 'developer', order: 5 }, - { id: 'task-manager', visible: true, window: 'developer', order: 6 }, - { id: 'settings', visible: true, window: 'developer', order: 7 }, - { id: 'notifications', visible: true, window: 'developer', order: 8 }, - { id: 'service-status', visible: true, window: 'developer', order: 9 }, -]; - export const categoryLabels: Record = { profile: 'Profile & Account', file_sharing: 'File Sharing', @@ -119,3 +100,15 @@ export const categoryIcons: Record = { services: 'i-ph:cube', preferences: 'i-ph:sliders', }; + +export interface Profile { + username?: string; + bio?: string; + avatar?: string; + preferences?: { + notifications?: boolean; + theme?: 'light' | 'dark' | 'system'; + language?: string; + timezone?: string; + }; +} diff --git a/app/components/@settings/index.ts b/app/components/@settings/index.ts new file mode 100644 index 0000000000..862c33ef77 --- /dev/null +++ b/app/components/@settings/index.ts @@ -0,0 +1,14 @@ +// Core exports +export { ControlPanel } from './core/ControlPanel'; +export type { TabType, TabVisibilityConfig } from './core/types'; + +// Constants +export { TAB_LABELS, TAB_DESCRIPTIONS, DEFAULT_TAB_CONFIG } from './core/constants'; + +// Shared components +export { TabTile } from './shared/components/TabTile'; +export { TabManagement } from './shared/components/TabManagement'; + +// Utils +export { getVisibleTabs, reorderTabs, resetToDefaultConfig } from './utils/tab-helpers'; +export * from './utils/animations'; diff --git a/app/components/settings/shared/DraggableTabList.tsx b/app/components/@settings/shared/components/DraggableTabList.tsx similarity index 96% rename from app/components/settings/shared/DraggableTabList.tsx rename to app/components/@settings/shared/components/DraggableTabList.tsx index 5a583742f2..a8681835dc 100644 --- a/app/components/settings/shared/DraggableTabList.tsx +++ b/app/components/@settings/shared/components/DraggableTabList.tsx @@ -1,8 +1,8 @@ import { useDrag, useDrop } from 'react-dnd'; import { motion } from 'framer-motion'; import { classNames } from '~/utils/classNames'; -import type { TabVisibilityConfig } from '~/components/settings/settings.types'; -import { TAB_LABELS } from '~/components/settings/settings.types'; +import type { TabVisibilityConfig } from '~/components/@settings/core/types'; +import { TAB_LABELS } from '~/components/@settings/core/types'; import { Switch } from '~/components/ui/Switch'; interface DraggableTabListProps { diff --git a/app/components/@settings/shared/components/TabManagement.tsx b/app/components/@settings/shared/components/TabManagement.tsx new file mode 100644 index 0000000000..112dac5383 --- /dev/null +++ b/app/components/@settings/shared/components/TabManagement.tsx @@ -0,0 +1,259 @@ +import { useState } from 'react'; +import { motion } from 'framer-motion'; +import { useStore } from '@nanostores/react'; +import { Switch } from '@radix-ui/react-switch'; +import { classNames } from '~/utils/classNames'; +import { tabConfigurationStore } from '~/lib/stores/settings'; +import { TAB_LABELS } from '~/components/@settings/core/constants'; +import type { TabType } from '~/components/@settings/core/types'; +import { toast } from 'react-toastify'; +import { TbLayoutGrid } from 'react-icons/tb'; + +// Define tab icons mapping +const TAB_ICONS: Record = { + profile: 'i-ph:user-circle-fill', + settings: 'i-ph:gear-six-fill', + notifications: 'i-ph:bell-fill', + features: 'i-ph:star-fill', + data: 'i-ph:database-fill', + 'cloud-providers': 'i-ph:cloud-fill', + 'local-providers': 'i-ph:desktop-fill', + 'service-status': 'i-ph:activity-fill', + connection: 'i-ph:wifi-high-fill', + debug: 'i-ph:bug-fill', + 'event-logs': 'i-ph:list-bullets-fill', + update: 'i-ph:arrow-clockwise-fill', + 'task-manager': 'i-ph:chart-line-fill', + 'tab-management': 'i-ph:squares-four-fill', +}; + +// Define which tabs are default in user mode +const DEFAULT_USER_TABS: TabType[] = [ + 'features', + 'data', + 'cloud-providers', + 'local-providers', + 'connection', + 'notifications', + 'event-logs', +]; + +// Define which tabs can be added to user mode +const OPTIONAL_USER_TABS: TabType[] = ['profile', 'settings', 'task-manager', 'service-status', 'debug', 'update']; + +// All available tabs for user mode +const ALL_USER_TABS = [...DEFAULT_USER_TABS, ...OPTIONAL_USER_TABS]; + +export const TabManagement = () => { + const [searchQuery, setSearchQuery] = useState(''); + const tabConfiguration = useStore(tabConfigurationStore); + + const handleTabVisibilityChange = (tabId: TabType, checked: boolean) => { + // Get current tab configuration + const currentTab = tabConfiguration.userTabs.find((tab) => tab.id === tabId); + + // If tab doesn't exist in configuration, create it + if (!currentTab) { + const newTab = { + id: tabId, + visible: checked, + window: 'user' as const, + order: tabConfiguration.userTabs.length, + }; + + const updatedTabs = [...tabConfiguration.userTabs, newTab]; + + tabConfigurationStore.set({ + ...tabConfiguration, + userTabs: updatedTabs, + }); + + toast.success(`Tab ${checked ? 'enabled' : 'disabled'} successfully`); + + return; + } + + // Check if tab can be enabled in user mode + const canBeEnabled = DEFAULT_USER_TABS.includes(tabId) || OPTIONAL_USER_TABS.includes(tabId); + + if (!canBeEnabled && checked) { + toast.error('This tab cannot be enabled in user mode'); + return; + } + + // Update tab visibility + const updatedTabs = tabConfiguration.userTabs.map((tab) => { + if (tab.id === tabId) { + return { ...tab, visible: checked }; + } + + return tab; + }); + + // Update store + tabConfigurationStore.set({ + ...tabConfiguration, + userTabs: updatedTabs, + }); + + // Show success message + toast.success(`Tab ${checked ? 'enabled' : 'disabled'} successfully`); + }; + + // Create a map of existing tab configurations + const tabConfigMap = new Map(tabConfiguration.userTabs.map((tab) => [tab.id, tab])); + + // Generate the complete list of tabs, including those not in the configuration + const allTabs = ALL_USER_TABS.map((tabId) => { + return ( + tabConfigMap.get(tabId) || { + id: tabId, + visible: false, + window: 'user' as const, + order: -1, + } + ); + }); + + // Filter tabs based on search query + const filteredTabs = allTabs.filter((tab) => TAB_LABELS[tab.id].toLowerCase().includes(searchQuery.toLowerCase())); + + return ( +
+ + {/* Header */} +
+
+
+ +
+
+

Tab Management

+

Configure visible tabs and their order

+
+
+ + {/* Search */} +
+
+
+
+ setSearchQuery(e.target.value)} + placeholder="Search tabs..." + className={classNames( + 'w-full pl-10 pr-4 py-2 rounded-lg', + 'bg-bolt-elements-background-depth-2', + 'border border-bolt-elements-borderColor', + 'text-bolt-elements-textPrimary', + 'placeholder-bolt-elements-textTertiary', + 'focus:outline-none focus:ring-2 focus:ring-purple-500/30', + 'transition-all duration-200', + )} + /> +
+
+ + {/* Tab Grid */} +
+ {filteredTabs.map((tab, index) => ( + + {/* Status Badges */} +
+ {DEFAULT_USER_TABS.includes(tab.id) && ( + + Default + + )} + {OPTIONAL_USER_TABS.includes(tab.id) && ( + + Optional + + )} +
+ +
+ +
+
+
+ + +
+
+
+

+ {TAB_LABELS[tab.id]} +

+

+ {tab.visible ? 'Visible in user mode' : 'Hidden in user mode'} +

+
+ handleTabVisibilityChange(tab.id, checked)} + disabled={!DEFAULT_USER_TABS.includes(tab.id) && !OPTIONAL_USER_TABS.includes(tab.id)} + className={classNames( + 'relative inline-flex h-5 w-9 items-center rounded-full', + 'transition-colors duration-200', + tab.visible ? 'bg-purple-500' : 'bg-bolt-elements-background-depth-4', + { + 'opacity-50 cursor-not-allowed': + !DEFAULT_USER_TABS.includes(tab.id) && !OPTIONAL_USER_TABS.includes(tab.id), + }, + )} + /> +
+
+
+ + + + ))} +
+
+
+ ); +}; diff --git a/app/components/@settings/shared/components/TabTile.tsx b/app/components/@settings/shared/components/TabTile.tsx new file mode 100644 index 0000000000..be91c4f73b --- /dev/null +++ b/app/components/@settings/shared/components/TabTile.tsx @@ -0,0 +1,162 @@ +import { motion } from 'framer-motion'; +import * as Tooltip from '@radix-ui/react-tooltip'; +import { classNames } from '~/utils/classNames'; +import type { TabVisibilityConfig } from '~/components/@settings/core/types'; +import { TAB_LABELS, TAB_ICONS } from '~/components/@settings/core/constants'; + +interface TabTileProps { + tab: TabVisibilityConfig; + onClick?: () => void; + isActive?: boolean; + hasUpdate?: boolean; + statusMessage?: string; + description?: string; + isLoading?: boolean; + className?: string; +} + +export const TabTile = ({ + tab, + onClick, + isActive, + hasUpdate, + statusMessage, + description, + isLoading, + className, +}: TabTileProps) => { + return ( + + + + + {/* Main Content */} +
+ {/* Icon */} + + + + + {/* Label and Description */} +
+

+ {TAB_LABELS[tab.id]} +

+ {description && ( +

+ {description} +

+ )} +
+
+ + {/* Status Indicator */} + {hasUpdate && ( + + )} + + {/* Loading Overlay */} + {isLoading && ( + + + + )} + +
+ + + {statusMessage || TAB_LABELS[tab.id]} + + + +
+
+ ); +}; diff --git a/app/components/settings/connections/ConnectionsTab.tsx b/app/components/@settings/tabs/connections/ConnectionsTab.tsx similarity index 100% rename from app/components/settings/connections/ConnectionsTab.tsx rename to app/components/@settings/tabs/connections/ConnectionsTab.tsx diff --git a/app/components/settings/connections/components/ConnectionForm.tsx b/app/components/@settings/tabs/connections/components/ConnectionForm.tsx similarity index 98% rename from app/components/settings/connections/components/ConnectionForm.tsx rename to app/components/@settings/tabs/connections/components/ConnectionForm.tsx index d1edb1c5a2..04210e2b5f 100644 --- a/app/components/settings/connections/components/ConnectionForm.tsx +++ b/app/components/@settings/tabs/connections/components/ConnectionForm.tsx @@ -1,6 +1,6 @@ import React, { useEffect } from 'react'; import { classNames } from '~/utils/classNames'; -import type { GitHubAuthState } from '~/components/settings/connections/types/GitHub'; +import type { GitHubAuthState } from '~/components/@settings/tabs/connections/types/GitHub'; import Cookies from 'js-cookie'; import { getLocalStorage } from '~/lib/persistence'; diff --git a/app/components/settings/connections/components/CreateBranchDialog.tsx b/app/components/@settings/tabs/connections/components/CreateBranchDialog.tsx similarity index 98% rename from app/components/settings/connections/components/CreateBranchDialog.tsx rename to app/components/@settings/tabs/connections/components/CreateBranchDialog.tsx index 2f1912637f..3fd32ff275 100644 --- a/app/components/settings/connections/components/CreateBranchDialog.tsx +++ b/app/components/@settings/tabs/connections/components/CreateBranchDialog.tsx @@ -1,7 +1,7 @@ import { useState } from 'react'; import * as Dialog from '@radix-ui/react-dialog'; import { classNames } from '~/utils/classNames'; -import type { GitHubRepoInfo } from '~/components/settings/connections/types/GitHub'; +import type { GitHubRepoInfo } from '~/components/@settings/tabs/connections/types/GitHub'; import { GitBranch } from '@phosphor-icons/react'; interface GitHubBranch { diff --git a/app/components/settings/connections/components/PushToGitHubDialog.tsx b/app/components/@settings/tabs/connections/components/PushToGitHubDialog.tsx similarity index 100% rename from app/components/settings/connections/components/PushToGitHubDialog.tsx rename to app/components/@settings/tabs/connections/components/PushToGitHubDialog.tsx diff --git a/app/components/settings/connections/components/RepositorySelectionDialog.tsx b/app/components/@settings/tabs/connections/components/RepositorySelectionDialog.tsx similarity index 100% rename from app/components/settings/connections/components/RepositorySelectionDialog.tsx rename to app/components/@settings/tabs/connections/components/RepositorySelectionDialog.tsx diff --git a/app/components/settings/connections/types/GitHub.ts b/app/components/@settings/tabs/connections/types/GitHub.ts similarity index 100% rename from app/components/settings/connections/types/GitHub.ts rename to app/components/@settings/tabs/connections/types/GitHub.ts diff --git a/app/components/settings/data/DataTab.tsx b/app/components/@settings/tabs/data/DataTab.tsx similarity index 100% rename from app/components/settings/data/DataTab.tsx rename to app/components/@settings/tabs/data/DataTab.tsx diff --git a/app/components/settings/debug/DebugTab.tsx b/app/components/@settings/tabs/debug/DebugTab.tsx similarity index 90% rename from app/components/settings/debug/DebugTab.tsx rename to app/components/@settings/tabs/debug/DebugTab.tsx index be919c8e71..8d8b0307a0 100644 --- a/app/components/settings/debug/DebugTab.tsx +++ b/app/components/@settings/tabs/debug/DebugTab.tsx @@ -131,6 +131,13 @@ interface WebAppInfo { gitInfo: GitInfo; } +// Add Ollama service status interface +interface OllamaServiceStatus { + isRunning: boolean; + lastChecked: Date; + error?: string; +} + const DependencySection = ({ title, deps, @@ -146,7 +153,17 @@ const DependencySection = ({ return ( - +
@@ -157,15 +174,22 @@ const DependencySection = ({ {isOpen ? 'Hide' : 'Show'}
- -
+ +
{deps.map((dep) => (
{dep.name} @@ -182,6 +206,10 @@ const DependencySection = ({ export default function DebugTab() { const [systemInfo, setSystemInfo] = useState(null); const [webAppInfo, setWebAppInfo] = useState(null); + const [ollamaStatus, setOllamaStatus] = useState({ + isRunning: false, + lastChecked: new Date(), + }); const [loading, setLoading] = useState({ systemInfo: false, webAppInfo: false, @@ -259,7 +287,8 @@ export default function DebugTab() { return undefined; } - const interval = setInterval(async () => { + // Initial fetch + const fetchGitInfo = async () => { try { const response = await fetch('/api/system/git-info'); const updatedGitInfo = (await response.json()) as GitInfo; @@ -269,21 +298,27 @@ export default function DebugTab() { 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 refresh git info:', error); + console.error('Failed to fetch git info:', error); } - }, 5000); - - const cleanup = () => { - clearInterval(interval); }; - return cleanup; + fetchGitInfo(); + + // Refresh every 5 minutes instead of every second + const interval = setInterval(fetchGitInfo, 5 * 60 * 1000); + + return () => clearInterval(interval); }, [openSections.webapp]); const getSystemInfo = async () => { @@ -616,11 +651,68 @@ export default function DebugTab() { } }; + // Add Ollama health check function + const checkOllamaHealth = async () => { + try { + const response = await fetch('http://127.0.0.1:11434/api/version'); + const isHealthy = response.ok; + + setOllamaStatus({ + isRunning: isHealthy, + lastChecked: new Date(), + error: isHealthy ? undefined : 'Ollama service is not responding', + }); + + return isHealthy; + } catch { + setOllamaStatus({ + isRunning: false, + lastChecked: new Date(), + error: 'Failed to connect to Ollama service', + }); + return false; + } + }; + + // Add Ollama health check effect + useEffect(() => { + const checkHealth = async () => { + await checkOllamaHealth(); + }; + + checkHealth(); + + const interval = setInterval(checkHealth, 30000); // Check every 30 seconds + + return () => clearInterval(interval); + }, []); + return (
{/* Quick Stats Banner */}
-
+ {/* Add Ollama Service Status Card */} +
+
Ollama Service
+
+
+ + {ollamaStatus.isRunning ? 'Running' : 'Not Running'} + +
+
+ Last checked: {ollamaStatus.lastChecked.toLocaleTimeString()} +
+
+ +
Memory Usage
{systemInfo?.memory.percentage}% @@ -628,7 +720,7 @@ export default function DebugTab() {
-
+
Page Load Time
{systemInfo ? (systemInfo.performance.timing.loadTime / 1000).toFixed(2) + 's' : '-'} @@ -638,7 +730,7 @@ export default function DebugTab() {
-
+
Network Speed
{systemInfo?.network.downlink || '-'} Mbps @@ -646,7 +738,7 @@ export default function DebugTab() {
RTT: {systemInfo?.network.rtt || '-'} ms
-
+
Errors
{errorLogs.length}
@@ -659,10 +751,11 @@ export default function DebugTab() { disabled={loading.systemInfo} className={classNames( 'flex items-center gap-2 px-4 py-2 text-sm font-medium rounded-lg transition-colors', - 'bg-[#F5F5F5] hover:bg-purple-500/10 hover:text-purple-500', - 'dark:bg-[#1A1A1A] dark:hover:bg-purple-500/20', - 'text-bolt-elements-textPrimary dark:hover:text-purple-500', - 'focus:outline-none focus:ring-2 focus:ring-purple-500 focus:ring-offset-2 dark:focus:ring-offset-[#0A0A0A]', + '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 }, )} > @@ -679,10 +772,11 @@ export default function DebugTab() { disabled={loading.performance} className={classNames( 'flex items-center gap-2 px-4 py-2 text-sm font-medium rounded-lg transition-colors', - 'bg-[#F5F5F5] hover:bg-purple-500/10 hover:text-purple-500', - 'dark:bg-[#1A1A1A] dark:hover:bg-purple-500/20', - 'text-bolt-elements-textPrimary dark:hover:text-purple-500', - 'focus:outline-none focus:ring-2 focus:ring-purple-500 focus:ring-offset-2 dark:focus:ring-offset-[#0A0A0A]', + '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 }, )} > @@ -699,10 +793,11 @@ export default function DebugTab() { disabled={loading.errors} className={classNames( 'flex items-center gap-2 px-4 py-2 text-sm font-medium rounded-lg transition-colors', - 'bg-[#F5F5F5] hover:bg-purple-500/10 hover:text-purple-500', - 'dark:bg-[#1A1A1A] dark:hover:bg-purple-500/20', - 'text-bolt-elements-textPrimary dark:hover:text-purple-500', - 'focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2 dark:focus:ring-offset-[#0A0A0A]', + '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 }, )} > @@ -719,10 +814,11 @@ export default function DebugTab() { disabled={loading.webAppInfo} className={classNames( 'flex items-center gap-2 px-4 py-2 text-sm font-medium rounded-lg transition-colors', - 'bg-[#F5F5F5] hover:bg-purple-500/10 hover:text-purple-500', - 'dark:bg-[#1A1A1A] dark:hover:bg-purple-500/20', - 'text-bolt-elements-textPrimary dark:hover:text-purple-500', - 'focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 dark:focus:ring-offset-[#0A0A0A]', + '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 }, )} > @@ -738,10 +834,11 @@ export default function DebugTab() { onClick={exportDebugInfo} className={classNames( 'flex items-center gap-2 px-4 py-2 text-sm font-medium rounded-lg transition-colors', - 'bg-[#F5F5F5] hover:bg-purple-500/10 hover:text-purple-500', - 'dark:bg-[#1A1A1A] dark:hover:bg-purple-500/20', - 'text-bolt-elements-textPrimary dark:hover:text-purple-500', - 'focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 dark:focus:ring-offset-[#0A0A0A]', + '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', )} >
@@ -1152,7 +1249,7 @@ export default function DebugTab() { {webAppInfo && (

Dependencies

-
+
diff --git a/app/components/@settings/tabs/event-logs/EventLogsTab.tsx b/app/components/@settings/tabs/event-logs/EventLogsTab.tsx new file mode 100644 index 0000000000..c414d6cf2a --- /dev/null +++ b/app/components/@settings/tabs/event-logs/EventLogsTab.tsx @@ -0,0 +1,613 @@ +import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { motion } from 'framer-motion'; +import { Switch } from '~/components/ui/Switch'; +import { logStore, type LogEntry } from '~/lib/stores/logs'; +import { useStore } from '@nanostores/react'; +import { classNames } from '~/utils/classNames'; +import * as DropdownMenu from '@radix-ui/react-dropdown-menu'; + +interface SelectOption { + value: string; + label: string; + icon?: string; + color?: string; +} + +const logLevelOptions: SelectOption[] = [ + { + value: 'all', + label: 'All Types', + icon: 'i-ph:funnel', + color: '#9333ea', + }, + { + value: 'provider', + label: 'LLM', + icon: 'i-ph:robot', + color: '#10b981', + }, + { + value: 'api', + label: 'API', + icon: 'i-ph:cloud', + color: '#3b82f6', + }, + { + value: 'error', + label: 'Errors', + icon: 'i-ph:warning-circle', + color: '#ef4444', + }, + { + value: 'warning', + label: 'Warnings', + icon: 'i-ph:warning', + color: '#f59e0b', + }, + { + value: 'info', + label: 'Info', + icon: 'i-ph:info', + color: '#3b82f6', + }, + { + value: 'debug', + label: 'Debug', + icon: 'i-ph:bug', + color: '#6b7280', + }, +]; + +interface LogEntryItemProps { + log: LogEntry; + isExpanded: boolean; + use24Hour: boolean; + showTimestamp: boolean; +} + +const LogEntryItem = ({ log, isExpanded: forceExpanded, use24Hour, showTimestamp }: LogEntryItemProps) => { + const [localExpanded, setLocalExpanded] = useState(forceExpanded); + + useEffect(() => { + setLocalExpanded(forceExpanded); + }, [forceExpanded]); + + const timestamp = useMemo(() => { + const date = new Date(log.timestamp); + return date.toLocaleTimeString('en-US', { hour12: !use24Hour }); + }, [log.timestamp, use24Hour]); + + const style = useMemo(() => { + if (log.category === 'provider') { + return { + icon: 'i-ph:robot', + color: 'text-emerald-500 dark:text-emerald-400', + bg: 'hover:bg-emerald-500/10 dark:hover:bg-emerald-500/20', + badge: 'text-emerald-500 bg-emerald-50 dark:bg-emerald-500/10', + }; + } + + if (log.category === 'api') { + return { + icon: 'i-ph:cloud', + color: 'text-blue-500 dark:text-blue-400', + bg: 'hover:bg-blue-500/10 dark:hover:bg-blue-500/20', + badge: 'text-blue-500 bg-blue-50 dark:bg-blue-500/10', + }; + } + + switch (log.level) { + case 'error': + return { + icon: 'i-ph:warning-circle', + color: 'text-red-500 dark:text-red-400', + bg: 'hover:bg-red-500/10 dark:hover:bg-red-500/20', + badge: 'text-red-500 bg-red-50 dark:bg-red-500/10', + }; + case 'warning': + return { + icon: 'i-ph:warning', + color: 'text-yellow-500 dark:text-yellow-400', + bg: 'hover:bg-yellow-500/10 dark:hover:bg-yellow-500/20', + badge: 'text-yellow-500 bg-yellow-50 dark:bg-yellow-500/10', + }; + case 'debug': + return { + icon: 'i-ph:bug', + color: 'text-gray-500 dark:text-gray-400', + bg: 'hover:bg-gray-500/10 dark:hover:bg-gray-500/20', + badge: 'text-gray-500 bg-gray-50 dark:bg-gray-500/10', + }; + default: + return { + icon: 'i-ph:info', + color: 'text-blue-500 dark:text-blue-400', + bg: 'hover:bg-blue-500/10 dark:hover:bg-blue-500/20', + badge: 'text-blue-500 bg-blue-50 dark:bg-blue-500/10', + }; + } + }, [log.level, log.category]); + + const renderDetails = (details: any) => { + if (log.category === 'provider') { + return ( +
+
+ Model: {details.model} + • + Tokens: {details.totalTokens} + • + Duration: {details.duration}ms +
+ {details.prompt && ( +
+
Prompt:
+
+                {details.prompt}
+              
+
+ )} + {details.response && ( +
+
Response:
+
+                {details.response}
+              
+
+ )} +
+ ); + } + + if (log.category === 'api') { + return ( +
+
+ {details.method} + • + Status: {details.statusCode} + • + Duration: {details.duration}ms +
+
{details.url}
+ {details.request && ( +
+
Request:
+
+                {JSON.stringify(details.request, null, 2)}
+              
+
+ )} + {details.response && ( +
+
Response:
+
+                {JSON.stringify(details.response, null, 2)}
+              
+
+ )} + {details.error && ( +
+
Error:
+
+                {JSON.stringify(details.error, null, 2)}
+              
+
+ )} +
+ ); + } + + return ( +
+        {JSON.stringify(details, null, 2)}
+      
+ ); + }; + + return ( + +
+
+ +
+
{log.message}
+ {log.details && ( + <> + + {localExpanded && renderDetails(log.details)} + + )} +
+
+ {log.level} +
+ {log.category && ( +
+ {log.category} +
+ )} +
+
+
+ {showTimestamp && } +
+
+ ); +}; + +export function EventLogsTab() { + const logs = useStore(logStore.logs); + const [selectedLevel, setSelectedLevel] = useState<'all' | string>('all'); + const [searchQuery, setSearchQuery] = useState(''); + const [use24Hour, setUse24Hour] = useState(false); + const [autoExpand, setAutoExpand] = useState(false); + const [showTimestamps, setShowTimestamps] = useState(true); + const [showLevelFilter, setShowLevelFilter] = useState(false); + const [isRefreshing, setIsRefreshing] = useState(false); + const levelFilterRef = useRef(null); + + const filteredLogs = useMemo(() => { + const allLogs = Object.values(logs); + + if (selectedLevel === 'all') { + return allLogs.filter((log) => + searchQuery ? log.message.toLowerCase().includes(searchQuery.toLowerCase()) : true, + ); + } + + return allLogs.filter((log) => { + const matchesType = log.category === selectedLevel || log.level === selectedLevel; + const matchesSearch = searchQuery ? log.message.toLowerCase().includes(searchQuery.toLowerCase()) : true; + + return matchesType && matchesSearch; + }); + }, [logs, selectedLevel, searchQuery]); + + // Add performance tracking on mount + useEffect(() => { + const startTime = performance.now(); + + logStore.logInfo('Event Logs tab mounted', { + type: 'component_mount', + message: 'Event Logs tab component mounted', + component: 'EventLogsTab', + }); + + return () => { + const duration = performance.now() - startTime; + logStore.logPerformanceMetric('EventLogsTab', 'mount-duration', duration); + }; + }, []); + + // Log filter changes + const handleLevelFilterChange = useCallback( + (newLevel: string) => { + logStore.logInfo('Log level filter changed', { + type: 'filter_change', + message: `Log level filter changed from ${selectedLevel} to ${newLevel}`, + component: 'EventLogsTab', + previousLevel: selectedLevel, + newLevel, + }); + setSelectedLevel(newLevel as string); + setShowLevelFilter(false); + }, + [selectedLevel], + ); + + // Log search changes with debounce + useEffect(() => { + const timeoutId = setTimeout(() => { + if (searchQuery) { + logStore.logInfo('Log search performed', { + type: 'search', + message: `Search performed with query "${searchQuery}" (${filteredLogs.length} results)`, + component: 'EventLogsTab', + query: searchQuery, + resultsCount: filteredLogs.length, + }); + } + }, 1000); + + return () => clearTimeout(timeoutId); + }, [searchQuery, filteredLogs.length]); + + // Enhanced export logs handler + const handleExportLogs = useCallback(() => { + const startTime = performance.now(); + + try { + const exportData = { + timestamp: new Date().toISOString(), + logs: filteredLogs, + filters: { + level: selectedLevel, + searchQuery, + }, + }; + + const blob = new Blob([JSON.stringify(exportData, null, 2)], { type: 'application/json' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = `bolt-logs-${new Date().toISOString()}.json`; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); + + const duration = performance.now() - startTime; + logStore.logSuccess('Logs exported successfully', { + type: 'export', + message: `Successfully exported ${filteredLogs.length} logs`, + component: 'EventLogsTab', + exportedCount: filteredLogs.length, + filters: { + level: selectedLevel, + searchQuery, + }, + duration, + }); + } catch (error) { + logStore.logError('Failed to export logs', error, { + type: 'export_error', + message: 'Failed to export logs', + component: 'EventLogsTab', + }); + } + }, [filteredLogs, selectedLevel, searchQuery]); + + // Enhanced refresh handler + const handleRefresh = useCallback(async () => { + const startTime = performance.now(); + setIsRefreshing(true); + + try { + await logStore.refreshLogs(); + + const duration = performance.now() - startTime; + + logStore.logSuccess('Logs refreshed successfully', { + type: 'refresh', + message: `Successfully refreshed ${Object.keys(logs).length} logs`, + component: 'EventLogsTab', + duration, + logsCount: Object.keys(logs).length, + }); + } catch (error) { + logStore.logError('Failed to refresh logs', error, { + type: 'refresh_error', + message: 'Failed to refresh logs', + component: 'EventLogsTab', + }); + } finally { + setTimeout(() => setIsRefreshing(false), 500); + } + }, [logs]); + + // Log preference changes + const handlePreferenceChange = useCallback((type: string, value: boolean) => { + logStore.logInfo('Log preference changed', { + type: 'preference_change', + message: `Log preference "${type}" changed to ${value}`, + component: 'EventLogsTab', + preference: type, + value, + }); + + switch (type) { + case 'timestamps': + setShowTimestamps(value); + break; + case '24hour': + setUse24Hour(value); + break; + case 'autoExpand': + setAutoExpand(value); + break; + } + }, []); + + // Close filters when clicking outside + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if (levelFilterRef.current && !levelFilterRef.current.contains(event.target as Node)) { + setShowLevelFilter(false); + } + }; + + document.addEventListener('mousedown', handleClickOutside); + + return () => { + document.removeEventListener('mousedown', handleClickOutside); + }; + }, []); + + const selectedLevelOption = logLevelOptions.find((opt) => opt.value === selectedLevel); + + return ( +
+
+ + + + + + + + {logLevelOptions.map((option) => ( + handleLevelFilterChange(option.value)} + > +
+
+
+ {option.label} + + ))} + + + + +
+
+ handlePreferenceChange('timestamps', value)} + className="data-[state=checked]:bg-purple-500" + /> + Show Timestamps +
+ +
+ handlePreferenceChange('24hour', value)} + className="data-[state=checked]:bg-purple-500" + /> + 24h Time +
+ +
+ handlePreferenceChange('autoExpand', value)} + className="data-[state=checked]:bg-purple-500" + /> + Auto Expand +
+ +
+ + + + +
+
+ +
+
+ setSearchQuery(e.target.value)} + className={classNames( + 'w-full px-4 py-2 pl-10 rounded-lg', + 'bg-[#FAFAFA] dark:bg-[#0A0A0A]', + 'border border-[#E5E5E5] dark:border-[#1A1A1A]', + 'text-gray-900 dark:text-white placeholder-gray-500 dark:placeholder-gray-400', + 'focus:outline-none focus:ring-2 focus:ring-purple-500/20 focus:border-purple-500', + 'transition-all duration-200', + )} + /> +
+
+
+
+ + {filteredLogs.length === 0 ? ( + + +
+

No Logs Found

+

Try adjusting your search or filters

+
+
+ ) : ( + filteredLogs.map((log) => ( + + )) + )} +
+
+ ); +} diff --git a/app/components/settings/features/FeaturesTab.tsx b/app/components/@settings/tabs/features/FeaturesTab.tsx similarity index 84% rename from app/components/settings/features/FeaturesTab.tsx rename to app/components/@settings/tabs/features/FeaturesTab.tsx index 2cf735d64b..9aa838c4ce 100644 --- a/app/components/settings/features/FeaturesTab.tsx +++ b/app/components/@settings/tabs/features/FeaturesTab.tsx @@ -111,44 +111,66 @@ export default function FeaturesTab() { isLatestBranch, contextOptimizationEnabled, eventLogs, - isLocalModel, setAutoSelectTemplate, enableLatestBranch, enableContextOptimization, setEventLogs, - enableLocalModels, setPromptId, promptId, } = useSettings(); + // Enable features by default on first load + React.useEffect(() => { + // Only enable if they haven't been explicitly set before + if (isLatestBranch === undefined) { + enableLatestBranch(true); + } + + if (contextOptimizationEnabled === undefined) { + enableContextOptimization(true); + } + + if (autoSelectTemplate === undefined) { + setAutoSelectTemplate(true); + } + + if (eventLogs === undefined) { + setEventLogs(true); + } + }, []); // Only run once on component mount + const handleToggleFeature = useCallback( (id: string, enabled: boolean) => { switch (id) { - case 'latestBranch': + case 'latestBranch': { enableLatestBranch(enabled); toast.success(`Main branch updates ${enabled ? 'enabled' : 'disabled'}`); break; - case 'autoSelectTemplate': + } + + case 'autoSelectTemplate': { setAutoSelectTemplate(enabled); toast.success(`Auto select template ${enabled ? 'enabled' : 'disabled'}`); break; - case 'contextOptimization': + } + + case 'contextOptimization': { enableContextOptimization(enabled); toast.success(`Context optimization ${enabled ? 'enabled' : 'disabled'}`); break; - case 'eventLogs': + } + + case 'eventLogs': { setEventLogs(enabled); toast.success(`Event logging ${enabled ? 'enabled' : 'disabled'}`); break; - case 'localModels': - enableLocalModels(enabled); - toast.success(`Experimental providers ${enabled ? 'enabled' : 'disabled'}`); - break; + } + default: break; } }, - [enableLatestBranch, setAutoSelectTemplate, enableContextOptimization, setEventLogs, enableLocalModels], + [enableLatestBranch, setAutoSelectTemplate, enableContextOptimization, setEventLogs], ); const features = { @@ -159,7 +181,7 @@ export default function FeaturesTab() { description: 'Get the latest updates from the main branch', icon: 'i-ph:git-branch', enabled: isLatestBranch, - tooltip: 'Enable to receive updates from the main development branch', + tooltip: 'Enabled by default to receive updates from the main development branch', }, { id: 'autoSelectTemplate', @@ -167,7 +189,7 @@ export default function FeaturesTab() { description: 'Automatically select starter template', icon: 'i-ph:selection', enabled: autoSelectTemplate, - tooltip: 'Automatically select the most appropriate starter template', + tooltip: 'Enabled by default to automatically select the most appropriate starter template', }, { id: 'contextOptimization', @@ -175,7 +197,7 @@ export default function FeaturesTab() { description: 'Optimize context for better responses', icon: 'i-ph:brain', enabled: contextOptimizationEnabled, - tooltip: 'Enable context optimization for improved AI responses', + tooltip: 'Enabled by default for improved AI responses', }, { id: 'eventLogs', @@ -183,30 +205,19 @@ export default function FeaturesTab() { description: 'Enable detailed event logging and history', icon: 'i-ph:list-bullets', enabled: eventLogs, - tooltip: 'Record detailed logs of system events and user actions', + tooltip: 'Enabled by default to record detailed logs of system events and user actions', }, ], beta: [], - experimental: [ - { - id: 'localModels', - title: 'Experimental Providers', - description: 'Enable experimental providers like Ollama, LMStudio, and OpenAILike', - icon: 'i-ph:robot', - enabled: isLocalModel, - experimental: true, - tooltip: 'Try out new AI providers and models in development', - }, - ], }; return (
@@ -220,16 +231,6 @@ export default function FeaturesTab() { /> )} - {features.experimental.length > 0 && ( - - )} - ) => { + const file = e.target.files?.[0]; + + if (!file) { + return; + } + + try { + setIsUploading(true); + + // Convert the file to base64 + const reader = new FileReader(); + + reader.onloadend = () => { + const base64String = reader.result as string; + updateProfile({ avatar: base64String }); + setIsUploading(false); + toast.success('Profile picture updated'); + }; + + reader.onerror = () => { + console.error('Error reading file:', reader.error); + setIsUploading(false); + toast.error('Failed to update profile picture'); + }; + reader.readAsDataURL(file); + } catch (error) { + console.error('Error uploading avatar:', error); + setIsUploading(false); + toast.error('Failed to update profile picture'); + } + }; + + const handleProfileUpdate = (field: 'username' | 'bio', value: string) => { + updateProfile({ [field]: value }); + + // Only show toast for completed typing (after 1 second of no typing) + const debounceToast = setTimeout(() => { + toast.success(`${field.charAt(0).toUpperCase() + field.slice(1)} updated`); + }, 1000); + + return () => clearTimeout(debounceToast); + }; + + return ( +
+
+ {/* Personal Information Section */} +
+ {/* Avatar Upload */} +
+
+ {profile.avatar ? ( + Profile + ) : ( +
+ )} + +