Stijnus commited on
Commit
fc3dd8c
Β·
1 Parent(s): 999d87b

Final UI V3

Browse files

# UI V3 Changelog

Major updates and improvements in this release:

## Core Changes
- Complete NEW REWRITTEN UI system overhaul (V3) with semantic design tokens
- New settings management system with drag-and-drop capabilities
- Enhanced provider system supporting multiple AI services
- Improved theme system with better dark mode support
- New component library with consistent design patterns

## Technical Updates
- Reorganized project architecture for better maintainability
- Performance optimizations and bundle size improvements
- Enhanced security features and access controls
- Improved developer experience with better tooling
- Comprehensive testing infrastructure

## New Features
- Background rays effect for improved visual feedback
- Advanced tab management system
- Automatic and manual update support
- Enhanced error handling and visualization
- Improved accessibility across all components

For detailed information about all changes and improvements, please see the full changelog.

This view is limited to 50 files because it contains too many changes. Β  See raw diff
Files changed (50) hide show
  1. .gitignore +1 -0
  2. app/components/@settings/core/AvatarDropdown.tsx +181 -0
  3. app/components/{settings β†’ @settings/core}/ControlPanel.tsx +197 -345
  4. app/components/@settings/core/constants.ts +88 -0
  5. app/components/{settings/settings.types.ts β†’ @settings/core/types.ts} +31 -38
  6. app/components/@settings/index.ts +14 -0
  7. app/components/{settings/shared β†’ @settings/shared/components}/DraggableTabList.tsx +2 -2
  8. app/components/@settings/shared/components/TabManagement.tsx +259 -0
  9. app/components/{settings/shared β†’ @settings/shared/components}/TabTile.tsx +86 -158
  10. app/components/{settings β†’ @settings/tabs}/connections/ConnectionsTab.tsx +0 -0
  11. app/components/{settings β†’ @settings/tabs}/connections/components/ConnectionForm.tsx +1 -1
  12. app/components/{settings β†’ @settings/tabs}/connections/components/CreateBranchDialog.tsx +1 -1
  13. app/components/{settings β†’ @settings/tabs}/connections/components/PushToGitHubDialog.tsx +0 -0
  14. app/components/{settings β†’ @settings/tabs}/connections/components/RepositorySelectionDialog.tsx +0 -0
  15. app/components/{settings β†’ @settings/tabs}/connections/types/GitHub.ts +0 -0
  16. app/components/{settings β†’ @settings/tabs}/data/DataTab.tsx +0 -0
  17. app/components/{settings β†’ @settings/tabs}/debug/DebugTab.tsx +133 -36
  18. app/components/@settings/tabs/event-logs/EventLogsTab.tsx +613 -0
  19. app/components/{settings β†’ @settings/tabs}/features/FeaturesTab.tsx +39 -38
  20. app/components/{settings β†’ @settings/tabs}/notifications/NotificationsTab.tsx +0 -0
  21. app/components/@settings/tabs/profile/ProfileTab.tsx +174 -0
  22. app/components/{settings/providers β†’ @settings/tabs/providers/cloud}/CloudProvidersTab.tsx +0 -0
  23. app/components/{settings/providers β†’ @settings/tabs/providers/local}/LocalProvidersTab.tsx +362 -558
  24. app/components/@settings/tabs/providers/local/OllamaModelInstaller.tsx +597 -0
  25. app/components/{settings β†’ @settings/tabs}/providers/service-status/ServiceStatusTab.tsx +0 -0
  26. app/components/{settings β†’ @settings/tabs}/providers/service-status/base-provider.ts +0 -0
  27. app/components/{settings β†’ @settings/tabs}/providers/service-status/provider-factory.ts +0 -0
  28. app/components/{settings β†’ @settings/tabs}/providers/service-status/providers/amazon-bedrock.ts +2 -2
  29. app/components/{settings β†’ @settings/tabs}/providers/service-status/providers/anthropic.ts +2 -2
  30. app/components/{settings β†’ @settings/tabs}/providers/service-status/providers/cohere.ts +2 -2
  31. app/components/{settings β†’ @settings/tabs}/providers/service-status/providers/deepseek.ts +2 -2
  32. app/components/{settings β†’ @settings/tabs}/providers/service-status/providers/google.ts +2 -2
  33. app/components/{settings β†’ @settings/tabs}/providers/service-status/providers/groq.ts +2 -2
  34. app/components/{settings β†’ @settings/tabs}/providers/service-status/providers/huggingface.ts +2 -2
  35. app/components/{settings β†’ @settings/tabs}/providers/service-status/providers/hyperbolic.ts +2 -2
  36. app/components/{settings β†’ @settings/tabs}/providers/service-status/providers/mistral.ts +2 -2
  37. app/components/{settings β†’ @settings/tabs}/providers/service-status/providers/openai.ts +2 -2
  38. app/components/{settings β†’ @settings/tabs}/providers/service-status/providers/openrouter.ts +2 -2
  39. app/components/{settings β†’ @settings/tabs}/providers/service-status/providers/perplexity.ts +2 -2
  40. app/components/{settings β†’ @settings/tabs}/providers/service-status/providers/together.ts +2 -2
  41. app/components/{settings β†’ @settings/tabs}/providers/service-status/providers/xai.ts +2 -2
  42. app/components/{settings β†’ @settings/tabs}/providers/service-status/types.ts +0 -0
  43. app/components/{settings/providers β†’ @settings/tabs/providers/status}/ServiceStatusTab.tsx +0 -0
  44. app/components/{settings β†’ @settings/tabs}/settings/SettingsTab.tsx +1 -1
  45. app/components/{settings β†’ @settings/tabs}/task-manager/TaskManagerTab.tsx +336 -174
  46. app/components/{settings β†’ @settings/tabs}/update/UpdateTab.tsx +84 -94
  47. app/components/@settings/utils/animations.ts +41 -0
  48. app/components/@settings/utils/tab-helpers.ts +89 -0
  49. app/components/chat/Chat.client.tsx +32 -25
  50. app/components/chat/GitCloneButton.tsx +1 -1
.gitignore CHANGED
@@ -44,3 +44,4 @@ changelogUI.md
44
  docs/instructions/Roadmap.md
45
  .cursorrules
46
  .cursorrules
 
 
44
  docs/instructions/Roadmap.md
45
  .cursorrules
46
  .cursorrules
47
+ *.md
app/components/@settings/core/AvatarDropdown.tsx ADDED
@@ -0,0 +1,181 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as DropdownMenu from '@radix-ui/react-dropdown-menu';
2
+ import { motion } from 'framer-motion';
3
+ import { useStore } from '@nanostores/react';
4
+ import { classNames } from '~/utils/classNames';
5
+ import { profileStore } from '~/lib/stores/profile';
6
+ import type { TabType, Profile } from './types';
7
+
8
+ interface AvatarDropdownProps {
9
+ onSelectTab: (tab: TabType) => void;
10
+ }
11
+
12
+ export const AvatarDropdown = ({ onSelectTab }: AvatarDropdownProps) => {
13
+ const profile = useStore(profileStore) as Profile;
14
+
15
+ return (
16
+ <DropdownMenu.Root>
17
+ <DropdownMenu.Trigger asChild>
18
+ <motion.button
19
+ className="group flex items-center justify-center"
20
+ whileHover={{ scale: 1.02 }}
21
+ whileTap={{ scale: 0.98 }}
22
+ >
23
+ <div
24
+ className={classNames(
25
+ 'w-10 h-10',
26
+ 'rounded-full overflow-hidden',
27
+ 'bg-gray-100/50 dark:bg-gray-800/50',
28
+ 'flex items-center justify-center',
29
+ 'ring-1 ring-gray-200/50 dark:ring-gray-700/50',
30
+ 'group-hover:ring-purple-500/50 dark:group-hover:ring-purple-500/50',
31
+ 'group-hover:bg-purple-500/10 dark:group-hover:bg-purple-500/10',
32
+ 'transition-all duration-200',
33
+ 'relative',
34
+ )}
35
+ >
36
+ {profile?.avatar ? (
37
+ <div className="w-full h-full">
38
+ <img
39
+ src={profile.avatar}
40
+ alt={profile?.username || 'Profile'}
41
+ className={classNames(
42
+ 'w-full h-full',
43
+ 'object-cover',
44
+ 'transform-gpu',
45
+ 'image-rendering-crisp',
46
+ 'group-hover:brightness-110',
47
+ 'group-hover:scale-105',
48
+ 'transition-all duration-200',
49
+ )}
50
+ loading="eager"
51
+ decoding="sync"
52
+ />
53
+ <div
54
+ className={classNames(
55
+ 'absolute inset-0',
56
+ 'ring-1 ring-inset ring-black/5 dark:ring-white/5',
57
+ 'group-hover:ring-purple-500/20 dark:group-hover:ring-purple-500/20',
58
+ 'group-hover:bg-purple-500/5 dark:group-hover:bg-purple-500/5',
59
+ 'transition-colors duration-200',
60
+ )}
61
+ />
62
+ </div>
63
+ ) : (
64
+ <div className="i-ph:robot-fill w-6 h-6 text-gray-400 dark:text-gray-400 group-hover:text-purple-500 dark:group-hover:text-purple-400 transition-colors" />
65
+ )}
66
+ </div>
67
+ </motion.button>
68
+ </DropdownMenu.Trigger>
69
+
70
+ <DropdownMenu.Portal>
71
+ <DropdownMenu.Content
72
+ className={classNames(
73
+ 'min-w-[240px] z-[250]',
74
+ 'bg-white dark:bg-[#141414]',
75
+ 'rounded-lg shadow-lg',
76
+ 'border border-gray-200/50 dark:border-gray-800/50',
77
+ 'animate-in fade-in-0 zoom-in-95',
78
+ 'py-1',
79
+ )}
80
+ sideOffset={5}
81
+ align="end"
82
+ >
83
+ <div
84
+ className={classNames(
85
+ 'px-4 py-3 flex items-center gap-3',
86
+ 'border-b border-gray-200/50 dark:border-gray-800/50',
87
+ )}
88
+ >
89
+ <div className="w-10 h-10 rounded-full overflow-hidden bg-gray-100/50 dark:bg-gray-800/50 flex-shrink-0">
90
+ {profile?.avatar ? (
91
+ <img
92
+ src={profile.avatar}
93
+ alt={profile?.username || 'Profile'}
94
+ className={classNames('w-full h-full', 'object-cover', 'transform-gpu', 'image-rendering-crisp')}
95
+ loading="eager"
96
+ decoding="sync"
97
+ />
98
+ ) : (
99
+ <div className="w-full h-full flex items-center justify-center">
100
+ <div className="i-ph:robot-fill w-6 h-6 text-gray-400 dark:text-gray-400 group-hover:text-purple-500 dark:group-hover:text-purple-400 transition-colors" />
101
+ </div>
102
+ )}
103
+ </div>
104
+ <div className="flex-1 min-w-0">
105
+ <div className="font-medium text-sm text-gray-900 dark:text-white truncate">
106
+ {profile?.username || 'Guest User'}
107
+ </div>
108
+ {profile?.bio && <div className="text-xs text-gray-500 dark:text-gray-400 truncate">{profile.bio}</div>}
109
+ </div>
110
+ </div>
111
+
112
+ <DropdownMenu.Item
113
+ className={classNames(
114
+ 'flex items-center gap-2 px-4 py-2.5',
115
+ 'text-sm text-gray-700 dark:text-gray-200',
116
+ 'hover:bg-purple-50 dark:hover:bg-purple-500/10',
117
+ 'hover:text-purple-500 dark:hover:text-purple-400',
118
+ 'cursor-pointer transition-all duration-200',
119
+ 'outline-none',
120
+ 'group',
121
+ )}
122
+ onClick={() => onSelectTab('profile')}
123
+ >
124
+ <div className="i-ph:robot-fill w-4 h-4 text-gray-400 group-hover:text-purple-500 dark:group-hover:text-purple-400 transition-colors" />
125
+ Edit Profile
126
+ </DropdownMenu.Item>
127
+
128
+ <DropdownMenu.Item
129
+ className={classNames(
130
+ 'flex items-center gap-2 px-4 py-2.5',
131
+ 'text-sm text-gray-700 dark:text-gray-200',
132
+ 'hover:bg-purple-50 dark:hover:bg-purple-500/10',
133
+ 'hover:text-purple-500 dark:hover:text-purple-400',
134
+ 'cursor-pointer transition-all duration-200',
135
+ 'outline-none',
136
+ 'group',
137
+ )}
138
+ onClick={() => onSelectTab('settings')}
139
+ >
140
+ <div className="i-ph:gear-six-fill w-4 h-4 text-gray-400 group-hover:text-purple-500 dark:group-hover:text-purple-400 transition-colors" />
141
+ Settings
142
+ </DropdownMenu.Item>
143
+
144
+ <div className="my-1 border-t border-gray-200/50 dark:border-gray-800/50" />
145
+
146
+ <DropdownMenu.Item
147
+ className={classNames(
148
+ 'flex items-center gap-2 px-4 py-2.5',
149
+ 'text-sm text-gray-700 dark:text-gray-200',
150
+ 'hover:bg-purple-50 dark:hover:bg-purple-500/10',
151
+ 'hover:text-purple-500 dark:hover:text-purple-400',
152
+ 'cursor-pointer transition-all duration-200',
153
+ 'outline-none',
154
+ 'group',
155
+ )}
156
+ onClick={() => onSelectTab('task-manager')}
157
+ >
158
+ <div className="i-ph:activity-fill w-4 h-4 text-gray-400 group-hover:text-purple-500 dark:group-hover:text-purple-400 transition-colors" />
159
+ Task Manager
160
+ </DropdownMenu.Item>
161
+
162
+ <DropdownMenu.Item
163
+ className={classNames(
164
+ 'flex items-center gap-2 px-4 py-2.5',
165
+ 'text-sm text-gray-700 dark:text-gray-200',
166
+ 'hover:bg-purple-50 dark:hover:bg-purple-500/10',
167
+ 'hover:text-purple-500 dark:hover:text-purple-400',
168
+ 'cursor-pointer transition-all duration-200',
169
+ 'outline-none',
170
+ 'group',
171
+ )}
172
+ onClick={() => onSelectTab('service-status')}
173
+ >
174
+ <div className="i-ph:heartbeat-fill w-4 h-4 text-gray-400 group-hover:text-purple-500 dark:group-hover:text-purple-400 transition-colors" />
175
+ Service Status
176
+ </DropdownMenu.Item>
177
+ </DropdownMenu.Content>
178
+ </DropdownMenu.Portal>
179
+ </DropdownMenu.Root>
180
+ );
181
+ };
app/components/{settings β†’ @settings/core}/ControlPanel.tsx RENAMED
@@ -3,37 +3,36 @@ import { motion, AnimatePresence } from 'framer-motion';
3
  import { useStore } from '@nanostores/react';
4
  import { Switch } from '@radix-ui/react-switch';
5
  import * as RadixDialog from '@radix-ui/react-dialog';
6
- import { DndProvider } from 'react-dnd';
7
- import { HTML5Backend } from 'react-dnd-html5-backend';
8
  import { classNames } from '~/utils/classNames';
9
- import { TabManagement } from './developer/TabManagement';
10
- import { TabTile } from './shared/TabTile';
11
  import { useUpdateCheck } from '~/lib/hooks/useUpdateCheck';
12
  import { useFeatures } from '~/lib/hooks/useFeatures';
13
  import { useNotifications } from '~/lib/hooks/useNotifications';
14
  import { useConnectionStatus } from '~/lib/hooks/useConnectionStatus';
15
  import { useDebugStatus } from '~/lib/hooks/useDebugStatus';
16
  import { tabConfigurationStore, developerModeStore, setDeveloperMode } from '~/lib/stores/settings';
17
- import type { TabType, TabVisibilityConfig } from './settings.types';
18
- import { TAB_LABELS, DEFAULT_TAB_CONFIG } from './settings.types';
 
19
  import { resetTabConfiguration } from '~/lib/stores/settings';
20
  import { DialogTitle } from '~/components/ui/Dialog';
21
- import { useDrag, useDrop } from 'react-dnd';
22
 
23
  // Import all tab components
24
- import ProfileTab from './profile/ProfileTab';
25
- import SettingsTab from './settings/SettingsTab';
26
- import NotificationsTab from './notifications/NotificationsTab';
27
- import FeaturesTab from './features/FeaturesTab';
28
- import DataTab from './data/DataTab';
29
- import DebugTab from './debug/DebugTab';
30
- import { EventLogsTab } from './event-logs/EventLogsTab';
31
- import UpdateTab from './update/UpdateTab';
32
- import ConnectionsTab from './connections/ConnectionsTab';
33
- import CloudProvidersTab from './providers/CloudProvidersTab';
34
- import ServiceStatusTab from './providers/ServiceStatusTab';
35
- import LocalProvidersTab from './providers/LocalProvidersTab';
36
- import TaskManagerTab from './task-manager/TaskManagerTab';
37
 
38
  interface ControlPanelProps {
39
  open: boolean;
@@ -58,124 +57,7 @@ const TAB_DESCRIPTIONS: Record<TabType, string> = {
58
  'event-logs': 'View system events and logs',
59
  update: 'Check for updates and release notes',
60
  'task-manager': 'Monitor system resources and processes',
61
- };
62
-
63
- // Add DraggableTabTile component before the ControlPanel component
64
- const DraggableTabTile = ({
65
- tab,
66
- index,
67
- moveTab,
68
- ...props
69
- }: {
70
- tab: TabWithDevType;
71
- index: number;
72
- moveTab: (dragIndex: number, hoverIndex: number) => void;
73
- onClick: () => void;
74
- isActive: boolean;
75
- hasUpdate: boolean;
76
- statusMessage: string;
77
- description: string;
78
- isLoading?: boolean;
79
- }) => {
80
- const [{ isDragging }, drag] = useDrag({
81
- type: 'tab',
82
- item: { index, id: tab.id },
83
- collect: (monitor) => ({
84
- isDragging: monitor.isDragging(),
85
- }),
86
- });
87
-
88
- const [{ isOver, canDrop }, drop] = useDrop({
89
- accept: 'tab',
90
- hover: (item: { index: number; id: string }, monitor) => {
91
- if (!monitor.isOver({ shallow: true })) {
92
- return;
93
- }
94
-
95
- if (item.id === tab.id) {
96
- return;
97
- }
98
-
99
- if (item.index === index) {
100
- return;
101
- }
102
-
103
- // Only move when hovering over the middle section
104
- const hoverBoundingRect = monitor.getSourceClientOffset();
105
- const clientOffset = monitor.getClientOffset();
106
-
107
- if (!hoverBoundingRect || !clientOffset) {
108
- return;
109
- }
110
-
111
- const hoverMiddleX = hoverBoundingRect.x + 150; // Half of typical card width
112
- const hoverClientX = clientOffset.x;
113
-
114
- // Only perform the move when the mouse has crossed half of the items width
115
- if (item.index < index && hoverClientX < hoverMiddleX) {
116
- return;
117
- }
118
-
119
- if (item.index > index && hoverClientX > hoverMiddleX) {
120
- return;
121
- }
122
-
123
- moveTab(item.index, index);
124
- item.index = index;
125
- },
126
- collect: (monitor) => ({
127
- isOver: monitor.isOver({ shallow: true }),
128
- canDrop: monitor.canDrop(),
129
- }),
130
- });
131
-
132
- const dropIndicatorClasses = classNames('rounded-xl border-2 border-transparent transition-all duration-200', {
133
- 'ring-2 ring-purple-500 ring-opacity-50 bg-purple-50 dark:bg-purple-900/20': isOver,
134
- 'hover:ring-2 hover:ring-purple-500/30': canDrop && !isOver,
135
- });
136
-
137
- return (
138
- <motion.div
139
- ref={(node) => drag(drop(node))}
140
- style={{
141
- opacity: isDragging ? 0.5 : 1,
142
- cursor: 'move',
143
- position: 'relative',
144
- zIndex: isDragging ? 100 : isOver ? 50 : 1,
145
- }}
146
- animate={{
147
- scale: isDragging ? 1.02 : isOver ? 1.05 : 1,
148
- boxShadow: isDragging
149
- ? '0 8px 24px rgba(0, 0, 0, 0.15)'
150
- : isOver
151
- ? '0 4px 12px rgba(147, 51, 234, 0.3)'
152
- : '0 0 0 rgba(0, 0, 0, 0)',
153
- borderColor: isOver ? 'rgb(147, 51, 234)' : isDragging ? 'rgba(147, 51, 234, 0.5)' : 'transparent',
154
- y: isOver ? -2 : 0,
155
- }}
156
- transition={{
157
- type: 'spring',
158
- stiffness: 500,
159
- damping: 30,
160
- mass: 0.8,
161
- }}
162
- className={dropIndicatorClasses}
163
- >
164
- <TabTile {...props} tab={tab} />
165
- {isOver && (
166
- <motion.div
167
- className="absolute inset-0 rounded-xl pointer-events-none"
168
- initial={{ opacity: 0 }}
169
- animate={{ opacity: 1 }}
170
- exit={{ opacity: 0 }}
171
- transition={{ duration: 0.2 }}
172
- >
173
- <div className="absolute inset-0 bg-gradient-to-r from-purple-500/10 to-purple-500/20 rounded-xl" />
174
- <div className="absolute inset-0 border-2 border-purple-500/50 rounded-xl" />
175
- </motion.div>
176
- )}
177
- </motion.div>
178
- );
179
  };
180
 
181
  export const ControlPanel = ({ open, onClose }: ControlPanelProps) => {
@@ -183,11 +65,11 @@ export const ControlPanel = ({ open, onClose }: ControlPanelProps) => {
183
  const [activeTab, setActiveTab] = useState<TabType | null>(null);
184
  const [loadingTab, setLoadingTab] = useState<TabType | null>(null);
185
  const [showTabManagement, setShowTabManagement] = useState(false);
186
- const [profile, setProfile] = useState({ avatar: null, notifications: true });
187
 
188
  // Store values
189
  const tabConfiguration = useStore(tabConfigurationStore);
190
  const developerMode = useStore(developerModeStore);
 
191
 
192
  // Status hooks
193
  const { hasUpdate, currentVersion, acknowledgeUpdate } = useUpdateCheck();
@@ -196,24 +78,6 @@ export const ControlPanel = ({ open, onClose }: ControlPanelProps) => {
196
  const { hasConnectionIssues, currentIssue, acknowledgeIssue } = useConnectionStatus();
197
  const { hasActiveWarnings, activeIssues, acknowledgeAllIssues } = useDebugStatus();
198
 
199
- // Initialize profile from localStorage on mount
200
- useEffect(() => {
201
- if (typeof window === 'undefined') {
202
- return;
203
- }
204
-
205
- const saved = localStorage.getItem('bolt_user_profile');
206
-
207
- if (saved) {
208
- try {
209
- const parsedProfile = JSON.parse(saved);
210
- setProfile(parsedProfile);
211
- } catch (error) {
212
- console.warn('Failed to parse profile from localStorage:', error);
213
- }
214
- }
215
- }, []);
216
-
217
  // Add visibleTabs logic using useMemo
218
  const visibleTabs = useMemo(() => {
219
  if (!tabConfiguration?.userTabs || !Array.isArray(tabConfiguration.userTabs)) {
@@ -248,10 +112,22 @@ export const ControlPanel = ({ open, onClose }: ControlPanelProps) => {
248
  };
249
  });
250
 
 
 
 
 
 
 
 
 
 
 
251
  return devTabs.sort((a, b) => a.order - b.order);
252
  }
253
 
254
  // In user mode, only show visible user tabs
 
 
255
  return tabConfiguration.userTabs
256
  .filter((tab) => {
257
  if (!tab || typeof tab.id !== 'string') {
@@ -259,8 +135,8 @@ export const ControlPanel = ({ open, onClose }: ControlPanelProps) => {
259
  return false;
260
  }
261
 
262
- // Hide notifications tab if notifications are disabled
263
- if (tab.id === 'notifications' && !profile.notifications) {
264
  return false;
265
  }
266
 
@@ -268,38 +144,7 @@ export const ControlPanel = ({ open, onClose }: ControlPanelProps) => {
268
  return tab.visible && tab.window === 'user';
269
  })
270
  .sort((a, b) => a.order - b.order);
271
- }, [tabConfiguration, profile.notifications, developerMode]);
272
-
273
- // Add moveTab handler
274
- const moveTab = (dragIndex: number, hoverIndex: number) => {
275
- const newTabs = [...visibleTabs];
276
- const dragTab = newTabs[dragIndex];
277
- newTabs.splice(dragIndex, 1);
278
- newTabs.splice(hoverIndex, 0, dragTab);
279
-
280
- // Update the order of the tabs
281
- const updatedTabs = newTabs.map((tab, index) => ({
282
- ...tab,
283
- order: index,
284
- window: 'developer' as const,
285
- visible: true,
286
- }));
287
-
288
- // Update the tab configuration store directly
289
- if (developerMode) {
290
- // In developer mode, update developerTabs while preserving configuration
291
- tabConfigurationStore.set({
292
- ...tabConfiguration,
293
- developerTabs: updatedTabs,
294
- });
295
- } else {
296
- // In user mode, update userTabs
297
- tabConfigurationStore.set({
298
- ...tabConfiguration,
299
- userTabs: updatedTabs.map((tab) => ({ ...tab, window: 'user' as const })),
300
- });
301
- }
302
- };
303
 
304
  // Handlers
305
  const handleBack = () => {
@@ -320,8 +165,12 @@ export const ControlPanel = ({ open, onClose }: ControlPanelProps) => {
320
  console.log('Current developer mode:', developerMode);
321
  }, [developerMode]);
322
 
323
- const getTabComponent = () => {
324
- switch (activeTab) {
 
 
 
 
325
  case 'profile':
326
  return <ProfileTab />;
327
  case 'settings':
@@ -398,6 +247,7 @@ export const ControlPanel = ({ open, onClose }: ControlPanelProps) => {
398
  const handleTabClick = (tabId: TabType) => {
399
  setLoadingTab(tabId);
400
  setActiveTab(tabId);
 
401
 
402
  // Acknowledge notifications based on tab
403
  switch (tabId) {
@@ -423,84 +273,75 @@ export const ControlPanel = ({ open, onClose }: ControlPanelProps) => {
423
  };
424
 
425
  return (
426
- <DndProvider backend={HTML5Backend}>
427
- <RadixDialog.Root open={open}>
428
- <RadixDialog.Portal>
429
- <div className="fixed inset-0 flex items-center justify-center z-[100]">
430
- <RadixDialog.Overlay asChild>
431
- <motion.div
432
- className="absolute inset-0 bg-black/50 backdrop-blur-sm"
433
- initial={{ opacity: 0 }}
434
- animate={{ opacity: 1 }}
435
- exit={{ opacity: 0 }}
436
- transition={{ duration: 0.2 }}
437
- />
438
- </RadixDialog.Overlay>
439
-
440
- <RadixDialog.Content
441
- aria-describedby={undefined}
442
- onEscapeKeyDown={onClose}
443
- onPointerDownOutside={onClose}
444
- className="relative z-[101]"
 
 
 
 
 
 
 
 
 
 
 
 
445
  >
446
- <motion.div
447
- className={classNames(
448
- 'w-[1200px] h-[90vh]',
449
- 'bg-[#FAFAFA] dark:bg-[#0A0A0A]',
450
- 'rounded-2xl shadow-2xl',
451
- 'border border-[#E5E5E5] dark:border-[#1A1A1A]',
452
- 'flex flex-col overflow-hidden',
453
- )}
454
- initial={{ opacity: 0, scale: 0.95, y: 20 }}
455
- animate={{ opacity: 1, scale: 1, y: 0 }}
456
- exit={{ opacity: 0, scale: 0.95, y: 20 }}
457
- transition={{ duration: 0.2 }}
458
- >
459
- {/* Header */}
460
- <div className="flex items-center justify-between px-6 py-4 border-b border-gray-200 dark:border-gray-700">
461
- <div className="flex items-center space-x-4">
462
- {activeTab || showTabManagement ? (
463
- <button
464
- onClick={handleBack}
465
- className="flex items-center justify-center w-8 h-8 rounded-lg bg-gray-100 dark:bg-gray-800 hover:bg-purple-500/10 dark:hover:bg-purple-500/20 group transition-all duration-200"
466
- >
467
- <div className="i-ph:arrow-left w-4 h-4 text-gray-500 dark:text-gray-400 group-hover:text-purple-500 transition-colors" />
468
- </button>
469
- ) : (
470
- <motion.div
471
- className="i-ph:lightning-fill w-5 h-5 text-purple-500"
472
- initial={{ rotate: -10 }}
473
- animate={{ rotate: 10 }}
474
- transition={{
475
- repeat: Infinity,
476
- repeatType: 'reverse',
477
- duration: 2,
478
- ease: 'easeInOut',
479
- }}
480
- />
481
- )}
482
- <DialogTitle className="text-xl font-semibold text-gray-900 dark:text-white">
483
- {showTabManagement ? 'Tab Management' : activeTab ? TAB_LABELS[activeTab] : 'Control Panel'}
484
- </DialogTitle>
485
- </div>
486
 
487
- <div className="flex items-center space-x-4">
488
- {/* Only show Manage Tabs button in developer mode */}
489
- {!activeTab && !showTabManagement && developerMode && (
490
- <motion.button
491
- onClick={() => setShowTabManagement(true)}
492
- className="flex items-center space-x-2 px-3 py-1.5 rounded-lg bg-gray-100 dark:bg-gray-800 hover:bg-purple-500/10 dark:hover:bg-purple-500/20 group transition-all duration-200"
493
- whileHover={{ scale: 1.05 }}
494
- whileTap={{ scale: 0.95 }}
495
- >
496
- <div className="i-ph:sliders-horizontal w-4 h-4 text-gray-500 dark:text-gray-400 group-hover:text-purple-500 transition-colors" />
497
- <span className="text-sm text-gray-500 dark:text-gray-400 group-hover:text-purple-500 transition-colors">
498
- Manage Tabs
499
- </span>
500
- </motion.button>
501
- )}
502
-
503
- <div className="flex items-center gap-2">
504
  <Switch
505
  id="developer-mode"
506
  checked={developerMode}
@@ -521,87 +362,98 @@ export const ControlPanel = ({ open, onClose }: ControlPanelProps) => {
521
  )}
522
  />
523
  </Switch>
524
- <label
525
- htmlFor="developer-mode"
526
- className="text-sm text-gray-500 dark:text-gray-400 select-none cursor-pointer"
527
- >
528
- {developerMode ? 'Developer Mode' : 'User Mode'}
529
- </label>
 
 
530
  </div>
 
531
 
532
- <button
533
- onClick={onClose}
534
- className="flex items-center justify-center w-8 h-8 rounded-lg bg-gray-100 dark:bg-gray-800 hover:bg-purple-500/10 dark:hover:bg-purple-500/20 group transition-all duration-200"
535
- >
536
- <div className="i-ph:x w-4 h-4 text-gray-500 dark:text-gray-400 group-hover:text-purple-500 transition-colors" />
537
- </button>
538
  </div>
539
- </div>
540
 
541
- {/* Content */}
542
- <div
543
- className={classNames(
544
- 'flex-1',
545
- 'overflow-y-auto',
546
- 'hover:overflow-y-auto',
547
- 'scrollbar scrollbar-w-2',
548
- 'scrollbar-track-transparent',
549
- 'scrollbar-thumb-[#E5E5E5] hover:scrollbar-thumb-[#CCCCCC]',
550
- 'dark:scrollbar-thumb-[#333333] dark:hover:scrollbar-thumb-[#444444]',
551
- 'will-change-scroll',
552
- 'touch-auto',
553
- )}
554
- >
555
- <motion.div
556
- key={activeTab || 'home'}
557
- initial={{ opacity: 0 }}
558
- animate={{ opacity: 1 }}
559
- exit={{ opacity: 0 }}
560
- transition={{ duration: 0.2 }}
561
- className="p-6"
562
  >
563
- {showTabManagement ? (
564
- <TabManagement />
565
- ) : activeTab ? (
566
- getTabComponent()
567
- ) : (
568
- <motion.div className="grid grid-cols-4 gap-4">
569
- <AnimatePresence mode="popLayout">
570
- {visibleTabs.map((tab: TabWithDevType, index: number) => (
571
- <motion.div
572
- key={tab.id}
573
- layout
574
- initial={{ opacity: 0, scale: 0.8, y: 20 }}
575
- animate={{ opacity: 1, scale: 1, y: 0 }}
576
- exit={{ opacity: 0, scale: 0.8, y: -20 }}
577
- transition={{
578
- duration: 0.2,
579
- delay: index * 0.05,
580
- }}
581
- >
582
- <DraggableTabTile
583
- tab={tab}
584
- index={index}
585
- moveTab={moveTab}
586
- onClick={() => handleTabClick(tab.id)}
587
- isActive={activeTab === tab.id}
588
- hasUpdate={getTabUpdateStatus(tab.id)}
589
- statusMessage={getStatusMessage(tab.id)}
590
- description={TAB_DESCRIPTIONS[tab.id]}
591
- isLoading={loadingTab === tab.id}
592
- />
593
- </motion.div>
594
- ))}
595
- </AnimatePresence>
596
- </motion.div>
597
- )}
598
- </motion.div>
599
  </div>
600
- </motion.div>
601
- </RadixDialog.Content>
602
- </div>
603
- </RadixDialog.Portal>
604
- </RadixDialog.Root>
605
- </DndProvider>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
606
  );
607
  };
 
3
  import { useStore } from '@nanostores/react';
4
  import { Switch } from '@radix-ui/react-switch';
5
  import * as RadixDialog from '@radix-ui/react-dialog';
 
 
6
  import { classNames } from '~/utils/classNames';
7
+ import { TabManagement } from '~/components/@settings/shared/components/TabManagement';
8
+ import { TabTile } from '~/components/@settings/shared/components/TabTile';
9
  import { useUpdateCheck } from '~/lib/hooks/useUpdateCheck';
10
  import { useFeatures } from '~/lib/hooks/useFeatures';
11
  import { useNotifications } from '~/lib/hooks/useNotifications';
12
  import { useConnectionStatus } from '~/lib/hooks/useConnectionStatus';
13
  import { useDebugStatus } from '~/lib/hooks/useDebugStatus';
14
  import { tabConfigurationStore, developerModeStore, setDeveloperMode } from '~/lib/stores/settings';
15
+ import { profileStore } from '~/lib/stores/profile';
16
+ import type { TabType, TabVisibilityConfig, DevTabConfig, Profile } from './types';
17
+ import { TAB_LABELS, DEFAULT_TAB_CONFIG } from './constants';
18
  import { resetTabConfiguration } from '~/lib/stores/settings';
19
  import { DialogTitle } from '~/components/ui/Dialog';
20
+ import { AvatarDropdown } from './AvatarDropdown';
21
 
22
  // Import all tab components
23
+ import ProfileTab from '~/components/@settings/tabs/profile/ProfileTab';
24
+ import SettingsTab from '~/components/@settings/tabs/settings/SettingsTab';
25
+ import NotificationsTab from '~/components/@settings/tabs/notifications/NotificationsTab';
26
+ import FeaturesTab from '~/components/@settings/tabs/features/FeaturesTab';
27
+ import DataTab from '~/components/@settings/tabs/data/DataTab';
28
+ import DebugTab from '~/components/@settings/tabs/debug/DebugTab';
29
+ import { EventLogsTab } from '~/components/@settings/tabs/event-logs/EventLogsTab';
30
+ import UpdateTab from '~/components/@settings/tabs/update/UpdateTab';
31
+ import ConnectionsTab from '~/components/@settings/tabs/connections/ConnectionsTab';
32
+ import CloudProvidersTab from '~/components/@settings/tabs/providers/cloud/CloudProvidersTab';
33
+ import ServiceStatusTab from '~/components/@settings/tabs/providers/status/ServiceStatusTab';
34
+ import LocalProvidersTab from '~/components/@settings/tabs/providers/local/LocalProvidersTab';
35
+ import TaskManagerTab from '~/components/@settings/tabs/task-manager/TaskManagerTab';
36
 
37
  interface ControlPanelProps {
38
  open: boolean;
 
57
  'event-logs': 'View system events and logs',
58
  update: 'Check for updates and release notes',
59
  'task-manager': 'Monitor system resources and processes',
60
+ 'tab-management': 'Configure visible tabs and their order',
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
61
  };
62
 
63
  export const ControlPanel = ({ open, onClose }: ControlPanelProps) => {
 
65
  const [activeTab, setActiveTab] = useState<TabType | null>(null);
66
  const [loadingTab, setLoadingTab] = useState<TabType | null>(null);
67
  const [showTabManagement, setShowTabManagement] = useState(false);
 
68
 
69
  // Store values
70
  const tabConfiguration = useStore(tabConfigurationStore);
71
  const developerMode = useStore(developerModeStore);
72
+ const profile = useStore(profileStore) as Profile;
73
 
74
  // Status hooks
75
  const { hasUpdate, currentVersion, acknowledgeUpdate } = useUpdateCheck();
 
78
  const { hasConnectionIssues, currentIssue, acknowledgeIssue } = useConnectionStatus();
79
  const { hasActiveWarnings, activeIssues, acknowledgeAllIssues } = useDebugStatus();
80
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
81
  // Add visibleTabs logic using useMemo
82
  const visibleTabs = useMemo(() => {
83
  if (!tabConfiguration?.userTabs || !Array.isArray(tabConfiguration.userTabs)) {
 
112
  };
113
  });
114
 
115
+ // Add Tab Management tile for developer mode
116
+ const tabManagementConfig: DevTabConfig = {
117
+ id: 'tab-management',
118
+ visible: true,
119
+ window: 'developer',
120
+ order: devTabs.length,
121
+ isExtraDevTab: true,
122
+ };
123
+ devTabs.push(tabManagementConfig);
124
+
125
  return devTabs.sort((a, b) => a.order - b.order);
126
  }
127
 
128
  // In user mode, only show visible user tabs
129
+ const notificationsDisabled = profile?.preferences?.notifications === false;
130
+
131
  return tabConfiguration.userTabs
132
  .filter((tab) => {
133
  if (!tab || typeof tab.id !== 'string') {
 
135
  return false;
136
  }
137
 
138
+ // Hide notifications tab if notifications are disabled in user preferences
139
+ if (tab.id === 'notifications' && notificationsDisabled) {
140
  return false;
141
  }
142
 
 
144
  return tab.visible && tab.window === 'user';
145
  })
146
  .sort((a, b) => a.order - b.order);
147
+ }, [tabConfiguration, developerMode, profile?.preferences?.notifications]);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
148
 
149
  // Handlers
150
  const handleBack = () => {
 
165
  console.log('Current developer mode:', developerMode);
166
  }, [developerMode]);
167
 
168
+ const getTabComponent = (tabId: TabType | 'tab-management') => {
169
+ if (tabId === 'tab-management') {
170
+ return <TabManagement />;
171
+ }
172
+
173
+ switch (tabId) {
174
  case 'profile':
175
  return <ProfileTab />;
176
  case 'settings':
 
247
  const handleTabClick = (tabId: TabType) => {
248
  setLoadingTab(tabId);
249
  setActiveTab(tabId);
250
+ setShowTabManagement(false);
251
 
252
  // Acknowledge notifications based on tab
253
  switch (tabId) {
 
273
  };
274
 
275
  return (
276
+ <RadixDialog.Root open={open}>
277
+ <RadixDialog.Portal>
278
+ <div className="fixed inset-0 flex items-center justify-center z-[100]">
279
+ <RadixDialog.Overlay asChild>
280
+ <motion.div
281
+ className="absolute inset-0 bg-black/50 backdrop-blur-sm"
282
+ initial={{ opacity: 0 }}
283
+ animate={{ opacity: 1 }}
284
+ exit={{ opacity: 0 }}
285
+ transition={{ duration: 0.2 }}
286
+ />
287
+ </RadixDialog.Overlay>
288
+
289
+ <RadixDialog.Content
290
+ aria-describedby={undefined}
291
+ onEscapeKeyDown={onClose}
292
+ onPointerDownOutside={onClose}
293
+ className="relative z-[101]"
294
+ >
295
+ <motion.div
296
+ className={classNames(
297
+ 'w-[1200px] h-[90vh]',
298
+ 'bg-[#FAFAFA] dark:bg-[#0A0A0A]',
299
+ 'rounded-2xl shadow-2xl',
300
+ 'border border-[#E5E5E5] dark:border-[#1A1A1A]',
301
+ 'flex flex-col overflow-hidden',
302
+ )}
303
+ initial={{ opacity: 0, scale: 0.95, y: 20 }}
304
+ animate={{ opacity: 1, scale: 1, y: 0 }}
305
+ exit={{ opacity: 0, scale: 0.95, y: 20 }}
306
+ transition={{ duration: 0.2 }}
307
  >
308
+ {/* Header */}
309
+ <div className="flex items-center justify-between px-6 py-4 border-b border-gray-200 dark:border-gray-700">
310
+ <div className="flex items-center space-x-4">
311
+ {activeTab || showTabManagement ? (
312
+ <button
313
+ onClick={handleBack}
314
+ className="flex items-center justify-center w-8 h-8 rounded-full bg-gray-100 dark:bg-gray-800 hover:bg-purple-500/10 dark:hover:bg-purple-500/20 group transition-all duration-200"
315
+ >
316
+ <div className="i-ph:arrow-left w-4 h-4 text-gray-500 dark:text-gray-400 group-hover:text-purple-500 transition-colors" />
317
+ </button>
318
+ ) : (
319
+ <motion.div
320
+ className="w-7 h-7"
321
+ initial={{ rotate: -5 }}
322
+ animate={{ rotate: 5 }}
323
+ transition={{
324
+ repeat: Infinity,
325
+ repeatType: 'reverse',
326
+ duration: 2,
327
+ ease: 'easeInOut',
328
+ }}
329
+ >
330
+ <div className="w-full h-full flex items-center justify-center bg-gray-100/50 dark:bg-gray-800/50 rounded-full">
331
+ <div className="i-ph:robot-fill w-5 h-5 text-gray-400 dark:text-gray-400 transition-colors" />
332
+ </div>
333
+ </motion.div>
334
+ )}
335
+ <DialogTitle className="text-xl font-semibold text-gray-900 dark:text-white">
336
+ {showTabManagement ? 'Tab Management' : activeTab ? TAB_LABELS[activeTab] : 'Control Panel'}
337
+ </DialogTitle>
338
+ </div>
 
 
 
 
 
 
 
 
 
339
 
340
+ <div className="flex items-center gap-6">
341
+ {/* Developer Mode Controls */}
342
+ <div className="flex items-center gap-6">
343
+ {/* Mode Toggle */}
344
+ <div className="flex items-center gap-2 min-w-[140px] border-r border-gray-200 dark:border-gray-800 pr-6">
 
 
 
 
 
 
 
 
 
 
 
 
345
  <Switch
346
  id="developer-mode"
347
  checked={developerMode}
 
362
  )}
363
  />
364
  </Switch>
365
+ <div className="flex items-center gap-2">
366
+ <label
367
+ htmlFor="developer-mode"
368
+ className="text-sm text-gray-500 dark:text-gray-400 select-none cursor-pointer whitespace-nowrap w-[88px]"
369
+ >
370
+ {developerMode ? 'Developer Mode' : 'User Mode'}
371
+ </label>
372
+ </div>
373
  </div>
374
+ </div>
375
 
376
+ {/* Avatar and Dropdown */}
377
+ <div className="border-l border-gray-200 dark:border-gray-800 pl-6">
378
+ <AvatarDropdown onSelectTab={handleTabClick} />
 
 
 
379
  </div>
 
380
 
381
+ {/* Close Button */}
382
+ <button
383
+ onClick={onClose}
384
+ className="flex items-center justify-center w-8 h-8 rounded-full bg-gray-100 dark:bg-gray-800 hover:bg-purple-500/10 dark:hover:bg-purple-500/20 group transition-all duration-200"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
385
  >
386
+ <div className="i-ph:x w-4 h-4 text-gray-500 dark:text-gray-400 group-hover:text-purple-500 transition-colors" />
387
+ </button>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
388
  </div>
389
+ </div>
390
+
391
+ {/* Content */}
392
+ <div
393
+ className={classNames(
394
+ 'flex-1',
395
+ 'overflow-y-auto',
396
+ 'hover:overflow-y-auto',
397
+ 'scrollbar scrollbar-w-2',
398
+ 'scrollbar-track-transparent',
399
+ 'scrollbar-thumb-[#E5E5E5] hover:scrollbar-thumb-[#CCCCCC]',
400
+ 'dark:scrollbar-thumb-[#333333] dark:hover:scrollbar-thumb-[#444444]',
401
+ 'will-change-scroll',
402
+ 'touch-auto',
403
+ )}
404
+ >
405
+ <motion.div
406
+ key={activeTab || 'home'}
407
+ initial={{ opacity: 0 }}
408
+ animate={{ opacity: 1 }}
409
+ exit={{ opacity: 0 }}
410
+ transition={{ duration: 0.2 }}
411
+ className="p-6"
412
+ >
413
+ {showTabManagement ? (
414
+ <TabManagement />
415
+ ) : activeTab ? (
416
+ getTabComponent(activeTab)
417
+ ) : (
418
+ <motion.div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 relative">
419
+ <AnimatePresence mode="popLayout">
420
+ {(visibleTabs as TabWithDevType[]).map((tab: TabWithDevType) => (
421
+ <motion.div
422
+ key={tab.id}
423
+ layout
424
+ initial={{ opacity: 0, scale: 0.8 }}
425
+ animate={{ opacity: 1, scale: 1 }}
426
+ exit={{ opacity: 0, scale: 0.8 }}
427
+ transition={{
428
+ type: 'spring',
429
+ stiffness: 400,
430
+ damping: 30,
431
+ mass: 0.8,
432
+ duration: 0.3,
433
+ }}
434
+ className="aspect-[1.5/1]"
435
+ >
436
+ <TabTile
437
+ tab={tab}
438
+ onClick={() => handleTabClick(tab.id as TabType)}
439
+ isActive={activeTab === tab.id}
440
+ hasUpdate={getTabUpdateStatus(tab.id)}
441
+ statusMessage={getStatusMessage(tab.id)}
442
+ description={TAB_DESCRIPTIONS[tab.id]}
443
+ isLoading={loadingTab === tab.id}
444
+ className="h-full"
445
+ />
446
+ </motion.div>
447
+ ))}
448
+ </AnimatePresence>
449
+ </motion.div>
450
+ )}
451
+ </motion.div>
452
+ </div>
453
+ </motion.div>
454
+ </RadixDialog.Content>
455
+ </div>
456
+ </RadixDialog.Portal>
457
+ </RadixDialog.Root>
458
  );
459
  };
app/components/@settings/core/constants.ts ADDED
@@ -0,0 +1,88 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { TabType } from './types';
2
+
3
+ export const TAB_ICONS: Record<TabType, string> = {
4
+ profile: 'i-ph:user-circle-fill',
5
+ settings: 'i-ph:gear-six-fill',
6
+ notifications: 'i-ph:bell-fill',
7
+ features: 'i-ph:star-fill',
8
+ data: 'i-ph:database-fill',
9
+ 'cloud-providers': 'i-ph:cloud-fill',
10
+ 'local-providers': 'i-ph:desktop-fill',
11
+ 'service-status': 'i-ph:activity-bold',
12
+ connection: 'i-ph:wifi-high-fill',
13
+ debug: 'i-ph:bug-fill',
14
+ 'event-logs': 'i-ph:list-bullets-fill',
15
+ update: 'i-ph:arrow-clockwise-fill',
16
+ 'task-manager': 'i-ph:chart-line-fill',
17
+ 'tab-management': 'i-ph:squares-four-fill',
18
+ };
19
+
20
+ export const TAB_LABELS: Record<TabType, string> = {
21
+ profile: 'Profile',
22
+ settings: 'Settings',
23
+ notifications: 'Notifications',
24
+ features: 'Features',
25
+ data: 'Data Management',
26
+ 'cloud-providers': 'Cloud Providers',
27
+ 'local-providers': 'Local Providers',
28
+ 'service-status': 'Service Status',
29
+ connection: 'Connection',
30
+ debug: 'Debug',
31
+ 'event-logs': 'Event Logs',
32
+ update: 'Updates',
33
+ 'task-manager': 'Task Manager',
34
+ 'tab-management': 'Tab Management',
35
+ };
36
+
37
+ export const TAB_DESCRIPTIONS: Record<TabType, string> = {
38
+ profile: 'Manage your profile and account settings',
39
+ settings: 'Configure application preferences',
40
+ notifications: 'View and manage your notifications',
41
+ features: 'Explore new and upcoming features',
42
+ data: 'Manage your data and storage',
43
+ 'cloud-providers': 'Configure cloud AI providers and models',
44
+ 'local-providers': 'Configure local AI providers and models',
45
+ 'service-status': 'Monitor cloud LLM service status',
46
+ connection: 'Check connection status and settings',
47
+ debug: 'Debug tools and system information',
48
+ 'event-logs': 'View system events and logs',
49
+ update: 'Check for updates and release notes',
50
+ 'task-manager': 'Monitor system resources and processes',
51
+ 'tab-management': 'Configure visible tabs and their order',
52
+ };
53
+
54
+ export const DEFAULT_TAB_CONFIG = [
55
+ // User Window Tabs (Always visible by default)
56
+ { id: 'features', visible: true, window: 'user' as const, order: 0 },
57
+ { id: 'data', visible: true, window: 'user' as const, order: 1 },
58
+ { id: 'cloud-providers', visible: true, window: 'user' as const, order: 2 },
59
+ { id: 'local-providers', visible: true, window: 'user' as const, order: 3 },
60
+ { id: 'connection', visible: true, window: 'user' as const, order: 4 },
61
+ { id: 'notifications', visible: true, window: 'user' as const, order: 5 },
62
+ { id: 'event-logs', visible: true, window: 'user' as const, order: 6 },
63
+
64
+ // User Window Tabs (In dropdown, initially hidden)
65
+ { id: 'profile', visible: false, window: 'user' as const, order: 7 },
66
+ { id: 'settings', visible: false, window: 'user' as const, order: 8 },
67
+ { id: 'task-manager', visible: false, window: 'user' as const, order: 9 },
68
+ { id: 'service-status', visible: false, window: 'user' as const, order: 10 },
69
+
70
+ // User Window Tabs (Hidden, controlled by TaskManagerTab)
71
+ { id: 'debug', visible: false, window: 'user' as const, order: 11 },
72
+ { id: 'update', visible: false, window: 'user' as const, order: 12 },
73
+
74
+ // Developer Window Tabs (All visible by default)
75
+ { id: 'features', visible: true, window: 'developer' as const, order: 0 },
76
+ { id: 'data', visible: true, window: 'developer' as const, order: 1 },
77
+ { id: 'cloud-providers', visible: true, window: 'developer' as const, order: 2 },
78
+ { id: 'local-providers', visible: true, window: 'developer' as const, order: 3 },
79
+ { id: 'connection', visible: true, window: 'developer' as const, order: 4 },
80
+ { id: 'notifications', visible: true, window: 'developer' as const, order: 5 },
81
+ { id: 'event-logs', visible: true, window: 'developer' as const, order: 6 },
82
+ { id: 'profile', visible: true, window: 'developer' as const, order: 7 },
83
+ { id: 'settings', visible: true, window: 'developer' as const, order: 8 },
84
+ { id: 'task-manager', visible: true, window: 'developer' as const, order: 9 },
85
+ { id: 'service-status', visible: true, window: 'developer' as const, order: 10 },
86
+ { id: 'debug', visible: true, window: 'developer' as const, order: 11 },
87
+ { id: 'update', visible: true, window: 'developer' as const, order: 12 },
88
+ ];
app/components/{settings/settings.types.ts β†’ @settings/core/types.ts} RENAMED
@@ -10,12 +10,13 @@ export type TabType =
10
  | 'data'
11
  | 'cloud-providers'
12
  | 'local-providers'
 
13
  | 'connection'
14
  | 'debug'
15
  | 'event-logs'
16
  | 'update'
17
  | 'task-manager'
18
- | 'service-status';
19
 
20
  export type WindowType = 'user' | 'developer';
21
 
@@ -46,14 +47,23 @@ export interface SettingItem {
46
  export interface TabVisibilityConfig {
47
  id: TabType;
48
  visible: boolean;
49
- window: 'user' | 'developer';
50
  order: number;
 
51
  locked?: boolean;
52
  }
53
 
 
 
 
 
 
 
 
 
54
  export interface TabWindowConfig {
55
- userTabs: TabVisibilityConfig[];
56
- developerTabs: TabVisibilityConfig[];
57
  }
58
 
59
  export const TAB_LABELS: Record<TabType, string> = {
@@ -61,47 +71,18 @@ export const TAB_LABELS: Record<TabType, string> = {
61
  settings: 'Settings',
62
  notifications: 'Notifications',
63
  features: 'Features',
64
- data: 'Data',
65
  'cloud-providers': 'Cloud Providers',
66
  'local-providers': 'Local Providers',
67
- connection: 'Connection',
 
68
  debug: 'Debug',
69
  'event-logs': 'Event Logs',
70
- update: 'Update',
71
  'task-manager': 'Task Manager',
72
- 'service-status': 'Service Status',
73
  };
74
 
75
- export const DEFAULT_TAB_CONFIG: TabVisibilityConfig[] = [
76
- // User Window Tabs (Visible by default)
77
- { id: 'features', visible: true, window: 'user', order: 0 },
78
- { id: 'data', visible: true, window: 'user', order: 1 },
79
- { id: 'cloud-providers', visible: true, window: 'user', order: 2 },
80
- { id: 'local-providers', visible: true, window: 'user', order: 3 },
81
- { id: 'connection', visible: true, window: 'user', order: 4 },
82
- { id: 'debug', visible: true, window: 'user', order: 5 },
83
-
84
- // User Window Tabs (Hidden by default)
85
- { id: 'profile', visible: false, window: 'user', order: 6 },
86
- { id: 'settings', visible: false, window: 'user', order: 7 },
87
- { id: 'notifications', visible: false, window: 'user', order: 8 },
88
- { id: 'event-logs', visible: false, window: 'user', order: 9 },
89
- { id: 'update', visible: false, window: 'user', order: 10 },
90
- { id: 'service-status', visible: false, window: 'user', order: 11 },
91
-
92
- // Developer Window Tabs (All visible by default)
93
- { id: 'features', visible: true, window: 'developer', order: 0 },
94
- { id: 'data', visible: true, window: 'developer', order: 1 },
95
- { id: 'cloud-providers', visible: true, window: 'developer', order: 2 },
96
- { id: 'local-providers', visible: true, window: 'developer', order: 3 },
97
- { id: 'connection', visible: true, window: 'developer', order: 4 },
98
- { id: 'debug', visible: true, window: 'developer', order: 5 },
99
- { id: 'task-manager', visible: true, window: 'developer', order: 6 },
100
- { id: 'settings', visible: true, window: 'developer', order: 7 },
101
- { id: 'notifications', visible: true, window: 'developer', order: 8 },
102
- { id: 'service-status', visible: true, window: 'developer', order: 9 },
103
- ];
104
-
105
  export const categoryLabels: Record<SettingCategory, string> = {
106
  profile: 'Profile & Account',
107
  file_sharing: 'File Sharing',
@@ -119,3 +100,15 @@ export const categoryIcons: Record<SettingCategory, string> = {
119
  services: 'i-ph:cube',
120
  preferences: 'i-ph:sliders',
121
  };
 
 
 
 
 
 
 
 
 
 
 
 
 
10
  | 'data'
11
  | 'cloud-providers'
12
  | 'local-providers'
13
+ | 'service-status'
14
  | 'connection'
15
  | 'debug'
16
  | 'event-logs'
17
  | 'update'
18
  | 'task-manager'
19
+ | 'tab-management';
20
 
21
  export type WindowType = 'user' | 'developer';
22
 
 
47
  export interface TabVisibilityConfig {
48
  id: TabType;
49
  visible: boolean;
50
+ window: WindowType;
51
  order: number;
52
+ isExtraDevTab?: boolean;
53
  locked?: boolean;
54
  }
55
 
56
+ export interface DevTabConfig extends TabVisibilityConfig {
57
+ window: 'developer';
58
+ }
59
+
60
+ export interface UserTabConfig extends TabVisibilityConfig {
61
+ window: 'user';
62
+ }
63
+
64
  export interface TabWindowConfig {
65
+ userTabs: UserTabConfig[];
66
+ developerTabs: DevTabConfig[];
67
  }
68
 
69
  export const TAB_LABELS: Record<TabType, string> = {
 
71
  settings: 'Settings',
72
  notifications: 'Notifications',
73
  features: 'Features',
74
+ data: 'Data Management',
75
  'cloud-providers': 'Cloud Providers',
76
  'local-providers': 'Local Providers',
77
+ 'service-status': 'Service Status',
78
+ connection: 'Connections',
79
  debug: 'Debug',
80
  'event-logs': 'Event Logs',
81
+ update: 'Updates',
82
  'task-manager': 'Task Manager',
83
+ 'tab-management': 'Tab Management',
84
  };
85
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
86
  export const categoryLabels: Record<SettingCategory, string> = {
87
  profile: 'Profile & Account',
88
  file_sharing: 'File Sharing',
 
100
  services: 'i-ph:cube',
101
  preferences: 'i-ph:sliders',
102
  };
103
+
104
+ export interface Profile {
105
+ username?: string;
106
+ bio?: string;
107
+ avatar?: string;
108
+ preferences?: {
109
+ notifications?: boolean;
110
+ theme?: 'light' | 'dark' | 'system';
111
+ language?: string;
112
+ timezone?: string;
113
+ };
114
+ }
app/components/@settings/index.ts ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Core exports
2
+ export { ControlPanel } from './core/ControlPanel';
3
+ export type { TabType, TabVisibilityConfig } from './core/types';
4
+
5
+ // Constants
6
+ export { TAB_LABELS, TAB_DESCRIPTIONS, DEFAULT_TAB_CONFIG } from './core/constants';
7
+
8
+ // Shared components
9
+ export { TabTile } from './shared/components/TabTile';
10
+ export { TabManagement } from './shared/components/TabManagement';
11
+
12
+ // Utils
13
+ export { getVisibleTabs, reorderTabs, resetToDefaultConfig } from './utils/tab-helpers';
14
+ export * from './utils/animations';
app/components/{settings/shared β†’ @settings/shared/components}/DraggableTabList.tsx RENAMED
@@ -1,8 +1,8 @@
1
  import { useDrag, useDrop } from 'react-dnd';
2
  import { motion } from 'framer-motion';
3
  import { classNames } from '~/utils/classNames';
4
- import type { TabVisibilityConfig } from '~/components/settings/settings.types';
5
- import { TAB_LABELS } from '~/components/settings/settings.types';
6
  import { Switch } from '~/components/ui/Switch';
7
 
8
  interface DraggableTabListProps {
 
1
  import { useDrag, useDrop } from 'react-dnd';
2
  import { motion } from 'framer-motion';
3
  import { classNames } from '~/utils/classNames';
4
+ import type { TabVisibilityConfig } from '~/components/@settings/core/types';
5
+ import { TAB_LABELS } from '~/components/@settings/core/types';
6
  import { Switch } from '~/components/ui/Switch';
7
 
8
  interface DraggableTabListProps {
app/components/@settings/shared/components/TabManagement.tsx ADDED
@@ -0,0 +1,259 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState } from 'react';
2
+ import { motion } from 'framer-motion';
3
+ import { useStore } from '@nanostores/react';
4
+ import { Switch } from '@radix-ui/react-switch';
5
+ import { classNames } from '~/utils/classNames';
6
+ import { tabConfigurationStore } from '~/lib/stores/settings';
7
+ import { TAB_LABELS } from '~/components/@settings/core/constants';
8
+ import type { TabType } from '~/components/@settings/core/types';
9
+ import { toast } from 'react-toastify';
10
+ import { TbLayoutGrid } from 'react-icons/tb';
11
+
12
+ // Define tab icons mapping
13
+ const TAB_ICONS: Record<TabType, string> = {
14
+ profile: 'i-ph:user-circle-fill',
15
+ settings: 'i-ph:gear-six-fill',
16
+ notifications: 'i-ph:bell-fill',
17
+ features: 'i-ph:star-fill',
18
+ data: 'i-ph:database-fill',
19
+ 'cloud-providers': 'i-ph:cloud-fill',
20
+ 'local-providers': 'i-ph:desktop-fill',
21
+ 'service-status': 'i-ph:activity-fill',
22
+ connection: 'i-ph:wifi-high-fill',
23
+ debug: 'i-ph:bug-fill',
24
+ 'event-logs': 'i-ph:list-bullets-fill',
25
+ update: 'i-ph:arrow-clockwise-fill',
26
+ 'task-manager': 'i-ph:chart-line-fill',
27
+ 'tab-management': 'i-ph:squares-four-fill',
28
+ };
29
+
30
+ // Define which tabs are default in user mode
31
+ const DEFAULT_USER_TABS: TabType[] = [
32
+ 'features',
33
+ 'data',
34
+ 'cloud-providers',
35
+ 'local-providers',
36
+ 'connection',
37
+ 'notifications',
38
+ 'event-logs',
39
+ ];
40
+
41
+ // Define which tabs can be added to user mode
42
+ const OPTIONAL_USER_TABS: TabType[] = ['profile', 'settings', 'task-manager', 'service-status', 'debug', 'update'];
43
+
44
+ // All available tabs for user mode
45
+ const ALL_USER_TABS = [...DEFAULT_USER_TABS, ...OPTIONAL_USER_TABS];
46
+
47
+ export const TabManagement = () => {
48
+ const [searchQuery, setSearchQuery] = useState('');
49
+ const tabConfiguration = useStore(tabConfigurationStore);
50
+
51
+ const handleTabVisibilityChange = (tabId: TabType, checked: boolean) => {
52
+ // Get current tab configuration
53
+ const currentTab = tabConfiguration.userTabs.find((tab) => tab.id === tabId);
54
+
55
+ // If tab doesn't exist in configuration, create it
56
+ if (!currentTab) {
57
+ const newTab = {
58
+ id: tabId,
59
+ visible: checked,
60
+ window: 'user' as const,
61
+ order: tabConfiguration.userTabs.length,
62
+ };
63
+
64
+ const updatedTabs = [...tabConfiguration.userTabs, newTab];
65
+
66
+ tabConfigurationStore.set({
67
+ ...tabConfiguration,
68
+ userTabs: updatedTabs,
69
+ });
70
+
71
+ toast.success(`Tab ${checked ? 'enabled' : 'disabled'} successfully`);
72
+
73
+ return;
74
+ }
75
+
76
+ // Check if tab can be enabled in user mode
77
+ const canBeEnabled = DEFAULT_USER_TABS.includes(tabId) || OPTIONAL_USER_TABS.includes(tabId);
78
+
79
+ if (!canBeEnabled && checked) {
80
+ toast.error('This tab cannot be enabled in user mode');
81
+ return;
82
+ }
83
+
84
+ // Update tab visibility
85
+ const updatedTabs = tabConfiguration.userTabs.map((tab) => {
86
+ if (tab.id === tabId) {
87
+ return { ...tab, visible: checked };
88
+ }
89
+
90
+ return tab;
91
+ });
92
+
93
+ // Update store
94
+ tabConfigurationStore.set({
95
+ ...tabConfiguration,
96
+ userTabs: updatedTabs,
97
+ });
98
+
99
+ // Show success message
100
+ toast.success(`Tab ${checked ? 'enabled' : 'disabled'} successfully`);
101
+ };
102
+
103
+ // Create a map of existing tab configurations
104
+ const tabConfigMap = new Map(tabConfiguration.userTabs.map((tab) => [tab.id, tab]));
105
+
106
+ // Generate the complete list of tabs, including those not in the configuration
107
+ const allTabs = ALL_USER_TABS.map((tabId) => {
108
+ return (
109
+ tabConfigMap.get(tabId) || {
110
+ id: tabId,
111
+ visible: false,
112
+ window: 'user' as const,
113
+ order: -1,
114
+ }
115
+ );
116
+ });
117
+
118
+ // Filter tabs based on search query
119
+ const filteredTabs = allTabs.filter((tab) => TAB_LABELS[tab.id].toLowerCase().includes(searchQuery.toLowerCase()));
120
+
121
+ return (
122
+ <div className="space-y-6">
123
+ <motion.div
124
+ className="space-y-4"
125
+ initial={{ opacity: 0, y: 20 }}
126
+ animate={{ opacity: 1, y: 0 }}
127
+ transition={{ duration: 0.3 }}
128
+ >
129
+ {/* Header */}
130
+ <div className="flex items-center justify-between gap-4 mt-8 mb-4">
131
+ <div className="flex items-center gap-2">
132
+ <div
133
+ className={classNames(
134
+ 'w-8 h-8 flex items-center justify-center rounded-lg',
135
+ 'bg-bolt-elements-background-depth-3',
136
+ 'text-purple-500',
137
+ )}
138
+ >
139
+ <TbLayoutGrid className="w-5 h-5" />
140
+ </div>
141
+ <div>
142
+ <h4 className="text-md font-medium text-bolt-elements-textPrimary">Tab Management</h4>
143
+ <p className="text-sm text-bolt-elements-textSecondary">Configure visible tabs and their order</p>
144
+ </div>
145
+ </div>
146
+
147
+ {/* Search */}
148
+ <div className="relative w-64">
149
+ <div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
150
+ <div className="i-ph:magnifying-glass w-4 h-4 text-gray-400" />
151
+ </div>
152
+ <input
153
+ type="text"
154
+ value={searchQuery}
155
+ onChange={(e) => setSearchQuery(e.target.value)}
156
+ placeholder="Search tabs..."
157
+ className={classNames(
158
+ 'w-full pl-10 pr-4 py-2 rounded-lg',
159
+ 'bg-bolt-elements-background-depth-2',
160
+ 'border border-bolt-elements-borderColor',
161
+ 'text-bolt-elements-textPrimary',
162
+ 'placeholder-bolt-elements-textTertiary',
163
+ 'focus:outline-none focus:ring-2 focus:ring-purple-500/30',
164
+ 'transition-all duration-200',
165
+ )}
166
+ />
167
+ </div>
168
+ </div>
169
+
170
+ {/* Tab Grid */}
171
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
172
+ {filteredTabs.map((tab, index) => (
173
+ <motion.div
174
+ key={tab.id}
175
+ className={classNames(
176
+ 'rounded-lg border bg-bolt-elements-background text-bolt-elements-textPrimary',
177
+ 'bg-bolt-elements-background-depth-2',
178
+ 'hover:bg-bolt-elements-background-depth-3',
179
+ 'transition-all duration-200',
180
+ 'relative overflow-hidden group',
181
+ )}
182
+ initial={{ opacity: 0, y: 20 }}
183
+ animate={{ opacity: 1, y: 0 }}
184
+ transition={{ delay: index * 0.1 }}
185
+ whileHover={{ scale: 1.02 }}
186
+ >
187
+ {/* Status Badges */}
188
+ <div className="absolute top-2 right-2 flex gap-1">
189
+ {DEFAULT_USER_TABS.includes(tab.id) && (
190
+ <span className="px-2 py-0.5 text-xs rounded-full bg-purple-500/10 text-purple-500 font-medium">
191
+ Default
192
+ </span>
193
+ )}
194
+ {OPTIONAL_USER_TABS.includes(tab.id) && (
195
+ <span className="px-2 py-0.5 text-xs rounded-full bg-blue-500/10 text-blue-500 font-medium">
196
+ Optional
197
+ </span>
198
+ )}
199
+ </div>
200
+
201
+ <div className="flex items-start gap-4 p-4">
202
+ <motion.div
203
+ className={classNames(
204
+ 'w-10 h-10 flex items-center justify-center rounded-xl',
205
+ 'bg-bolt-elements-background-depth-3 group-hover:bg-bolt-elements-background-depth-4',
206
+ 'transition-all duration-200',
207
+ tab.visible ? 'text-purple-500' : 'text-bolt-elements-textSecondary',
208
+ )}
209
+ whileHover={{ scale: 1.1 }}
210
+ whileTap={{ scale: 0.9 }}
211
+ >
212
+ <div className={classNames('w-6 h-6', 'transition-transform duration-200', 'group-hover:rotate-12')}>
213
+ <div className={classNames(TAB_ICONS[tab.id], 'w-full h-full')} />
214
+ </div>
215
+ </motion.div>
216
+
217
+ <div className="flex-1 min-w-0">
218
+ <div className="flex items-center justify-between gap-4">
219
+ <div>
220
+ <h4 className="text-sm font-medium text-bolt-elements-textPrimary group-hover:text-purple-500 transition-colors">
221
+ {TAB_LABELS[tab.id]}
222
+ </h4>
223
+ <p className="text-xs text-bolt-elements-textSecondary mt-0.5">
224
+ {tab.visible ? 'Visible in user mode' : 'Hidden in user mode'}
225
+ </p>
226
+ </div>
227
+ <Switch
228
+ checked={tab.visible}
229
+ onCheckedChange={(checked) => handleTabVisibilityChange(tab.id, checked)}
230
+ disabled={!DEFAULT_USER_TABS.includes(tab.id) && !OPTIONAL_USER_TABS.includes(tab.id)}
231
+ className={classNames(
232
+ 'relative inline-flex h-5 w-9 items-center rounded-full',
233
+ 'transition-colors duration-200',
234
+ tab.visible ? 'bg-purple-500' : 'bg-bolt-elements-background-depth-4',
235
+ {
236
+ 'opacity-50 cursor-not-allowed':
237
+ !DEFAULT_USER_TABS.includes(tab.id) && !OPTIONAL_USER_TABS.includes(tab.id),
238
+ },
239
+ )}
240
+ />
241
+ </div>
242
+ </div>
243
+ </div>
244
+
245
+ <motion.div
246
+ className="absolute inset-0 border-2 border-purple-500/0 rounded-lg pointer-events-none"
247
+ animate={{
248
+ borderColor: tab.visible ? 'rgba(168, 85, 247, 0.2)' : 'rgba(168, 85, 247, 0)',
249
+ scale: tab.visible ? 1 : 0.98,
250
+ }}
251
+ transition={{ duration: 0.2 }}
252
+ />
253
+ </motion.div>
254
+ ))}
255
+ </div>
256
+ </motion.div>
257
+ </div>
258
+ );
259
+ };
app/components/{settings/shared β†’ @settings/shared/components}/TabTile.tsx RENAMED
@@ -1,134 +1,104 @@
1
  import { motion } from 'framer-motion';
2
  import * as Tooltip from '@radix-ui/react-tooltip';
3
  import { classNames } from '~/utils/classNames';
4
- import type { TabVisibilityConfig } from '~/components/settings/settings.types';
5
- import { TAB_LABELS } from '~/components/settings/settings.types';
6
-
7
- const TAB_ICONS = {
8
- profile: 'i-ph:user',
9
- settings: 'i-ph:gear',
10
- notifications: 'i-ph:bell',
11
- features: 'i-ph:star',
12
- data: 'i-ph:database',
13
- providers: 'i-ph:plug',
14
- connection: 'i-ph:wifi-high',
15
- debug: 'i-ph:bug',
16
- 'event-logs': 'i-ph:list-bullets',
17
- update: 'i-ph:arrow-clockwise',
18
- 'task-manager': 'i-ph:activity',
19
- 'cloud-providers': 'i-ph:cloud',
20
- 'local-providers': 'i-ph:desktop',
21
- 'service-status': 'i-ph:activity-bold',
22
- };
23
 
24
  interface TabTileProps {
25
  tab: TabVisibilityConfig;
26
- onClick: () => void;
27
  isActive?: boolean;
28
  hasUpdate?: boolean;
29
  statusMessage?: string;
30
  description?: string;
31
  isLoading?: boolean;
 
32
  }
33
 
34
  export const TabTile = ({
35
  tab,
36
  onClick,
37
- isActive = false,
38
- hasUpdate = false,
39
  statusMessage,
40
  description,
41
- isLoading = false,
 
42
  }: TabTileProps) => {
43
  return (
44
  <Tooltip.Provider delayDuration={200}>
45
  <Tooltip.Root>
46
  <Tooltip.Trigger asChild>
47
- <motion.button
48
  onClick={onClick}
49
- disabled={isLoading}
50
  className={classNames(
51
- 'relative flex flex-col items-center justify-center gap-3 p-6 rounded-xl',
52
  'w-full h-full min-h-[160px]',
53
-
54
- // Background and border styles
55
  'bg-white dark:bg-[#141414]',
56
- 'border border-[#E5E5E5]/50 dark:border-[#333333]/50',
57
-
58
- // Shadow and glass effect
59
- 'shadow-sm',
60
- 'dark:shadow-[0_0_15px_rgba(0,0,0,0.1)]',
61
- 'dark:bg-opacity-50',
62
-
63
- // Hover effects
64
- 'hover:border-purple-500/30 dark:hover:border-purple-500/30',
65
- 'hover:bg-gradient-to-br hover:from-purple-50/50 hover:to-white dark:hover:from-purple-500/5 dark:hover:to-[#141414]',
66
- 'hover:shadow-md hover:shadow-purple-500/5',
67
- 'dark:hover:shadow-purple-500/10',
68
-
69
- // Focus states for keyboard navigation
70
- 'focus:outline-none',
71
- 'focus:ring-2 focus:ring-purple-500/50 focus:ring-offset-2',
72
- 'dark:focus:ring-offset-[#141414]',
73
- 'focus:border-purple-500/30',
74
-
75
- // Active state
76
- isActive
77
- ? [
78
- 'border-purple-500/50 dark:border-purple-500/50',
79
- 'bg-gradient-to-br from-purple-50 to-white dark:from-purple-500/10 dark:to-[#141414]',
80
- 'shadow-md shadow-purple-500/10',
81
- ]
82
- : '',
83
-
84
- // Loading state
85
- isLoading ? 'cursor-wait opacity-70' : '',
86
-
87
- // Transitions
88
- 'transition-all duration-300 ease-out',
89
  'group',
 
 
 
 
 
90
  )}
91
- whileHover={
92
- !isLoading
93
- ? {
94
- scale: 1.02,
95
- transition: { duration: 0.2, ease: 'easeOut' },
96
- }
97
- : {}
98
- }
99
- whileTap={
100
- !isLoading
101
- ? {
102
- scale: 0.98,
103
- transition: { duration: 0.1, ease: 'easeIn' },
104
- }
105
- : {}
106
- }
107
  >
108
- {/* Loading Overlay */}
109
- {isLoading && (
 
110
  <motion.div
111
  className={classNames(
112
- 'absolute inset-0 rounded-xl z-10',
113
- 'bg-white/50 dark:bg-black/50',
114
- 'backdrop-blur-sm',
115
  'flex items-center justify-center',
 
 
 
 
 
 
116
  )}
117
- initial={{ opacity: 0 }}
118
- animate={{ opacity: 1 }}
119
- transition={{ duration: 0.2 }}
120
  >
121
  <motion.div
122
- className={classNames('w-8 h-8 rounded-full', 'border-2 border-purple-500/30', 'border-t-purple-500')}
123
- animate={{ rotate: 360 }}
124
- transition={{
125
- duration: 1,
126
- repeat: Infinity,
127
- ease: 'linear',
128
- }}
129
  />
130
  </motion.div>
131
- )}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
132
 
133
  {/* Status Indicator */}
134
  {hasUpdate && (
@@ -136,9 +106,8 @@ export const TabTile = ({
136
  className={classNames(
137
  'absolute top-3 right-3',
138
  'w-2.5 h-2.5 rounded-full',
139
- 'bg-green-500',
140
- 'shadow-lg shadow-green-500/20',
141
- 'ring-4 ring-green-500/20',
142
  )}
143
  initial={{ scale: 0 }}
144
  animate={{ scale: 1 }}
@@ -146,70 +115,30 @@ export const TabTile = ({
146
  />
147
  )}
148
 
149
- {/* Background glow effect */}
150
- <div
151
- className={classNames(
152
- 'absolute inset-0 rounded-xl opacity-0 group-hover:opacity-100',
153
- 'bg-gradient-to-br from-purple-500/5 to-transparent dark:from-purple-500/10',
154
- 'transition-opacity duration-300',
155
- isActive ? 'opacity-100' : '',
156
- )}
157
- />
158
-
159
- {/* Icon */}
160
- <div
161
- className={classNames(
162
- TAB_ICONS[tab.id],
163
- 'w-12 h-12',
164
- 'relative',
165
- 'text-gray-600 dark:text-gray-300',
166
- 'group-hover:text-purple-500 dark:group-hover:text-purple-400',
167
- 'transition-all duration-300',
168
- isActive ? 'text-purple-500 dark:text-purple-400 scale-110' : '',
169
- )}
170
- />
171
-
172
- {/* Label and Description */}
173
- <div className="relative flex flex-col items-center text-center">
174
- <div
175
  className={classNames(
176
- 'text-base font-medium',
177
- 'text-gray-700 dark:text-gray-200',
178
- 'group-hover:text-purple-500 dark:group-hover:text-purple-400',
179
- 'transition-colors duration-300',
180
- isActive ? 'text-purple-500 dark:text-purple-400' : '',
181
  )}
 
 
 
182
  >
183
- {TAB_LABELS[tab.id]}
184
- </div>
185
- {description && (
186
- <div
187
- className={classNames(
188
- 'text-xs mt-1',
189
- 'text-gray-500 dark:text-gray-400',
190
- 'group-hover:text-purple-400/70 dark:group-hover:text-purple-300/70',
191
- 'transition-colors duration-300',
192
- 'max-w-[180px]',
193
- isActive ? 'text-purple-400/70 dark:text-purple-300/70' : '',
194
- )}
195
- >
196
- {description}
197
- </div>
198
- )}
199
- </div>
200
-
201
- {/* Bottom indicator line */}
202
- <div
203
- className={classNames(
204
- 'absolute bottom-0 left-1/2 -translate-x-1/2',
205
- 'w-12 h-0.5 rounded-full',
206
- 'bg-purple-500/0 group-hover:bg-purple-500/50',
207
- 'transition-all duration-300 ease-out',
208
- 'transform scale-x-0 group-hover:scale-x-100',
209
- isActive ? 'bg-purple-500 scale-x-100' : '',
210
- )}
211
- />
212
- </motion.button>
213
  </Tooltip.Trigger>
214
  <Tooltip.Portal>
215
  <Tooltip.Content
@@ -217,7 +146,6 @@ export const TabTile = ({
217
  'px-3 py-1.5 rounded-lg',
218
  'bg-[#18181B] text-white',
219
  'text-sm font-medium',
220
- 'shadow-xl',
221
  'select-none',
222
  'z-[100]',
223
  )}
 
1
  import { motion } from 'framer-motion';
2
  import * as Tooltip from '@radix-ui/react-tooltip';
3
  import { classNames } from '~/utils/classNames';
4
+ import type { TabVisibilityConfig } from '~/components/@settings/core/types';
5
+ import { TAB_LABELS, TAB_ICONS } from '~/components/@settings/core/constants';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6
 
7
  interface TabTileProps {
8
  tab: TabVisibilityConfig;
9
+ onClick?: () => void;
10
  isActive?: boolean;
11
  hasUpdate?: boolean;
12
  statusMessage?: string;
13
  description?: string;
14
  isLoading?: boolean;
15
+ className?: string;
16
  }
17
 
18
  export const TabTile = ({
19
  tab,
20
  onClick,
21
+ isActive,
22
+ hasUpdate,
23
  statusMessage,
24
  description,
25
+ isLoading,
26
+ className,
27
  }: TabTileProps) => {
28
  return (
29
  <Tooltip.Provider delayDuration={200}>
30
  <Tooltip.Root>
31
  <Tooltip.Trigger asChild>
32
+ <motion.div
33
  onClick={onClick}
 
34
  className={classNames(
35
+ 'relative flex flex-col items-center p-6 rounded-xl',
36
  'w-full h-full min-h-[160px]',
 
 
37
  'bg-white dark:bg-[#141414]',
38
+ 'border border-[#E5E5E5] dark:border-[#333333]',
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
39
  'group',
40
+ 'hover:bg-purple-50 dark:hover:bg-[#1a1a1a]',
41
+ 'hover:border-purple-200 dark:hover:border-purple-900/30',
42
+ isActive ? 'border-purple-500 dark:border-purple-500/50 bg-purple-500/5 dark:bg-purple-500/10' : '',
43
+ isLoading ? 'cursor-wait opacity-70' : '',
44
+ className || '',
45
  )}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
46
  >
47
+ {/* Main Content */}
48
+ <div className="flex flex-col items-center justify-center flex-1 w-full">
49
+ {/* Icon */}
50
  <motion.div
51
  className={classNames(
52
+ 'relative',
53
+ 'w-14 h-14',
 
54
  'flex items-center justify-center',
55
+ 'rounded-xl',
56
+ 'bg-gray-100 dark:bg-gray-800',
57
+ 'ring-1 ring-gray-200 dark:ring-gray-700',
58
+ 'group-hover:bg-purple-100 dark:group-hover:bg-gray-700/80',
59
+ 'group-hover:ring-purple-200 dark:group-hover:ring-purple-800/30',
60
+ isActive ? 'bg-purple-500/10 dark:bg-purple-500/10 ring-purple-500/30 dark:ring-purple-500/20' : '',
61
  )}
 
 
 
62
  >
63
  <motion.div
64
+ className={classNames(
65
+ TAB_ICONS[tab.id],
66
+ 'w-8 h-8',
67
+ 'text-gray-600 dark:text-gray-300',
68
+ 'group-hover:text-purple-500 dark:group-hover:text-purple-400/80',
69
+ isActive ? 'text-purple-500 dark:text-purple-400/90' : '',
70
+ )}
71
  />
72
  </motion.div>
73
+
74
+ {/* Label and Description */}
75
+ <div className="flex flex-col items-center mt-5 w-full">
76
+ <h3
77
+ className={classNames(
78
+ 'text-[15px] font-medium leading-snug mb-2',
79
+ 'text-gray-700 dark:text-gray-200',
80
+ 'group-hover:text-purple-600 dark:group-hover:text-purple-300/90',
81
+ isActive ? 'text-purple-500 dark:text-purple-400/90' : '',
82
+ )}
83
+ >
84
+ {TAB_LABELS[tab.id]}
85
+ </h3>
86
+ {description && (
87
+ <p
88
+ className={classNames(
89
+ 'text-[13px] leading-relaxed',
90
+ 'text-gray-500 dark:text-gray-400',
91
+ 'max-w-[85%]',
92
+ 'text-center',
93
+ 'group-hover:text-purple-500 dark:group-hover:text-purple-400/70',
94
+ isActive ? 'text-purple-400 dark:text-purple-400/80' : '',
95
+ )}
96
+ >
97
+ {description}
98
+ </p>
99
+ )}
100
+ </div>
101
+ </div>
102
 
103
  {/* Status Indicator */}
104
  {hasUpdate && (
 
106
  className={classNames(
107
  'absolute top-3 right-3',
108
  'w-2.5 h-2.5 rounded-full',
109
+ 'bg-purple-500',
110
+ 'ring-4 ring-purple-500',
 
111
  )}
112
  initial={{ scale: 0 }}
113
  animate={{ scale: 1 }}
 
115
  />
116
  )}
117
 
118
+ {/* Loading Overlay */}
119
+ {isLoading && (
120
+ <motion.div
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
121
  className={classNames(
122
+ 'absolute inset-0 rounded-xl z-10',
123
+ 'bg-white dark:bg-black',
124
+ 'flex items-center justify-center',
 
 
125
  )}
126
+ initial={{ opacity: 0 }}
127
+ animate={{ opacity: 1 }}
128
+ transition={{ duration: 0.2 }}
129
  >
130
+ <motion.div
131
+ className={classNames('w-8 h-8 rounded-full', 'border-2 border-purple-500', 'border-t-purple-500')}
132
+ animate={{ rotate: 360 }}
133
+ transition={{
134
+ duration: 1,
135
+ repeat: Infinity,
136
+ ease: 'linear',
137
+ }}
138
+ />
139
+ </motion.div>
140
+ )}
141
+ </motion.div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
142
  </Tooltip.Trigger>
143
  <Tooltip.Portal>
144
  <Tooltip.Content
 
146
  'px-3 py-1.5 rounded-lg',
147
  'bg-[#18181B] text-white',
148
  'text-sm font-medium',
 
149
  'select-none',
150
  'z-[100]',
151
  )}
app/components/{settings β†’ @settings/tabs}/connections/ConnectionsTab.tsx RENAMED
File without changes
app/components/{settings β†’ @settings/tabs}/connections/components/ConnectionForm.tsx RENAMED
@@ -1,6 +1,6 @@
1
  import React, { useEffect } from 'react';
2
  import { classNames } from '~/utils/classNames';
3
- import type { GitHubAuthState } from '~/components/settings/connections/types/GitHub';
4
  import Cookies from 'js-cookie';
5
  import { getLocalStorage } from '~/lib/persistence';
6
 
 
1
  import React, { useEffect } from 'react';
2
  import { classNames } from '~/utils/classNames';
3
+ import type { GitHubAuthState } from '~/components/@settings/tabs/connections/types/GitHub';
4
  import Cookies from 'js-cookie';
5
  import { getLocalStorage } from '~/lib/persistence';
6
 
app/components/{settings β†’ @settings/tabs}/connections/components/CreateBranchDialog.tsx RENAMED
@@ -1,7 +1,7 @@
1
  import { useState } from 'react';
2
  import * as Dialog from '@radix-ui/react-dialog';
3
  import { classNames } from '~/utils/classNames';
4
- import type { GitHubRepoInfo } from '~/components/settings/connections/types/GitHub';
5
  import { GitBranch } from '@phosphor-icons/react';
6
 
7
  interface GitHubBranch {
 
1
  import { useState } from 'react';
2
  import * as Dialog from '@radix-ui/react-dialog';
3
  import { classNames } from '~/utils/classNames';
4
+ import type { GitHubRepoInfo } from '~/components/@settings/tabs/connections/types/GitHub';
5
  import { GitBranch } from '@phosphor-icons/react';
6
 
7
  interface GitHubBranch {
app/components/{settings β†’ @settings/tabs}/connections/components/PushToGitHubDialog.tsx RENAMED
File without changes
app/components/{settings β†’ @settings/tabs}/connections/components/RepositorySelectionDialog.tsx RENAMED
File without changes
app/components/{settings β†’ @settings/tabs}/connections/types/GitHub.ts RENAMED
File without changes
app/components/{settings β†’ @settings/tabs}/data/DataTab.tsx RENAMED
File without changes
app/components/{settings β†’ @settings/tabs}/debug/DebugTab.tsx RENAMED
@@ -131,6 +131,13 @@ interface WebAppInfo {
131
  gitInfo: GitInfo;
132
  }
133
 
 
 
 
 
 
 
 
134
  const DependencySection = ({
135
  title,
136
  deps,
@@ -146,7 +153,17 @@ const DependencySection = ({
146
 
147
  return (
148
  <Collapsible open={isOpen} onOpenChange={setIsOpen}>
149
- <CollapsibleTrigger className="flex w-full items-center justify-between p-4 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-lg">
 
 
 
 
 
 
 
 
 
 
150
  <div className="flex items-center gap-3">
151
  <div className="i-ph:package text-bolt-elements-textSecondary w-4 h-4" />
152
  <span className="text-base text-bolt-elements-textPrimary">
@@ -157,15 +174,22 @@ const DependencySection = ({
157
  <span className="text-sm text-bolt-elements-textSecondary">{isOpen ? 'Hide' : 'Show'}</span>
158
  <div
159
  className={classNames(
160
- 'i-ph:caret-down w-4 h-4 transform transition-transform duration-200',
161
  isOpen ? 'rotate-180' : '',
162
  )}
163
  />
164
  </div>
165
  </CollapsibleTrigger>
166
  <CollapsibleContent>
167
- <ScrollArea className="h-[200px] w-full p-4">
168
- <div className="space-y-2 pl-7">
 
 
 
 
 
 
 
169
  {deps.map((dep) => (
170
  <div key={dep.name} className="flex items-center justify-between text-sm">
171
  <span className="text-bolt-elements-textPrimary">{dep.name}</span>
@@ -182,6 +206,10 @@ const DependencySection = ({
182
  export default function DebugTab() {
183
  const [systemInfo, setSystemInfo] = useState<SystemInfo | null>(null);
184
  const [webAppInfo, setWebAppInfo] = useState<WebAppInfo | null>(null);
 
 
 
 
185
  const [loading, setLoading] = useState({
186
  systemInfo: false,
187
  webAppInfo: false,
@@ -259,7 +287,8 @@ export default function DebugTab() {
259
  return undefined;
260
  }
261
 
262
- const interval = setInterval(async () => {
 
263
  try {
264
  const response = await fetch('/api/system/git-info');
265
  const updatedGitInfo = (await response.json()) as GitInfo;
@@ -269,21 +298,27 @@ export default function DebugTab() {
269
  return null;
270
  }
271
 
 
 
 
 
 
272
  return {
273
  ...prev,
274
  gitInfo: updatedGitInfo,
275
  };
276
  });
277
  } catch (error) {
278
- console.error('Failed to refresh git info:', error);
279
  }
280
- }, 5000);
281
-
282
- const cleanup = () => {
283
- clearInterval(interval);
284
  };
285
 
286
- return cleanup;
 
 
 
 
 
287
  }, [openSections.webapp]);
288
 
289
  const getSystemInfo = async () => {
@@ -616,11 +651,68 @@ export default function DebugTab() {
616
  }
617
  };
618
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
619
  return (
620
  <div className="flex flex-col gap-6 max-w-7xl mx-auto p-4">
621
  {/* Quick Stats Banner */}
622
  <div className="grid grid-cols-1 md:grid-cols-4 gap-4">
623
- <div className="p-4 rounded-xl bg-gradient-to-br from-purple-500/10 to-purple-500/5 border border-purple-500/20">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
624
  <div className="text-sm text-bolt-elements-textSecondary">Memory Usage</div>
625
  <div className="text-2xl font-semibold text-bolt-elements-textPrimary mt-1">
626
  {systemInfo?.memory.percentage}%
@@ -628,7 +720,7 @@ export default function DebugTab() {
628
  <Progress value={systemInfo?.memory.percentage || 0} className="mt-2" />
629
  </div>
630
 
631
- <div className="p-4 rounded-xl bg-gradient-to-br from-blue-500/10 to-blue-500/5 border border-blue-500/20">
632
  <div className="text-sm text-bolt-elements-textSecondary">Page Load Time</div>
633
  <div className="text-2xl font-semibold text-bolt-elements-textPrimary mt-1">
634
  {systemInfo ? (systemInfo.performance.timing.loadTime / 1000).toFixed(2) + 's' : '-'}
@@ -638,7 +730,7 @@ export default function DebugTab() {
638
  </div>
639
  </div>
640
 
641
- <div className="p-4 rounded-xl bg-gradient-to-br from-green-500/10 to-green-500/5 border border-green-500/20">
642
  <div className="text-sm text-bolt-elements-textSecondary">Network Speed</div>
643
  <div className="text-2xl font-semibold text-bolt-elements-textPrimary mt-1">
644
  {systemInfo?.network.downlink || '-'} Mbps
@@ -646,7 +738,7 @@ export default function DebugTab() {
646
  <div className="text-xs text-bolt-elements-textSecondary mt-2">RTT: {systemInfo?.network.rtt || '-'} ms</div>
647
  </div>
648
 
649
- <div className="p-4 rounded-xl bg-gradient-to-br from-red-500/10 to-red-500/5 border border-red-500/20">
650
  <div className="text-sm text-bolt-elements-textSecondary">Errors</div>
651
  <div className="text-2xl font-semibold text-bolt-elements-textPrimary mt-1">{errorLogs.length}</div>
652
  </div>
@@ -659,10 +751,11 @@ export default function DebugTab() {
659
  disabled={loading.systemInfo}
660
  className={classNames(
661
  'flex items-center gap-2 px-4 py-2 text-sm font-medium rounded-lg transition-colors',
662
- 'bg-[#F5F5F5] hover:bg-purple-500/10 hover:text-purple-500',
663
- 'dark:bg-[#1A1A1A] dark:hover:bg-purple-500/20',
664
- 'text-bolt-elements-textPrimary dark:hover:text-purple-500',
665
- 'focus:outline-none focus:ring-2 focus:ring-purple-500 focus:ring-offset-2 dark:focus:ring-offset-[#0A0A0A]',
 
666
  { 'opacity-50 cursor-not-allowed': loading.systemInfo },
667
  )}
668
  >
@@ -679,10 +772,11 @@ export default function DebugTab() {
679
  disabled={loading.performance}
680
  className={classNames(
681
  'flex items-center gap-2 px-4 py-2 text-sm font-medium rounded-lg transition-colors',
682
- 'bg-[#F5F5F5] hover:bg-purple-500/10 hover:text-purple-500',
683
- 'dark:bg-[#1A1A1A] dark:hover:bg-purple-500/20',
684
- 'text-bolt-elements-textPrimary dark:hover:text-purple-500',
685
- 'focus:outline-none focus:ring-2 focus:ring-purple-500 focus:ring-offset-2 dark:focus:ring-offset-[#0A0A0A]',
 
686
  { 'opacity-50 cursor-not-allowed': loading.performance },
687
  )}
688
  >
@@ -699,10 +793,11 @@ export default function DebugTab() {
699
  disabled={loading.errors}
700
  className={classNames(
701
  'flex items-center gap-2 px-4 py-2 text-sm font-medium rounded-lg transition-colors',
702
- 'bg-[#F5F5F5] hover:bg-purple-500/10 hover:text-purple-500',
703
- 'dark:bg-[#1A1A1A] dark:hover:bg-purple-500/20',
704
- 'text-bolt-elements-textPrimary dark:hover:text-purple-500',
705
- 'focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2 dark:focus:ring-offset-[#0A0A0A]',
 
706
  { 'opacity-50 cursor-not-allowed': loading.errors },
707
  )}
708
  >
@@ -719,10 +814,11 @@ export default function DebugTab() {
719
  disabled={loading.webAppInfo}
720
  className={classNames(
721
  'flex items-center gap-2 px-4 py-2 text-sm font-medium rounded-lg transition-colors',
722
- 'bg-[#F5F5F5] hover:bg-purple-500/10 hover:text-purple-500',
723
- 'dark:bg-[#1A1A1A] dark:hover:bg-purple-500/20',
724
- 'text-bolt-elements-textPrimary dark:hover:text-purple-500',
725
- 'focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 dark:focus:ring-offset-[#0A0A0A]',
 
726
  { 'opacity-50 cursor-not-allowed': loading.webAppInfo },
727
  )}
728
  >
@@ -738,10 +834,11 @@ export default function DebugTab() {
738
  onClick={exportDebugInfo}
739
  className={classNames(
740
  'flex items-center gap-2 px-4 py-2 text-sm font-medium rounded-lg transition-colors',
741
- 'bg-[#F5F5F5] hover:bg-purple-500/10 hover:text-purple-500',
742
- 'dark:bg-[#1A1A1A] dark:hover:bg-purple-500/20',
743
- 'text-bolt-elements-textPrimary dark:hover:text-purple-500',
744
- 'focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 dark:focus:ring-offset-[#0A0A0A]',
 
745
  )}
746
  >
747
  <div className="i-ph:download w-4 h-4" />
@@ -1152,7 +1249,7 @@ export default function DebugTab() {
1152
  {webAppInfo && (
1153
  <div className="mt-6">
1154
  <h3 className="mb-4 text-base font-medium text-bolt-elements-textPrimary">Dependencies</h3>
1155
- <div className="space-y-2 bg-gray-50 dark:bg-[#1A1A1A] rounded-lg">
1156
  <DependencySection title="Production" deps={webAppInfo.dependencies.production} />
1157
  <DependencySection title="Development" deps={webAppInfo.dependencies.development} />
1158
  <DependencySection title="Peer" deps={webAppInfo.dependencies.peer} />
 
131
  gitInfo: GitInfo;
132
  }
133
 
134
+ // Add Ollama service status interface
135
+ interface OllamaServiceStatus {
136
+ isRunning: boolean;
137
+ lastChecked: Date;
138
+ error?: string;
139
+ }
140
+
141
  const DependencySection = ({
142
  title,
143
  deps,
 
153
 
154
  return (
155
  <Collapsible open={isOpen} onOpenChange={setIsOpen}>
156
+ <CollapsibleTrigger
157
+ className={classNames(
158
+ 'flex w-full items-center justify-between p-4',
159
+ 'bg-white dark:bg-[#0A0A0A]',
160
+ 'hover:bg-purple-50/50 dark:hover:bg-[#1a1a1a]',
161
+ 'border-b border-[#E5E5E5] dark:border-[#1A1A1A]',
162
+ 'transition-colors duration-200',
163
+ 'first:rounded-t-lg last:rounded-b-lg',
164
+ { 'hover:rounded-lg': !isOpen },
165
+ )}
166
+ >
167
  <div className="flex items-center gap-3">
168
  <div className="i-ph:package text-bolt-elements-textSecondary w-4 h-4" />
169
  <span className="text-base text-bolt-elements-textPrimary">
 
174
  <span className="text-sm text-bolt-elements-textSecondary">{isOpen ? 'Hide' : 'Show'}</span>
175
  <div
176
  className={classNames(
177
+ 'i-ph:caret-down w-4 h-4 transform transition-transform duration-200 text-bolt-elements-textSecondary',
178
  isOpen ? 'rotate-180' : '',
179
  )}
180
  />
181
  </div>
182
  </CollapsibleTrigger>
183
  <CollapsibleContent>
184
+ <ScrollArea
185
+ className={classNames(
186
+ 'h-[200px] w-full',
187
+ 'bg-white dark:bg-[#0A0A0A]',
188
+ 'border-b border-[#E5E5E5] dark:border-[#1A1A1A]',
189
+ 'last:rounded-b-lg last:border-b-0',
190
+ )}
191
+ >
192
+ <div className="space-y-2 p-4">
193
  {deps.map((dep) => (
194
  <div key={dep.name} className="flex items-center justify-between text-sm">
195
  <span className="text-bolt-elements-textPrimary">{dep.name}</span>
 
206
  export default function DebugTab() {
207
  const [systemInfo, setSystemInfo] = useState<SystemInfo | null>(null);
208
  const [webAppInfo, setWebAppInfo] = useState<WebAppInfo | null>(null);
209
+ const [ollamaStatus, setOllamaStatus] = useState<OllamaServiceStatus>({
210
+ isRunning: false,
211
+ lastChecked: new Date(),
212
+ });
213
  const [loading, setLoading] = useState({
214
  systemInfo: false,
215
  webAppInfo: false,
 
287
  return undefined;
288
  }
289
 
290
+ // Initial fetch
291
+ const fetchGitInfo = async () => {
292
  try {
293
  const response = await fetch('/api/system/git-info');
294
  const updatedGitInfo = (await response.json()) as GitInfo;
 
298
  return null;
299
  }
300
 
301
+ // Only update if the data has changed
302
+ if (JSON.stringify(prev.gitInfo) === JSON.stringify(updatedGitInfo)) {
303
+ return prev;
304
+ }
305
+
306
  return {
307
  ...prev,
308
  gitInfo: updatedGitInfo,
309
  };
310
  });
311
  } catch (error) {
312
+ console.error('Failed to fetch git info:', error);
313
  }
 
 
 
 
314
  };
315
 
316
+ fetchGitInfo();
317
+
318
+ // Refresh every 5 minutes instead of every second
319
+ const interval = setInterval(fetchGitInfo, 5 * 60 * 1000);
320
+
321
+ return () => clearInterval(interval);
322
  }, [openSections.webapp]);
323
 
324
  const getSystemInfo = async () => {
 
651
  }
652
  };
653
 
654
+ // Add Ollama health check function
655
+ const checkOllamaHealth = async () => {
656
+ try {
657
+ const response = await fetch('http://127.0.0.1:11434/api/version');
658
+ const isHealthy = response.ok;
659
+
660
+ setOllamaStatus({
661
+ isRunning: isHealthy,
662
+ lastChecked: new Date(),
663
+ error: isHealthy ? undefined : 'Ollama service is not responding',
664
+ });
665
+
666
+ return isHealthy;
667
+ } catch {
668
+ setOllamaStatus({
669
+ isRunning: false,
670
+ lastChecked: new Date(),
671
+ error: 'Failed to connect to Ollama service',
672
+ });
673
+ return false;
674
+ }
675
+ };
676
+
677
+ // Add Ollama health check effect
678
+ useEffect(() => {
679
+ const checkHealth = async () => {
680
+ await checkOllamaHealth();
681
+ };
682
+
683
+ checkHealth();
684
+
685
+ const interval = setInterval(checkHealth, 30000); // Check every 30 seconds
686
+
687
+ return () => clearInterval(interval);
688
+ }, []);
689
+
690
  return (
691
  <div className="flex flex-col gap-6 max-w-7xl mx-auto p-4">
692
  {/* Quick Stats Banner */}
693
  <div className="grid grid-cols-1 md:grid-cols-4 gap-4">
694
+ {/* Add Ollama Service Status Card */}
695
+ <div className="p-4 rounded-xl bg-white dark:bg-[#0A0A0A] border border-[#E5E5E5] dark:border-[#1A1A1A]">
696
+ <div className="text-sm text-bolt-elements-textSecondary">Ollama Service</div>
697
+ <div className="flex items-center gap-2 mt-2">
698
+ <div
699
+ className={classNames(
700
+ 'w-2 h-2 rounded-full animate-pulse',
701
+ ollamaStatus.isRunning ? 'bg-green-500' : 'bg-red-500',
702
+ )}
703
+ />
704
+ <span
705
+ className={classNames('text-sm font-medium', ollamaStatus.isRunning ? 'text-green-500' : 'text-red-500')}
706
+ >
707
+ {ollamaStatus.isRunning ? 'Running' : 'Not Running'}
708
+ </span>
709
+ </div>
710
+ <div className="text-xs text-bolt-elements-textSecondary mt-2">
711
+ Last checked: {ollamaStatus.lastChecked.toLocaleTimeString()}
712
+ </div>
713
+ </div>
714
+
715
+ <div className="p-4 rounded-xl bg-white dark:bg-[#0A0A0A] border border-[#E5E5E5] dark:border-[#1A1A1A]">
716
  <div className="text-sm text-bolt-elements-textSecondary">Memory Usage</div>
717
  <div className="text-2xl font-semibold text-bolt-elements-textPrimary mt-1">
718
  {systemInfo?.memory.percentage}%
 
720
  <Progress value={systemInfo?.memory.percentage || 0} className="mt-2" />
721
  </div>
722
 
723
+ <div className="p-4 rounded-xl bg-white dark:bg-[#0A0A0A] border border-[#E5E5E5] dark:border-[#1A1A1A]">
724
  <div className="text-sm text-bolt-elements-textSecondary">Page Load Time</div>
725
  <div className="text-2xl font-semibold text-bolt-elements-textPrimary mt-1">
726
  {systemInfo ? (systemInfo.performance.timing.loadTime / 1000).toFixed(2) + 's' : '-'}
 
730
  </div>
731
  </div>
732
 
733
+ <div className="p-4 rounded-xl bg-white dark:bg-[#0A0A0A] border border-[#E5E5E5] dark:border-[#1A1A1A]">
734
  <div className="text-sm text-bolt-elements-textSecondary">Network Speed</div>
735
  <div className="text-2xl font-semibold text-bolt-elements-textPrimary mt-1">
736
  {systemInfo?.network.downlink || '-'} Mbps
 
738
  <div className="text-xs text-bolt-elements-textSecondary mt-2">RTT: {systemInfo?.network.rtt || '-'} ms</div>
739
  </div>
740
 
741
+ <div className="p-4 rounded-xl bg-white dark:bg-[#0A0A0A] border border-[#E5E5E5] dark:border-[#1A1A1A]">
742
  <div className="text-sm text-bolt-elements-textSecondary">Errors</div>
743
  <div className="text-2xl font-semibold text-bolt-elements-textPrimary mt-1">{errorLogs.length}</div>
744
  </div>
 
751
  disabled={loading.systemInfo}
752
  className={classNames(
753
  'flex items-center gap-2 px-4 py-2 text-sm font-medium rounded-lg transition-colors',
754
+ 'bg-white dark:bg-[#0A0A0A]',
755
+ 'border border-[#E5E5E5] dark:border-[#1A1A1A]',
756
+ 'hover:bg-purple-50 dark:hover:bg-[#1a1a1a]',
757
+ 'hover:border-purple-200 dark:hover:border-purple-900/30',
758
+ 'text-bolt-elements-textPrimary',
759
  { 'opacity-50 cursor-not-allowed': loading.systemInfo },
760
  )}
761
  >
 
772
  disabled={loading.performance}
773
  className={classNames(
774
  'flex items-center gap-2 px-4 py-2 text-sm font-medium rounded-lg transition-colors',
775
+ 'bg-white dark:bg-[#0A0A0A]',
776
+ 'border border-[#E5E5E5] dark:border-[#1A1A1A]',
777
+ 'hover:bg-purple-50 dark:hover:bg-[#1a1a1a]',
778
+ 'hover:border-purple-200 dark:hover:border-purple-900/30',
779
+ 'text-bolt-elements-textPrimary',
780
  { 'opacity-50 cursor-not-allowed': loading.performance },
781
  )}
782
  >
 
793
  disabled={loading.errors}
794
  className={classNames(
795
  'flex items-center gap-2 px-4 py-2 text-sm font-medium rounded-lg transition-colors',
796
+ 'bg-white dark:bg-[#0A0A0A]',
797
+ 'border border-[#E5E5E5] dark:border-[#1A1A1A]',
798
+ 'hover:bg-purple-50 dark:hover:bg-[#1a1a1a]',
799
+ 'hover:border-purple-200 dark:hover:border-purple-900/30',
800
+ 'text-bolt-elements-textPrimary',
801
  { 'opacity-50 cursor-not-allowed': loading.errors },
802
  )}
803
  >
 
814
  disabled={loading.webAppInfo}
815
  className={classNames(
816
  'flex items-center gap-2 px-4 py-2 text-sm font-medium rounded-lg transition-colors',
817
+ 'bg-white dark:bg-[#0A0A0A]',
818
+ 'border border-[#E5E5E5] dark:border-[#1A1A1A]',
819
+ 'hover:bg-purple-50 dark:hover:bg-[#1a1a1a]',
820
+ 'hover:border-purple-200 dark:hover:border-purple-900/30',
821
+ 'text-bolt-elements-textPrimary',
822
  { 'opacity-50 cursor-not-allowed': loading.webAppInfo },
823
  )}
824
  >
 
834
  onClick={exportDebugInfo}
835
  className={classNames(
836
  'flex items-center gap-2 px-4 py-2 text-sm font-medium rounded-lg transition-colors',
837
+ 'bg-white dark:bg-[#0A0A0A]',
838
+ 'border border-[#E5E5E5] dark:border-[#1A1A1A]',
839
+ 'hover:bg-purple-50 dark:hover:bg-[#1a1a1a]',
840
+ 'hover:border-purple-200 dark:hover:border-purple-900/30',
841
+ 'text-bolt-elements-textPrimary',
842
  )}
843
  >
844
  <div className="i-ph:download w-4 h-4" />
 
1249
  {webAppInfo && (
1250
  <div className="mt-6">
1251
  <h3 className="mb-4 text-base font-medium text-bolt-elements-textPrimary">Dependencies</h3>
1252
+ <div className="bg-white dark:bg-[#0A0A0A] border border-[#E5E5E5] dark:border-[#1A1A1A] rounded-lg divide-y divide-[#E5E5E5] dark:divide-[#1A1A1A]">
1253
  <DependencySection title="Production" deps={webAppInfo.dependencies.production} />
1254
  <DependencySection title="Development" deps={webAppInfo.dependencies.development} />
1255
  <DependencySection title="Peer" deps={webAppInfo.dependencies.peer} />
app/components/@settings/tabs/event-logs/EventLogsTab.tsx ADDED
@@ -0,0 +1,613 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
2
+ import { motion } from 'framer-motion';
3
+ import { Switch } from '~/components/ui/Switch';
4
+ import { logStore, type LogEntry } from '~/lib/stores/logs';
5
+ import { useStore } from '@nanostores/react';
6
+ import { classNames } from '~/utils/classNames';
7
+ import * as DropdownMenu from '@radix-ui/react-dropdown-menu';
8
+
9
+ interface SelectOption {
10
+ value: string;
11
+ label: string;
12
+ icon?: string;
13
+ color?: string;
14
+ }
15
+
16
+ const logLevelOptions: SelectOption[] = [
17
+ {
18
+ value: 'all',
19
+ label: 'All Types',
20
+ icon: 'i-ph:funnel',
21
+ color: '#9333ea',
22
+ },
23
+ {
24
+ value: 'provider',
25
+ label: 'LLM',
26
+ icon: 'i-ph:robot',
27
+ color: '#10b981',
28
+ },
29
+ {
30
+ value: 'api',
31
+ label: 'API',
32
+ icon: 'i-ph:cloud',
33
+ color: '#3b82f6',
34
+ },
35
+ {
36
+ value: 'error',
37
+ label: 'Errors',
38
+ icon: 'i-ph:warning-circle',
39
+ color: '#ef4444',
40
+ },
41
+ {
42
+ value: 'warning',
43
+ label: 'Warnings',
44
+ icon: 'i-ph:warning',
45
+ color: '#f59e0b',
46
+ },
47
+ {
48
+ value: 'info',
49
+ label: 'Info',
50
+ icon: 'i-ph:info',
51
+ color: '#3b82f6',
52
+ },
53
+ {
54
+ value: 'debug',
55
+ label: 'Debug',
56
+ icon: 'i-ph:bug',
57
+ color: '#6b7280',
58
+ },
59
+ ];
60
+
61
+ interface LogEntryItemProps {
62
+ log: LogEntry;
63
+ isExpanded: boolean;
64
+ use24Hour: boolean;
65
+ showTimestamp: boolean;
66
+ }
67
+
68
+ const LogEntryItem = ({ log, isExpanded: forceExpanded, use24Hour, showTimestamp }: LogEntryItemProps) => {
69
+ const [localExpanded, setLocalExpanded] = useState(forceExpanded);
70
+
71
+ useEffect(() => {
72
+ setLocalExpanded(forceExpanded);
73
+ }, [forceExpanded]);
74
+
75
+ const timestamp = useMemo(() => {
76
+ const date = new Date(log.timestamp);
77
+ return date.toLocaleTimeString('en-US', { hour12: !use24Hour });
78
+ }, [log.timestamp, use24Hour]);
79
+
80
+ const style = useMemo(() => {
81
+ if (log.category === 'provider') {
82
+ return {
83
+ icon: 'i-ph:robot',
84
+ color: 'text-emerald-500 dark:text-emerald-400',
85
+ bg: 'hover:bg-emerald-500/10 dark:hover:bg-emerald-500/20',
86
+ badge: 'text-emerald-500 bg-emerald-50 dark:bg-emerald-500/10',
87
+ };
88
+ }
89
+
90
+ if (log.category === 'api') {
91
+ return {
92
+ icon: 'i-ph:cloud',
93
+ color: 'text-blue-500 dark:text-blue-400',
94
+ bg: 'hover:bg-blue-500/10 dark:hover:bg-blue-500/20',
95
+ badge: 'text-blue-500 bg-blue-50 dark:bg-blue-500/10',
96
+ };
97
+ }
98
+
99
+ switch (log.level) {
100
+ case 'error':
101
+ return {
102
+ icon: 'i-ph:warning-circle',
103
+ color: 'text-red-500 dark:text-red-400',
104
+ bg: 'hover:bg-red-500/10 dark:hover:bg-red-500/20',
105
+ badge: 'text-red-500 bg-red-50 dark:bg-red-500/10',
106
+ };
107
+ case 'warning':
108
+ return {
109
+ icon: 'i-ph:warning',
110
+ color: 'text-yellow-500 dark:text-yellow-400',
111
+ bg: 'hover:bg-yellow-500/10 dark:hover:bg-yellow-500/20',
112
+ badge: 'text-yellow-500 bg-yellow-50 dark:bg-yellow-500/10',
113
+ };
114
+ case 'debug':
115
+ return {
116
+ icon: 'i-ph:bug',
117
+ color: 'text-gray-500 dark:text-gray-400',
118
+ bg: 'hover:bg-gray-500/10 dark:hover:bg-gray-500/20',
119
+ badge: 'text-gray-500 bg-gray-50 dark:bg-gray-500/10',
120
+ };
121
+ default:
122
+ return {
123
+ icon: 'i-ph:info',
124
+ color: 'text-blue-500 dark:text-blue-400',
125
+ bg: 'hover:bg-blue-500/10 dark:hover:bg-blue-500/20',
126
+ badge: 'text-blue-500 bg-blue-50 dark:bg-blue-500/10',
127
+ };
128
+ }
129
+ }, [log.level, log.category]);
130
+
131
+ const renderDetails = (details: any) => {
132
+ if (log.category === 'provider') {
133
+ return (
134
+ <div className="flex flex-col gap-2">
135
+ <div className="flex items-center gap-2 text-xs text-gray-500 dark:text-gray-400">
136
+ <span>Model: {details.model}</span>
137
+ <span>β€’</span>
138
+ <span>Tokens: {details.totalTokens}</span>
139
+ <span>β€’</span>
140
+ <span>Duration: {details.duration}ms</span>
141
+ </div>
142
+ {details.prompt && (
143
+ <div className="flex flex-col gap-1">
144
+ <div className="text-xs font-medium text-gray-700 dark:text-gray-300">Prompt:</div>
145
+ <pre className="text-xs text-gray-600 dark:text-gray-400 bg-gray-50 dark:bg-gray-800/50 rounded p-2 whitespace-pre-wrap">
146
+ {details.prompt}
147
+ </pre>
148
+ </div>
149
+ )}
150
+ {details.response && (
151
+ <div className="flex flex-col gap-1">
152
+ <div className="text-xs font-medium text-gray-700 dark:text-gray-300">Response:</div>
153
+ <pre className="text-xs text-gray-600 dark:text-gray-400 bg-gray-50 dark:bg-gray-800/50 rounded p-2 whitespace-pre-wrap">
154
+ {details.response}
155
+ </pre>
156
+ </div>
157
+ )}
158
+ </div>
159
+ );
160
+ }
161
+
162
+ if (log.category === 'api') {
163
+ return (
164
+ <div className="flex flex-col gap-2">
165
+ <div className="flex items-center gap-2 text-xs text-gray-500 dark:text-gray-400">
166
+ <span className={details.method === 'GET' ? 'text-green-500' : 'text-blue-500'}>{details.method}</span>
167
+ <span>β€’</span>
168
+ <span>Status: {details.statusCode}</span>
169
+ <span>β€’</span>
170
+ <span>Duration: {details.duration}ms</span>
171
+ </div>
172
+ <div className="text-xs text-gray-600 dark:text-gray-400 break-all">{details.url}</div>
173
+ {details.request && (
174
+ <div className="flex flex-col gap-1">
175
+ <div className="text-xs font-medium text-gray-700 dark:text-gray-300">Request:</div>
176
+ <pre className="text-xs text-gray-600 dark:text-gray-400 bg-gray-50 dark:bg-gray-800/50 rounded p-2 whitespace-pre-wrap">
177
+ {JSON.stringify(details.request, null, 2)}
178
+ </pre>
179
+ </div>
180
+ )}
181
+ {details.response && (
182
+ <div className="flex flex-col gap-1">
183
+ <div className="text-xs font-medium text-gray-700 dark:text-gray-300">Response:</div>
184
+ <pre className="text-xs text-gray-600 dark:text-gray-400 bg-gray-50 dark:bg-gray-800/50 rounded p-2 whitespace-pre-wrap">
185
+ {JSON.stringify(details.response, null, 2)}
186
+ </pre>
187
+ </div>
188
+ )}
189
+ {details.error && (
190
+ <div className="flex flex-col gap-1">
191
+ <div className="text-xs font-medium text-red-500">Error:</div>
192
+ <pre className="text-xs text-red-400 bg-red-50 dark:bg-red-500/10 rounded p-2 whitespace-pre-wrap">
193
+ {JSON.stringify(details.error, null, 2)}
194
+ </pre>
195
+ </div>
196
+ )}
197
+ </div>
198
+ );
199
+ }
200
+
201
+ return (
202
+ <pre className="text-xs text-gray-600 dark:text-gray-400 bg-gray-50 dark:bg-gray-800/50 rounded whitespace-pre-wrap">
203
+ {JSON.stringify(details, null, 2)}
204
+ </pre>
205
+ );
206
+ };
207
+
208
+ return (
209
+ <motion.div
210
+ initial={{ opacity: 0, y: 20 }}
211
+ animate={{ opacity: 1, y: 0 }}
212
+ className={classNames(
213
+ 'flex flex-col gap-2',
214
+ 'rounded-lg p-4',
215
+ 'bg-[#FAFAFA] dark:bg-[#0A0A0A]',
216
+ 'border border-[#E5E5E5] dark:border-[#1A1A1A]',
217
+ style.bg,
218
+ 'transition-all duration-200',
219
+ )}
220
+ >
221
+ <div className="flex items-start justify-between gap-4">
222
+ <div className="flex items-start gap-3">
223
+ <span className={classNames('text-lg', style.icon, style.color)} />
224
+ <div className="flex flex-col gap-1">
225
+ <div className="text-sm font-medium text-gray-900 dark:text-white">{log.message}</div>
226
+ {log.details && (
227
+ <>
228
+ <button
229
+ onClick={() => setLocalExpanded(!localExpanded)}
230
+ className="text-xs text-gray-500 dark:text-gray-400 hover:text-purple-500 dark:hover:text-purple-400 transition-colors"
231
+ >
232
+ {localExpanded ? 'Hide' : 'Show'} Details
233
+ </button>
234
+ {localExpanded && renderDetails(log.details)}
235
+ </>
236
+ )}
237
+ <div className="flex items-center gap-2">
238
+ <div className={classNames('px-2 py-0.5 rounded text-xs font-medium uppercase', style.badge)}>
239
+ {log.level}
240
+ </div>
241
+ {log.category && (
242
+ <div className="px-2 py-0.5 rounded-full text-xs bg-gray-100 dark:bg-gray-800 text-gray-500 dark:text-gray-400">
243
+ {log.category}
244
+ </div>
245
+ )}
246
+ </div>
247
+ </div>
248
+ </div>
249
+ {showTimestamp && <time className="shrink-0 text-xs text-gray-500 dark:text-gray-400">{timestamp}</time>}
250
+ </div>
251
+ </motion.div>
252
+ );
253
+ };
254
+
255
+ export function EventLogsTab() {
256
+ const logs = useStore(logStore.logs);
257
+ const [selectedLevel, setSelectedLevel] = useState<'all' | string>('all');
258
+ const [searchQuery, setSearchQuery] = useState('');
259
+ const [use24Hour, setUse24Hour] = useState(false);
260
+ const [autoExpand, setAutoExpand] = useState(false);
261
+ const [showTimestamps, setShowTimestamps] = useState(true);
262
+ const [showLevelFilter, setShowLevelFilter] = useState(false);
263
+ const [isRefreshing, setIsRefreshing] = useState(false);
264
+ const levelFilterRef = useRef<HTMLDivElement>(null);
265
+
266
+ const filteredLogs = useMemo(() => {
267
+ const allLogs = Object.values(logs);
268
+
269
+ if (selectedLevel === 'all') {
270
+ return allLogs.filter((log) =>
271
+ searchQuery ? log.message.toLowerCase().includes(searchQuery.toLowerCase()) : true,
272
+ );
273
+ }
274
+
275
+ return allLogs.filter((log) => {
276
+ const matchesType = log.category === selectedLevel || log.level === selectedLevel;
277
+ const matchesSearch = searchQuery ? log.message.toLowerCase().includes(searchQuery.toLowerCase()) : true;
278
+
279
+ return matchesType && matchesSearch;
280
+ });
281
+ }, [logs, selectedLevel, searchQuery]);
282
+
283
+ // Add performance tracking on mount
284
+ useEffect(() => {
285
+ const startTime = performance.now();
286
+
287
+ logStore.logInfo('Event Logs tab mounted', {
288
+ type: 'component_mount',
289
+ message: 'Event Logs tab component mounted',
290
+ component: 'EventLogsTab',
291
+ });
292
+
293
+ return () => {
294
+ const duration = performance.now() - startTime;
295
+ logStore.logPerformanceMetric('EventLogsTab', 'mount-duration', duration);
296
+ };
297
+ }, []);
298
+
299
+ // Log filter changes
300
+ const handleLevelFilterChange = useCallback(
301
+ (newLevel: string) => {
302
+ logStore.logInfo('Log level filter changed', {
303
+ type: 'filter_change',
304
+ message: `Log level filter changed from ${selectedLevel} to ${newLevel}`,
305
+ component: 'EventLogsTab',
306
+ previousLevel: selectedLevel,
307
+ newLevel,
308
+ });
309
+ setSelectedLevel(newLevel as string);
310
+ setShowLevelFilter(false);
311
+ },
312
+ [selectedLevel],
313
+ );
314
+
315
+ // Log search changes with debounce
316
+ useEffect(() => {
317
+ const timeoutId = setTimeout(() => {
318
+ if (searchQuery) {
319
+ logStore.logInfo('Log search performed', {
320
+ type: 'search',
321
+ message: `Search performed with query "${searchQuery}" (${filteredLogs.length} results)`,
322
+ component: 'EventLogsTab',
323
+ query: searchQuery,
324
+ resultsCount: filteredLogs.length,
325
+ });
326
+ }
327
+ }, 1000);
328
+
329
+ return () => clearTimeout(timeoutId);
330
+ }, [searchQuery, filteredLogs.length]);
331
+
332
+ // Enhanced export logs handler
333
+ const handleExportLogs = useCallback(() => {
334
+ const startTime = performance.now();
335
+
336
+ try {
337
+ const exportData = {
338
+ timestamp: new Date().toISOString(),
339
+ logs: filteredLogs,
340
+ filters: {
341
+ level: selectedLevel,
342
+ searchQuery,
343
+ },
344
+ };
345
+
346
+ const blob = new Blob([JSON.stringify(exportData, null, 2)], { type: 'application/json' });
347
+ const url = URL.createObjectURL(blob);
348
+ const a = document.createElement('a');
349
+ a.href = url;
350
+ a.download = `bolt-logs-${new Date().toISOString()}.json`;
351
+ document.body.appendChild(a);
352
+ a.click();
353
+ document.body.removeChild(a);
354
+ URL.revokeObjectURL(url);
355
+
356
+ const duration = performance.now() - startTime;
357
+ logStore.logSuccess('Logs exported successfully', {
358
+ type: 'export',
359
+ message: `Successfully exported ${filteredLogs.length} logs`,
360
+ component: 'EventLogsTab',
361
+ exportedCount: filteredLogs.length,
362
+ filters: {
363
+ level: selectedLevel,
364
+ searchQuery,
365
+ },
366
+ duration,
367
+ });
368
+ } catch (error) {
369
+ logStore.logError('Failed to export logs', error, {
370
+ type: 'export_error',
371
+ message: 'Failed to export logs',
372
+ component: 'EventLogsTab',
373
+ });
374
+ }
375
+ }, [filteredLogs, selectedLevel, searchQuery]);
376
+
377
+ // Enhanced refresh handler
378
+ const handleRefresh = useCallback(async () => {
379
+ const startTime = performance.now();
380
+ setIsRefreshing(true);
381
+
382
+ try {
383
+ await logStore.refreshLogs();
384
+
385
+ const duration = performance.now() - startTime;
386
+
387
+ logStore.logSuccess('Logs refreshed successfully', {
388
+ type: 'refresh',
389
+ message: `Successfully refreshed ${Object.keys(logs).length} logs`,
390
+ component: 'EventLogsTab',
391
+ duration,
392
+ logsCount: Object.keys(logs).length,
393
+ });
394
+ } catch (error) {
395
+ logStore.logError('Failed to refresh logs', error, {
396
+ type: 'refresh_error',
397
+ message: 'Failed to refresh logs',
398
+ component: 'EventLogsTab',
399
+ });
400
+ } finally {
401
+ setTimeout(() => setIsRefreshing(false), 500);
402
+ }
403
+ }, [logs]);
404
+
405
+ // Log preference changes
406
+ const handlePreferenceChange = useCallback((type: string, value: boolean) => {
407
+ logStore.logInfo('Log preference changed', {
408
+ type: 'preference_change',
409
+ message: `Log preference "${type}" changed to ${value}`,
410
+ component: 'EventLogsTab',
411
+ preference: type,
412
+ value,
413
+ });
414
+
415
+ switch (type) {
416
+ case 'timestamps':
417
+ setShowTimestamps(value);
418
+ break;
419
+ case '24hour':
420
+ setUse24Hour(value);
421
+ break;
422
+ case 'autoExpand':
423
+ setAutoExpand(value);
424
+ break;
425
+ }
426
+ }, []);
427
+
428
+ // Close filters when clicking outside
429
+ useEffect(() => {
430
+ const handleClickOutside = (event: MouseEvent) => {
431
+ if (levelFilterRef.current && !levelFilterRef.current.contains(event.target as Node)) {
432
+ setShowLevelFilter(false);
433
+ }
434
+ };
435
+
436
+ document.addEventListener('mousedown', handleClickOutside);
437
+
438
+ return () => {
439
+ document.removeEventListener('mousedown', handleClickOutside);
440
+ };
441
+ }, []);
442
+
443
+ const selectedLevelOption = logLevelOptions.find((opt) => opt.value === selectedLevel);
444
+
445
+ return (
446
+ <div className="flex h-full flex-col gap-6">
447
+ <div className="flex items-center justify-between">
448
+ <DropdownMenu.Root open={showLevelFilter} onOpenChange={setShowLevelFilter}>
449
+ <DropdownMenu.Trigger asChild>
450
+ <button
451
+ className={classNames(
452
+ 'flex items-center gap-2',
453
+ 'rounded-lg px-3 py-1.5',
454
+ 'text-sm text-gray-900 dark:text-white',
455
+ 'bg-[#FAFAFA] dark:bg-[#0A0A0A]',
456
+ 'border border-[#E5E5E5] dark:border-[#1A1A1A]',
457
+ 'hover:bg-purple-500/10 dark:hover:bg-purple-500/20',
458
+ 'transition-all duration-200',
459
+ )}
460
+ >
461
+ <span
462
+ className={classNames('text-lg', selectedLevelOption?.icon || 'i-ph:funnel')}
463
+ style={{ color: selectedLevelOption?.color }}
464
+ />
465
+ {selectedLevelOption?.label || 'All Types'}
466
+ <span className="i-ph:caret-down text-lg text-gray-500 dark:text-gray-400" />
467
+ </button>
468
+ </DropdownMenu.Trigger>
469
+
470
+ <DropdownMenu.Portal>
471
+ <DropdownMenu.Content
472
+ className="min-w-[200px] bg-white dark:bg-[#0A0A0A] rounded-lg shadow-lg py-1 z-[250] animate-in fade-in-0 zoom-in-95 border border-[#E5E5E5] dark:border-[#1A1A1A]"
473
+ sideOffset={5}
474
+ align="start"
475
+ side="bottom"
476
+ >
477
+ {logLevelOptions.map((option) => (
478
+ <DropdownMenu.Item
479
+ key={option.value}
480
+ className="group flex items-center px-4 py-2.5 text-sm text-gray-700 dark:text-gray-200 hover:bg-purple-500/10 dark:hover:bg-purple-500/20 cursor-pointer transition-colors"
481
+ onClick={() => handleLevelFilterChange(option.value)}
482
+ >
483
+ <div className="mr-3 flex h-5 w-5 items-center justify-center">
484
+ <div
485
+ className={classNames(option.icon, 'text-lg group-hover:text-purple-500 transition-colors')}
486
+ style={{ color: option.color }}
487
+ />
488
+ </div>
489
+ <span className="group-hover:text-purple-500 transition-colors">{option.label}</span>
490
+ </DropdownMenu.Item>
491
+ ))}
492
+ </DropdownMenu.Content>
493
+ </DropdownMenu.Portal>
494
+ </DropdownMenu.Root>
495
+
496
+ <div className="flex items-center gap-4">
497
+ <div className="flex items-center gap-2">
498
+ <Switch
499
+ checked={showTimestamps}
500
+ onCheckedChange={(value) => handlePreferenceChange('timestamps', value)}
501
+ className="data-[state=checked]:bg-purple-500"
502
+ />
503
+ <span className="text-sm text-gray-500 dark:text-gray-400">Show Timestamps</span>
504
+ </div>
505
+
506
+ <div className="flex items-center gap-2">
507
+ <Switch
508
+ checked={use24Hour}
509
+ onCheckedChange={(value) => handlePreferenceChange('24hour', value)}
510
+ className="data-[state=checked]:bg-purple-500"
511
+ />
512
+ <span className="text-sm text-gray-500 dark:text-gray-400">24h Time</span>
513
+ </div>
514
+
515
+ <div className="flex items-center gap-2">
516
+ <Switch
517
+ checked={autoExpand}
518
+ onCheckedChange={(value) => handlePreferenceChange('autoExpand', value)}
519
+ className="data-[state=checked]:bg-purple-500"
520
+ />
521
+ <span className="text-sm text-gray-500 dark:text-gray-400">Auto Expand</span>
522
+ </div>
523
+
524
+ <div className="w-px h-4 bg-gray-200 dark:bg-gray-700" />
525
+
526
+ <button
527
+ onClick={handleRefresh}
528
+ className={classNames(
529
+ 'group flex items-center gap-2',
530
+ 'rounded-lg px-3 py-1.5',
531
+ 'text-sm text-gray-900 dark:text-white',
532
+ 'bg-[#FAFAFA] dark:bg-[#0A0A0A]',
533
+ 'border border-[#E5E5E5] dark:border-[#1A1A1A]',
534
+ 'hover:bg-purple-500/10 dark:hover:bg-purple-500/20',
535
+ 'transition-all duration-200',
536
+ { 'animate-spin': isRefreshing },
537
+ )}
538
+ >
539
+ <span className="i-ph:arrows-clockwise text-lg text-gray-500 dark:text-gray-400 group-hover:text-purple-500 transition-colors" />
540
+ Refresh
541
+ </button>
542
+
543
+ <button
544
+ onClick={handleExportLogs}
545
+ className={classNames(
546
+ 'group flex items-center gap-2',
547
+ 'rounded-lg px-3 py-1.5',
548
+ 'text-sm text-gray-900 dark:text-white',
549
+ 'bg-[#FAFAFA] dark:bg-[#0A0A0A]',
550
+ 'border border-[#E5E5E5] dark:border-[#1A1A1A]',
551
+ 'hover:bg-purple-500/10 dark:hover:bg-purple-500/20',
552
+ 'transition-all duration-200',
553
+ )}
554
+ >
555
+ <span className="i-ph:download text-lg text-gray-500 dark:text-gray-400 group-hover:text-purple-500 transition-colors" />
556
+ Export
557
+ </button>
558
+ </div>
559
+ </div>
560
+
561
+ <div className="flex flex-col gap-4">
562
+ <div className="relative">
563
+ <input
564
+ type="text"
565
+ placeholder="Search logs..."
566
+ value={searchQuery}
567
+ onChange={(e) => setSearchQuery(e.target.value)}
568
+ className={classNames(
569
+ 'w-full px-4 py-2 pl-10 rounded-lg',
570
+ 'bg-[#FAFAFA] dark:bg-[#0A0A0A]',
571
+ 'border border-[#E5E5E5] dark:border-[#1A1A1A]',
572
+ 'text-gray-900 dark:text-white placeholder-gray-500 dark:placeholder-gray-400',
573
+ 'focus:outline-none focus:ring-2 focus:ring-purple-500/20 focus:border-purple-500',
574
+ 'transition-all duration-200',
575
+ )}
576
+ />
577
+ <div className="absolute left-3 top-1/2 -translate-y-1/2">
578
+ <div className="i-ph:magnifying-glass text-lg text-gray-500 dark:text-gray-400" />
579
+ </div>
580
+ </div>
581
+
582
+ {filteredLogs.length === 0 ? (
583
+ <motion.div
584
+ initial={{ opacity: 0, y: 20 }}
585
+ animate={{ opacity: 1, y: 0 }}
586
+ className={classNames(
587
+ 'flex flex-col items-center justify-center gap-4',
588
+ 'rounded-lg p-8 text-center',
589
+ 'bg-[#FAFAFA] dark:bg-[#0A0A0A]',
590
+ 'border border-[#E5E5E5] dark:border-[#1A1A1A]',
591
+ )}
592
+ >
593
+ <span className="i-ph:clipboard-text text-4xl text-gray-400 dark:text-gray-600" />
594
+ <div className="flex flex-col gap-1">
595
+ <h3 className="text-sm font-medium text-gray-900 dark:text-white">No Logs Found</h3>
596
+ <p className="text-sm text-gray-500 dark:text-gray-400">Try adjusting your search or filters</p>
597
+ </div>
598
+ </motion.div>
599
+ ) : (
600
+ filteredLogs.map((log) => (
601
+ <LogEntryItem
602
+ key={log.id}
603
+ log={log}
604
+ isExpanded={autoExpand}
605
+ use24Hour={use24Hour}
606
+ showTimestamp={showTimestamps}
607
+ />
608
+ ))
609
+ )}
610
+ </div>
611
+ </div>
612
+ );
613
+ }
app/components/{settings β†’ @settings/tabs}/features/FeaturesTab.tsx RENAMED
@@ -111,44 +111,66 @@ export default function FeaturesTab() {
111
  isLatestBranch,
112
  contextOptimizationEnabled,
113
  eventLogs,
114
- isLocalModel,
115
  setAutoSelectTemplate,
116
  enableLatestBranch,
117
  enableContextOptimization,
118
  setEventLogs,
119
- enableLocalModels,
120
  setPromptId,
121
  promptId,
122
  } = useSettings();
123
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
124
  const handleToggleFeature = useCallback(
125
  (id: string, enabled: boolean) => {
126
  switch (id) {
127
- case 'latestBranch':
128
  enableLatestBranch(enabled);
129
  toast.success(`Main branch updates ${enabled ? 'enabled' : 'disabled'}`);
130
  break;
131
- case 'autoSelectTemplate':
 
 
132
  setAutoSelectTemplate(enabled);
133
  toast.success(`Auto select template ${enabled ? 'enabled' : 'disabled'}`);
134
  break;
135
- case 'contextOptimization':
 
 
136
  enableContextOptimization(enabled);
137
  toast.success(`Context optimization ${enabled ? 'enabled' : 'disabled'}`);
138
  break;
139
- case 'eventLogs':
 
 
140
  setEventLogs(enabled);
141
  toast.success(`Event logging ${enabled ? 'enabled' : 'disabled'}`);
142
  break;
143
- case 'localModels':
144
- enableLocalModels(enabled);
145
- toast.success(`Experimental providers ${enabled ? 'enabled' : 'disabled'}`);
146
- break;
147
  default:
148
  break;
149
  }
150
  },
151
- [enableLatestBranch, setAutoSelectTemplate, enableContextOptimization, setEventLogs, enableLocalModels],
152
  );
153
 
154
  const features = {
@@ -159,7 +181,7 @@ export default function FeaturesTab() {
159
  description: 'Get the latest updates from the main branch',
160
  icon: 'i-ph:git-branch',
161
  enabled: isLatestBranch,
162
- tooltip: 'Enable to receive updates from the main development branch',
163
  },
164
  {
165
  id: 'autoSelectTemplate',
@@ -167,7 +189,7 @@ export default function FeaturesTab() {
167
  description: 'Automatically select starter template',
168
  icon: 'i-ph:selection',
169
  enabled: autoSelectTemplate,
170
- tooltip: 'Automatically select the most appropriate starter template',
171
  },
172
  {
173
  id: 'contextOptimization',
@@ -175,7 +197,7 @@ export default function FeaturesTab() {
175
  description: 'Optimize context for better responses',
176
  icon: 'i-ph:brain',
177
  enabled: contextOptimizationEnabled,
178
- tooltip: 'Enable context optimization for improved AI responses',
179
  },
180
  {
181
  id: 'eventLogs',
@@ -183,30 +205,19 @@ export default function FeaturesTab() {
183
  description: 'Enable detailed event logging and history',
184
  icon: 'i-ph:list-bullets',
185
  enabled: eventLogs,
186
- tooltip: 'Record detailed logs of system events and user actions',
187
  },
188
  ],
189
  beta: [],
190
- experimental: [
191
- {
192
- id: 'localModels',
193
- title: 'Experimental Providers',
194
- description: 'Enable experimental providers like Ollama, LMStudio, and OpenAILike',
195
- icon: 'i-ph:robot',
196
- enabled: isLocalModel,
197
- experimental: true,
198
- tooltip: 'Try out new AI providers and models in development',
199
- },
200
- ],
201
  };
202
 
203
  return (
204
  <div className="flex flex-col gap-8">
205
  <FeatureSection
206
- title="Stable Features"
207
  features={features.stable}
208
  icon="i-ph:check-circle"
209
- description="Production-ready features that have been thoroughly tested"
210
  onToggleFeature={handleToggleFeature}
211
  />
212
 
@@ -220,16 +231,6 @@ export default function FeaturesTab() {
220
  />
221
  )}
222
 
223
- {features.experimental.length > 0 && (
224
- <FeatureSection
225
- title="Experimental Features"
226
- features={features.experimental}
227
- icon="i-ph:flask"
228
- description="Features in early development that may be unstable or require additional setup"
229
- onToggleFeature={handleToggleFeature}
230
- />
231
- )}
232
-
233
  <motion.div
234
  layout
235
  className={classNames(
 
111
  isLatestBranch,
112
  contextOptimizationEnabled,
113
  eventLogs,
 
114
  setAutoSelectTemplate,
115
  enableLatestBranch,
116
  enableContextOptimization,
117
  setEventLogs,
 
118
  setPromptId,
119
  promptId,
120
  } = useSettings();
121
 
122
+ // Enable features by default on first load
123
+ React.useEffect(() => {
124
+ // Only enable if they haven't been explicitly set before
125
+ if (isLatestBranch === undefined) {
126
+ enableLatestBranch(true);
127
+ }
128
+
129
+ if (contextOptimizationEnabled === undefined) {
130
+ enableContextOptimization(true);
131
+ }
132
+
133
+ if (autoSelectTemplate === undefined) {
134
+ setAutoSelectTemplate(true);
135
+ }
136
+
137
+ if (eventLogs === undefined) {
138
+ setEventLogs(true);
139
+ }
140
+ }, []); // Only run once on component mount
141
+
142
  const handleToggleFeature = useCallback(
143
  (id: string, enabled: boolean) => {
144
  switch (id) {
145
+ case 'latestBranch': {
146
  enableLatestBranch(enabled);
147
  toast.success(`Main branch updates ${enabled ? 'enabled' : 'disabled'}`);
148
  break;
149
+ }
150
+
151
+ case 'autoSelectTemplate': {
152
  setAutoSelectTemplate(enabled);
153
  toast.success(`Auto select template ${enabled ? 'enabled' : 'disabled'}`);
154
  break;
155
+ }
156
+
157
+ case 'contextOptimization': {
158
  enableContextOptimization(enabled);
159
  toast.success(`Context optimization ${enabled ? 'enabled' : 'disabled'}`);
160
  break;
161
+ }
162
+
163
+ case 'eventLogs': {
164
  setEventLogs(enabled);
165
  toast.success(`Event logging ${enabled ? 'enabled' : 'disabled'}`);
166
  break;
167
+ }
168
+
 
 
169
  default:
170
  break;
171
  }
172
  },
173
+ [enableLatestBranch, setAutoSelectTemplate, enableContextOptimization, setEventLogs],
174
  );
175
 
176
  const features = {
 
181
  description: 'Get the latest updates from the main branch',
182
  icon: 'i-ph:git-branch',
183
  enabled: isLatestBranch,
184
+ tooltip: 'Enabled by default to receive updates from the main development branch',
185
  },
186
  {
187
  id: 'autoSelectTemplate',
 
189
  description: 'Automatically select starter template',
190
  icon: 'i-ph:selection',
191
  enabled: autoSelectTemplate,
192
+ tooltip: 'Enabled by default to automatically select the most appropriate starter template',
193
  },
194
  {
195
  id: 'contextOptimization',
 
197
  description: 'Optimize context for better responses',
198
  icon: 'i-ph:brain',
199
  enabled: contextOptimizationEnabled,
200
+ tooltip: 'Enabled by default for improved AI responses',
201
  },
202
  {
203
  id: 'eventLogs',
 
205
  description: 'Enable detailed event logging and history',
206
  icon: 'i-ph:list-bullets',
207
  enabled: eventLogs,
208
+ tooltip: 'Enabled by default to record detailed logs of system events and user actions',
209
  },
210
  ],
211
  beta: [],
 
 
 
 
 
 
 
 
 
 
 
212
  };
213
 
214
  return (
215
  <div className="flex flex-col gap-8">
216
  <FeatureSection
217
+ title="Core Features"
218
  features={features.stable}
219
  icon="i-ph:check-circle"
220
+ description="Essential features that are enabled by default for optimal performance"
221
  onToggleFeature={handleToggleFeature}
222
  />
223
 
 
231
  />
232
  )}
233
 
 
 
 
 
 
 
 
 
 
 
234
  <motion.div
235
  layout
236
  className={classNames(
app/components/{settings β†’ @settings/tabs}/notifications/NotificationsTab.tsx RENAMED
File without changes
app/components/@settings/tabs/profile/ProfileTab.tsx ADDED
@@ -0,0 +1,174 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState } from 'react';
2
+ import { useStore } from '@nanostores/react';
3
+ import { classNames } from '~/utils/classNames';
4
+ import { profileStore, updateProfile } from '~/lib/stores/profile';
5
+ import { toast } from 'react-toastify';
6
+
7
+ export default function ProfileTab() {
8
+ const profile = useStore(profileStore);
9
+ const [isUploading, setIsUploading] = useState(false);
10
+
11
+ const handleAvatarUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
12
+ const file = e.target.files?.[0];
13
+
14
+ if (!file) {
15
+ return;
16
+ }
17
+
18
+ try {
19
+ setIsUploading(true);
20
+
21
+ // Convert the file to base64
22
+ const reader = new FileReader();
23
+
24
+ reader.onloadend = () => {
25
+ const base64String = reader.result as string;
26
+ updateProfile({ avatar: base64String });
27
+ setIsUploading(false);
28
+ toast.success('Profile picture updated');
29
+ };
30
+
31
+ reader.onerror = () => {
32
+ console.error('Error reading file:', reader.error);
33
+ setIsUploading(false);
34
+ toast.error('Failed to update profile picture');
35
+ };
36
+ reader.readAsDataURL(file);
37
+ } catch (error) {
38
+ console.error('Error uploading avatar:', error);
39
+ setIsUploading(false);
40
+ toast.error('Failed to update profile picture');
41
+ }
42
+ };
43
+
44
+ const handleProfileUpdate = (field: 'username' | 'bio', value: string) => {
45
+ updateProfile({ [field]: value });
46
+
47
+ // Only show toast for completed typing (after 1 second of no typing)
48
+ const debounceToast = setTimeout(() => {
49
+ toast.success(`${field.charAt(0).toUpperCase() + field.slice(1)} updated`);
50
+ }, 1000);
51
+
52
+ return () => clearTimeout(debounceToast);
53
+ };
54
+
55
+ return (
56
+ <div className="max-w-2xl mx-auto">
57
+ <div className="space-y-6">
58
+ {/* Personal Information Section */}
59
+ <div>
60
+ {/* Avatar Upload */}
61
+ <div className="flex items-start gap-6 mb-8">
62
+ <div
63
+ className={classNames(
64
+ 'w-24 h-24 rounded-full overflow-hidden',
65
+ 'bg-gray-100 dark:bg-gray-800/50',
66
+ 'flex items-center justify-center',
67
+ 'ring-1 ring-gray-200 dark:ring-gray-700',
68
+ 'relative group',
69
+ 'transition-all duration-300 ease-out',
70
+ 'hover:ring-purple-500/30 dark:hover:ring-purple-500/30',
71
+ 'hover:shadow-lg hover:shadow-purple-500/10',
72
+ )}
73
+ >
74
+ {profile.avatar ? (
75
+ <img
76
+ src={profile.avatar}
77
+ alt="Profile"
78
+ className={classNames(
79
+ 'w-full h-full object-cover',
80
+ 'transition-all duration-300 ease-out',
81
+ 'group-hover:scale-105 group-hover:brightness-90',
82
+ )}
83
+ />
84
+ ) : (
85
+ <div className="i-ph:robot-fill w-16 h-16 text-gray-400 dark:text-gray-500 transition-colors group-hover:text-purple-500/70 transform -translate-y-1" />
86
+ )}
87
+
88
+ <label
89
+ className={classNames(
90
+ 'absolute inset-0',
91
+ 'flex items-center justify-center',
92
+ 'bg-black/0 group-hover:bg-black/40',
93
+ 'cursor-pointer transition-all duration-300 ease-out',
94
+ isUploading ? 'cursor-wait' : '',
95
+ )}
96
+ >
97
+ <input
98
+ type="file"
99
+ accept="image/*"
100
+ className="hidden"
101
+ onChange={handleAvatarUpload}
102
+ disabled={isUploading}
103
+ />
104
+ {isUploading ? (
105
+ <div className="i-ph:spinner-gap w-6 h-6 text-white animate-spin" />
106
+ ) : (
107
+ <div className="i-ph:camera-plus w-6 h-6 text-white opacity-0 group-hover:opacity-100 transition-all duration-300 ease-out transform group-hover:scale-110" />
108
+ )}
109
+ </label>
110
+ </div>
111
+
112
+ <div className="flex-1 pt-1">
113
+ <label className="block text-base font-medium text-gray-900 dark:text-gray-100 mb-1">
114
+ Profile Picture
115
+ </label>
116
+ <p className="text-sm text-gray-500 dark:text-gray-400">Upload a profile picture or avatar</p>
117
+ </div>
118
+ </div>
119
+
120
+ {/* Username Input */}
121
+ <div className="mb-6">
122
+ <label className="block text-sm font-medium text-gray-900 dark:text-gray-100 mb-2">Username</label>
123
+ <div className="relative group">
124
+ <div className="absolute left-3.5 top-1/2 -translate-y-1/2">
125
+ <div className="i-ph:user-circle-fill w-5 h-5 text-gray-400 dark:text-gray-500 transition-colors group-focus-within:text-purple-500" />
126
+ </div>
127
+ <input
128
+ type="text"
129
+ value={profile.username}
130
+ onChange={(e) => handleProfileUpdate('username', e.target.value)}
131
+ className={classNames(
132
+ 'w-full pl-11 pr-4 py-2.5 rounded-xl',
133
+ 'bg-white dark:bg-gray-800/50',
134
+ 'border border-gray-200 dark:border-gray-700/50',
135
+ 'text-gray-900 dark:text-white',
136
+ 'placeholder-gray-400 dark:placeholder-gray-500',
137
+ 'focus:outline-none focus:ring-2 focus:ring-purple-500/50 focus:border-purple-500/50',
138
+ 'transition-all duration-300 ease-out',
139
+ )}
140
+ placeholder="Enter your username"
141
+ />
142
+ </div>
143
+ </div>
144
+
145
+ {/* Bio Input */}
146
+ <div className="mb-8">
147
+ <label className="block text-sm font-medium text-gray-900 dark:text-gray-100 mb-2">Bio</label>
148
+ <div className="relative group">
149
+ <div className="absolute left-3.5 top-3">
150
+ <div className="i-ph:text-aa w-5 h-5 text-gray-400 dark:text-gray-500 transition-colors group-focus-within:text-purple-500" />
151
+ </div>
152
+ <textarea
153
+ value={profile.bio}
154
+ onChange={(e) => handleProfileUpdate('bio', e.target.value)}
155
+ className={classNames(
156
+ 'w-full pl-11 pr-4 py-2.5 rounded-xl',
157
+ 'bg-white dark:bg-gray-800/50',
158
+ 'border border-gray-200 dark:border-gray-700/50',
159
+ 'text-gray-900 dark:text-white',
160
+ 'placeholder-gray-400 dark:placeholder-gray-500',
161
+ 'focus:outline-none focus:ring-2 focus:ring-purple-500/50 focus:border-purple-500/50',
162
+ 'transition-all duration-300 ease-out',
163
+ 'resize-none',
164
+ 'h-32',
165
+ )}
166
+ placeholder="Tell us about yourself"
167
+ />
168
+ </div>
169
+ </div>
170
+ </div>
171
+ </div>
172
+ </div>
173
+ );
174
+ }
app/components/{settings/providers β†’ @settings/tabs/providers/cloud}/CloudProvidersTab.tsx RENAMED
File without changes
app/components/{settings/providers β†’ @settings/tabs/providers/local}/LocalProvidersTab.tsx RENAMED
@@ -4,7 +4,7 @@ import { useSettings } from '~/lib/hooks/useSettings';
4
  import { LOCAL_PROVIDERS, URL_CONFIGURABLE_PROVIDERS } from '~/lib/stores/settings';
5
  import type { IProviderConfig } from '~/types/model';
6
  import { logStore } from '~/lib/stores/logs';
7
- import { motion } from 'framer-motion';
8
  import { classNames } from '~/utils/classNames';
9
  import { BsRobot } from 'react-icons/bs';
10
  import type { IconType } from 'react-icons';
@@ -12,6 +12,8 @@ import { BiChip } from 'react-icons/bi';
12
  import { TbBrandOpenai } from 'react-icons/tb';
13
  import { providerBaseUrlEnvKeys } from '~/utils/constants';
14
  import { useToast } from '~/components/ui/use-toast';
 
 
15
 
16
  // Add type for provider names to ensure type safety
17
  type ProviderName = 'Ollama' | 'LMStudio' | 'OpenAILike';
@@ -53,12 +55,6 @@ interface OllamaModel {
53
  };
54
  }
55
 
56
- interface OllamaServiceStatus {
57
- isRunning: boolean;
58
- lastChecked: Date;
59
- error?: string;
60
- }
61
-
62
  interface OllamaPullResponse {
63
  status: string;
64
  completed?: number;
@@ -75,33 +71,14 @@ const isOllamaPullResponse = (data: unknown): data is OllamaPullResponse => {
75
  );
76
  };
77
 
78
- interface ManualInstallState {
79
- isOpen: boolean;
80
- modelString: string;
81
- }
82
-
83
- export function LocalProvidersTab() {
84
- const { success, error } = useToast();
85
  const { providers, updateProviderSettings } = useSettings();
86
  const [filteredProviders, setFilteredProviders] = useState<IProviderConfig[]>([]);
87
- const [categoryEnabled, setCategoryEnabled] = useState<boolean>(false);
88
- const [editingProvider, setEditingProvider] = useState<string | null>(null);
89
  const [ollamaModels, setOllamaModels] = useState<OllamaModel[]>([]);
90
  const [isLoadingModels, setIsLoadingModels] = useState(false);
91
- const [serviceStatus, setServiceStatus] = useState<OllamaServiceStatus>({
92
- isRunning: false,
93
- lastChecked: new Date(),
94
- });
95
- const [isInstallingModel, setIsInstallingModel] = useState<string | null>(null);
96
- const [installProgress, setInstallProgress] = useState<{
97
- model: string;
98
- progress: number;
99
- status: string;
100
- } | null>(null);
101
- const [manualInstall, setManualInstall] = useState<ManualInstallState>({
102
- isOpen: false,
103
- modelString: '',
104
- });
105
 
106
  // Effect to filter and sort providers
107
  useEffect(() => {
@@ -166,12 +143,6 @@ export function LocalProvidersTab() {
166
  setFilteredProviders(sorted);
167
  }, [providers, updateProviderSettings]);
168
 
169
- // Helper function to safely get environment URL
170
- const getEnvUrl = (provider: IProviderConfig): string | undefined => {
171
- const envKey = providerBaseUrlEnvKeys[provider.name]?.baseUrlKey;
172
- return envKey ? (import.meta.env[envKey] as string | undefined) : undefined;
173
- };
174
-
175
  // Add effect to update category toggle state based on provider states
176
  useEffect(() => {
177
  const newCategoryState = filteredProviders.every((p) => p.settings.enabled);
@@ -207,7 +178,7 @@ export function LocalProvidersTab() {
207
  }
208
  };
209
 
210
- const updateOllamaModel = async (modelName: string): Promise<{ success: boolean; newDigest?: string }> => {
211
  try {
212
  const response = await fetch(`${OLLAMA_API_URL}/api/pull`, {
213
  method: 'POST',
@@ -265,74 +236,54 @@ export function LocalProvidersTab() {
265
  const updatedData = (await updatedResponse.json()) as { models: OllamaModel[] };
266
  const updatedModel = updatedData.models.find((m) => m.name === modelName);
267
 
268
- return { success: true, newDigest: updatedModel?.digest };
269
  } catch (error) {
270
  console.error(`Error updating ${modelName}:`, error);
271
- return { success: false };
272
  }
273
  };
274
 
275
  const handleToggleCategory = useCallback(
276
- (enabled: boolean) => {
277
- setCategoryEnabled(enabled);
278
  filteredProviders.forEach((provider) => {
279
  updateProviderSettings(provider.name, { ...provider.settings, enabled });
280
  });
281
- success(enabled ? 'All local providers enabled' : 'All local providers disabled');
282
  },
283
- [filteredProviders, updateProviderSettings, success],
284
  );
285
 
286
  const handleToggleProvider = (provider: IProviderConfig, enabled: boolean) => {
287
- updateProviderSettings(provider.name, { ...provider.settings, enabled });
 
 
 
288
 
289
  if (enabled) {
290
  logStore.logProvider(`Provider ${provider.name} enabled`, { provider: provider.name });
291
- success(`${provider.name} enabled`);
292
  } else {
293
  logStore.logProvider(`Provider ${provider.name} disabled`, { provider: provider.name });
294
- success(`${provider.name} disabled`);
295
  }
296
  };
297
 
298
- const handleUpdateBaseUrl = (provider: IProviderConfig, baseUrl: string) => {
299
- let newBaseUrl: string | undefined = baseUrl;
300
-
301
- if (newBaseUrl && newBaseUrl.trim().length === 0) {
302
- newBaseUrl = undefined;
303
- }
304
-
305
- updateProviderSettings(provider.name, { ...provider.settings, baseUrl: newBaseUrl });
306
- logStore.logProvider(`Base URL updated for ${provider.name}`, {
307
- provider: provider.name,
308
  baseUrl: newBaseUrl,
309
  });
310
- success(`${provider.name} base URL updated`);
311
  setEditingProvider(null);
312
  };
313
 
314
  const handleUpdateOllamaModel = async (modelName: string) => {
315
- setOllamaModels((current) => current.map((m) => (m.name === modelName ? { ...m, status: 'updating' } : m)));
316
-
317
- const { success: updateSuccess, newDigest } = await updateOllamaModel(modelName);
318
-
319
- setOllamaModels((current) =>
320
- current.map((m) =>
321
- m.name === modelName
322
- ? {
323
- ...m,
324
- status: updateSuccess ? 'updated' : 'error',
325
- error: updateSuccess ? undefined : 'Update failed',
326
- newDigest,
327
- }
328
- : m,
329
- ),
330
- );
331
 
332
  if (updateSuccess) {
333
- success(`Updated ${modelName}`);
334
  } else {
335
- error(`Failed to update ${modelName}`);
336
  }
337
  };
338
 
@@ -351,336 +302,194 @@ export function LocalProvidersTab() {
351
  }
352
 
353
  setOllamaModels((current) => current.filter((m) => m.name !== modelName));
354
- success(`Deleted ${modelName}`);
355
  } catch (err) {
356
  const errorMessage = err instanceof Error ? err.message : 'Unknown error occurred';
357
  console.error(`Error deleting ${modelName}:`, errorMessage);
358
- error(`Failed to delete ${modelName}`);
359
- }
360
- };
361
-
362
- // Health check function
363
- const checkOllamaHealth = async () => {
364
- try {
365
- // Use the root endpoint instead of /api/health
366
- const response = await fetch(OLLAMA_API_URL);
367
- const text = await response.text();
368
- const isRunning = text.includes('Ollama is running');
369
-
370
- setServiceStatus({
371
- isRunning,
372
- lastChecked: new Date(),
373
- });
374
-
375
- if (isRunning) {
376
- // If Ollama is running, fetch models
377
- fetchOllamaModels();
378
- }
379
-
380
- return isRunning;
381
- } catch (error) {
382
- console.error('Health check error:', error);
383
- setServiceStatus({
384
- isRunning: false,
385
- lastChecked: new Date(),
386
- error: error instanceof Error ? error.message : 'Failed to connect to Ollama service',
387
- });
388
-
389
- return false;
390
- }
391
- };
392
-
393
- // Update manual installation function
394
- const handleManualInstall = async (modelString: string) => {
395
- try {
396
- setIsInstallingModel(modelString);
397
- setInstallProgress({ model: modelString, progress: 0, status: 'Starting download...' });
398
- setManualInstall((prev) => ({ ...prev, isOpen: false }));
399
-
400
- const response = await fetch(`${OLLAMA_API_URL}/api/pull`, {
401
- method: 'POST',
402
- headers: {
403
- 'Content-Type': 'application/json',
404
- },
405
- body: JSON.stringify({ name: modelString }),
406
- });
407
-
408
- if (!response.ok) {
409
- throw new Error(`Failed to install ${modelString}`);
410
- }
411
-
412
- const reader = response.body?.getReader();
413
-
414
- if (!reader) {
415
- throw new Error('No response reader available');
416
- }
417
-
418
- while (true) {
419
- const { done, value } = await reader.read();
420
-
421
- if (done) {
422
- break;
423
- }
424
-
425
- const text = new TextDecoder().decode(value);
426
- const lines = text.split('\n').filter(Boolean);
427
-
428
- for (const line of lines) {
429
- const rawData = JSON.parse(line);
430
-
431
- if (!isOllamaPullResponse(rawData)) {
432
- console.error('Invalid response format:', rawData);
433
- continue;
434
- }
435
-
436
- setInstallProgress({
437
- model: modelString,
438
- progress: rawData.completed && rawData.total ? (rawData.completed / rawData.total) * 100 : 0,
439
- status: rawData.status,
440
- });
441
- }
442
- }
443
-
444
- success(`Successfully installed ${modelString}`);
445
- await fetchOllamaModels();
446
- } catch (err) {
447
- const errorMessage = err instanceof Error ? err.message : 'Unknown error occurred';
448
- console.error(`Error installing ${modelString}:`, errorMessage);
449
- error(`Failed to install ${modelString}`);
450
- } finally {
451
- setIsInstallingModel(null);
452
- setInstallProgress(null);
453
  }
454
  };
455
 
456
- // Add health check effect
457
- useEffect(() => {
458
- const checkHealth = async () => {
459
- const isHealthy = await checkOllamaHealth();
460
-
461
- if (!isHealthy) {
462
- error('Ollama service is not running. Please start the Ollama service.');
463
- }
464
- };
465
-
466
- checkHealth();
467
-
468
- const interval = setInterval(checkHealth, 50000);
 
 
 
 
 
 
 
 
469
 
470
- // Check every 30 seconds
471
- return () => clearInterval(interval);
472
- }, []);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
473
 
474
  return (
475
  <div
476
  className={classNames(
477
- 'rounded-lg border bg-bolt-elements-background text-bolt-elements-textPrimary shadow-sm p-4',
478
  'hover:bg-bolt-elements-background-depth-2',
479
  'transition-all duration-200',
480
  )}
 
 
481
  >
482
- {/* Service Status Indicator - Move to top */}
483
- <div
484
- className={classNames(
485
- 'flex items-center gap-2 p-2 rounded-lg',
486
- serviceStatus.isRunning ? 'bg-green-500/10 text-green-500' : 'bg-red-500/10 text-red-500',
487
- )}
488
- >
489
- <div className={classNames('w-2 h-2 rounded-full', serviceStatus.isRunning ? 'bg-green-500' : 'bg-red-500')} />
490
- <span className="text-sm">
491
- {serviceStatus.isRunning ? 'Ollama service is running' : 'Ollama service is not running'}
492
- </span>
493
- <span className="text-xs text-bolt-elements-textSecondary ml-2">
494
- Last checked: {serviceStatus.lastChecked.toLocaleTimeString()}
495
- </span>
496
- </div>
497
-
498
  <motion.div
499
- className="space-y-4"
500
  initial={{ opacity: 0, y: 20 }}
501
  animate={{ opacity: 1, y: 0 }}
502
  transition={{ duration: 0.3 }}
503
  >
504
- <div className="flex items-center justify-between gap-4 mt-8 mb-4">
505
- <div className="flex items-center gap-2">
506
- <div
 
507
  className={classNames(
508
- 'w-8 h-8 flex items-center justify-center rounded-lg',
509
- 'bg-bolt-elements-background-depth-3',
510
- 'text-purple-500',
511
  )}
 
512
  >
513
- <BiChip className="w-5 h-5" />
514
- </div>
515
  <div>
516
- <h4 className="text-md font-medium text-bolt-elements-textPrimary">Local Providers</h4>
517
- <p className="text-sm text-bolt-elements-textSecondary">
518
- Configure and update local AI models on your machine
519
- </p>
520
  </div>
521
  </div>
522
 
523
  <div className="flex items-center gap-2">
524
- <span className="text-sm text-bolt-elements-textSecondary">Enable All Local</span>
525
- <Switch checked={categoryEnabled} onCheckedChange={handleToggleCategory} />
 
 
 
 
526
  </div>
527
  </div>
528
 
529
- <div className="grid grid-cols-2 gap-4">
530
- {filteredProviders.map((provider, index) => (
 
 
531
  <motion.div
532
  key={provider.name}
533
  className={classNames(
534
- 'bg-bolt-elements-background-depth-2',
535
  'hover:bg-bolt-elements-background-depth-3',
536
- 'transition-all duration-200',
537
  'relative overflow-hidden group',
538
- 'flex flex-col',
539
-
540
- // Make Ollama span 2 rows
541
- provider.name === 'Ollama' ? 'row-span-2' : '',
542
-
543
- // Place Ollama in the second column
544
- provider.name === 'Ollama' ? 'col-start-2' : 'col-start-1',
545
  )}
546
  initial={{ opacity: 0, y: 20 }}
547
  animate={{ opacity: 1, y: 0 }}
548
- transition={{ delay: index * 0.1 }}
549
- whileHover={{ scale: 1.02 }}
550
  >
551
- <div className="absolute top-0 right-0 p-2 flex gap-1">
552
- <motion.span
553
- className="px-2 py-0.5 text-xs rounded-full bg-green-500/10 text-green-500 font-medium"
554
- whileHover={{ scale: 1.05 }}
555
- whileTap={{ scale: 0.95 }}
556
- >
557
- Local
558
- </motion.span>
559
- {URL_CONFIGURABLE_PROVIDERS.includes(provider.name) && (
560
- <motion.span
561
- className="px-2 py-0.5 text-xs rounded-full bg-purple-500/10 text-purple-500 font-medium"
562
- whileHover={{ scale: 1.05 }}
563
- whileTap={{ scale: 0.95 }}
564
  >
565
- Configurable
566
- </motion.span>
567
- )}
568
- </div>
569
-
570
- <div className="flex items-start gap-4 p-4">
571
- <motion.div
572
- className={classNames(
573
- 'w-10 h-10 flex items-center justify-center rounded-xl',
574
- 'bg-bolt-elements-background-depth-3 group-hover:bg-bolt-elements-background-depth-4',
575
- 'transition-all duration-200',
576
- provider.settings.enabled ? 'text-purple-500' : 'text-bolt-elements-textSecondary',
577
- )}
578
- whileHover={{ scale: 1.1 }}
579
- whileTap={{ scale: 0.9 }}
580
- >
581
- <div className={classNames('w-6 h-6', 'transition-transform duration-200', 'group-hover:rotate-12')}>
582
  {React.createElement(PROVIDER_ICONS[provider.name as ProviderName] || BsRobot, {
583
- className: 'w-full h-full',
584
- 'aria-label': `${provider.name} logo`,
585
  })}
586
- </div>
587
- </motion.div>
588
-
589
- <div className="flex-1 min-w-0">
590
- <div className="flex items-center justify-between gap-4 mb-2">
591
- <div>
592
- <h4 className="text-sm font-medium text-bolt-elements-textPrimary group-hover:text-purple-500 transition-colors">
593
- {provider.name}
594
- </h4>
595
- <p className="text-xs text-bolt-elements-textSecondary mt-0.5">
596
- {PROVIDER_DESCRIPTIONS[provider.name as ProviderName]}
597
- </p>
598
  </div>
599
- <Switch
600
- checked={provider.settings.enabled}
601
- onCheckedChange={(checked) => handleToggleProvider(provider, checked)}
602
- />
603
  </div>
604
-
605
- {provider.settings.enabled && URL_CONFIGURABLE_PROVIDERS.includes(provider.name) && (
606
- <motion.div
607
- initial={{ opacity: 0, height: 0 }}
608
- animate={{ opacity: 1, height: 'auto' }}
609
- exit={{ opacity: 0, height: 0 }}
610
- transition={{ duration: 0.2 }}
611
- >
612
- <div className="flex items-center gap-2 mt-4">
613
- {editingProvider === provider.name ? (
614
- <input
615
- type="text"
616
- defaultValue={provider.settings.baseUrl}
617
- placeholder={`Enter ${provider.name} base URL`}
618
- className={classNames(
619
- 'flex-1 px-3 py-1.5 rounded-lg text-sm',
620
- 'bg-bolt-elements-background-depth-3 border border-bolt-elements-borderColor',
621
- 'text-bolt-elements-textPrimary placeholder-bolt-elements-textTertiary',
622
- 'focus:outline-none focus:ring-2 focus:ring-purple-500/30',
623
- 'transition-all duration-200',
624
- )}
625
- onKeyDown={(e) => {
626
- if (e.key === 'Enter') {
627
- handleUpdateBaseUrl(provider, e.currentTarget.value);
628
- } else if (e.key === 'Escape') {
629
- setEditingProvider(null);
630
- }
631
- }}
632
- onBlur={(e) => handleUpdateBaseUrl(provider, e.target.value)}
633
- autoFocus
634
- />
635
- ) : (
636
- <div
637
- className="flex-1 px-3 py-1.5 rounded-lg text-sm cursor-pointer group/url"
638
- onClick={() => setEditingProvider(provider.name)}
639
- >
640
- <div className="flex items-center gap-2 text-bolt-elements-textSecondary">
641
- <div className="i-ph:link text-sm" />
642
- <span className="group-hover/url:text-purple-500 transition-colors">
643
- {provider.settings.baseUrl || 'Click to set base URL'}
644
- </span>
645
- </div>
646
- </div>
647
- )}
648
-
649
- {providerBaseUrlEnvKeys[provider.name]?.baseUrlKey && (
650
- <div className="mt-2 text-xs">
651
- <div className="flex items-center gap-1">
652
- <div
653
- className={
654
- getEnvUrl(provider)
655
- ? 'i-ph:check-circle text-green-500'
656
- : 'i-ph:warning-circle text-yellow-500'
657
- }
658
- />
659
- <span className={getEnvUrl(provider) ? 'text-green-500' : 'text-yellow-500'}>
660
- {getEnvUrl(provider)
661
- ? 'Environment URL set in .env.local'
662
- : 'Environment URL not set in .env.local'}
663
- </span>
664
- </div>
665
- </div>
666
- )}
667
- </div>
668
- </motion.div>
669
- )}
670
  </div>
 
 
 
 
 
671
  </div>
672
 
673
- {provider.name === 'Ollama' && provider.settings.enabled && (
674
- <div className="mt-4 space-y-2">
 
675
  <div className="flex items-center justify-between">
676
  <div className="flex items-center gap-2">
677
  <div className="i-ph:cube-duotone text-purple-500" />
678
- <span className="text-sm font-medium text-bolt-elements-textPrimary">Installed Models</span>
679
  </div>
680
  {isLoadingModels ? (
681
- <div className="flex items-center gap-2 text-sm text-bolt-elements-textSecondary">
682
  <div className="i-ph:spinner-gap-bold animate-spin w-4 h-4" />
683
- Loading models...
684
  </div>
685
  ) : (
686
  <span className="text-sm text-bolt-elements-textSecondary">
@@ -689,226 +498,221 @@ export function LocalProvidersTab() {
689
  )}
690
  </div>
691
 
692
- <div className="space-y-2">
693
- {ollamaModels.map((model) => (
694
- <div
695
- key={model.name}
696
- className="flex items-center justify-between p-2 rounded-lg bg-bolt-elements-background-depth-3"
697
- >
698
- <div className="flex flex-col gap-1">
699
- <div className="flex items-center gap-2">
700
- <span className="text-sm text-bolt-elements-textPrimary">{model.name}</span>
701
- {model.status === 'updating' && (
702
- <div className="i-ph:spinner-gap-bold animate-spin w-4 h-4 text-purple-500" />
703
- )}
704
- {model.status === 'updated' && <div className="i-ph:check-circle text-green-500" />}
705
- {model.status === 'error' && <div className="i-ph:x-circle text-red-500" />}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
706
  </div>
707
- <div className="flex items-center gap-2 text-xs text-bolt-elements-textSecondary">
708
- <span>Version: {model.digest.substring(0, 7)}</span>
709
- {model.status === 'updated' && model.newDigest && (
710
- <>
711
- <div className="i-ph:arrow-right w-3 h-3" />
712
- <span className="text-green-500">{model.newDigest.substring(0, 7)}</span>
713
- </>
714
- )}
715
- {model.progress && (
716
- <span className="ml-2">
717
- {model.progress.status}{' '}
718
- {model.progress.total > 0 && (
719
- <>({Math.round((model.progress.current / model.progress.total) * 100)}%)</>
720
- )}
721
- </span>
722
- )}
723
- {model.details && (
724
- <span className="ml-2">
725
- ({model.details.parameter_size}, {model.details.quantization_level})
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
726
  </span>
727
  )}
728
  </div>
729
  </div>
730
- <div className="flex items-center gap-2">
731
- <motion.button
732
- onClick={() => handleUpdateOllamaModel(model.name)}
733
- disabled={model.status === 'updating'}
734
- className={classNames(
735
- 'rounded-md px-4 py-2 text-sm',
736
- 'bg-purple-500 text-white',
737
- 'hover:bg-purple-600',
738
- 'dark:bg-purple-500 dark:hover:bg-purple-600',
739
- 'transition-all duration-200',
740
- )}
741
- whileHover={{ scale: 1.02 }}
742
- whileTap={{ scale: 0.98 }}
743
- >
744
- <div className="i-ph:arrows-clockwise" />
745
- Update
746
- </motion.button>
747
- <motion.button
748
- onClick={() => {
749
- if (window.confirm(`Are you sure you want to delete ${model.name}?`)) {
750
- handleDeleteOllamaModel(model.name);
751
- }
752
- }}
753
- disabled={model.status === 'updating'}
754
- className={classNames(
755
- 'rounded-md px-4 py-2 text-sm',
756
- 'bg-red-500 text-white',
757
- 'hover:bg-red-600',
758
- 'dark:bg-red-500 dark:hover:bg-red-600',
759
- 'transition-all duration-200',
760
- )}
761
- whileHover={{ scale: 1.02 }}
762
- whileTap={{ scale: 0.98 }}
763
- >
764
- <div className="i-ph:trash" />
765
- Delete
766
- </motion.button>
767
- </div>
768
  </div>
769
- ))}
 
 
 
 
 
770
  </div>
771
- </div>
772
- )}
773
 
774
- <motion.div
775
- className="absolute inset-0 border-2 border-purple-500/0 rounded-lg pointer-events-none"
776
- animate={{
777
- borderColor: provider.settings.enabled ? 'rgba(168, 85, 247, 0.2)' : 'rgba(168, 85, 247, 0)',
778
- scale: provider.settings.enabled ? 1 : 0.98,
779
- }}
780
- transition={{ duration: 0.2 }}
781
- />
782
- </motion.div>
783
- ))}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
784
  </div>
785
  </motion.div>
 
 
 
786
 
787
- {/* Manual Installation Section */}
788
- {serviceStatus.isRunning && (
789
- <div className="mt-8 space-y-4">
790
- <div className="flex items-center justify-between">
791
- <div>
792
- <h3 className="text-lg font-medium text-bolt-elements-textPrimary">Install New Model</h3>
793
- <p className="text-sm text-bolt-elements-textSecondary">
794
- Enter the model name exactly as shown (e.g., deepseek-r1:1.5b)
795
- </p>
796
- </div>
797
- </div>
798
 
799
- {/* Model Information Section */}
800
- <div className="p-4 rounded-lg bg-bolt-elements-background-depth-2 space-y-3">
801
- <div className="flex items-center gap-2 text-bolt-elements-textPrimary">
802
- <div className="i-ph:info text-purple-500" />
803
- <span className="font-medium">Where to find models?</span>
804
- </div>
805
- <div className="space-y-2 text-sm text-bolt-elements-textSecondary">
806
- <p>
807
- Browse available models at{' '}
808
- <a
809
- href="https://ollama.com/library"
810
- target="_blank"
811
- rel="noopener noreferrer"
812
- className="text-purple-500 hover:underline"
813
- >
814
- ollama.com/library
815
- </a>
816
- </p>
817
- <div className="space-y-1">
818
- <p className="font-medium text-bolt-elements-textPrimary">Popular models:</p>
819
- <ul className="list-disc list-inside space-y-1 ml-2">
820
- <li>deepseek-r1:1.5b - DeepSeek's reasoning model</li>
821
- <li>llama3:8b - Meta's Llama 3 (8B parameters)</li>
822
- <li>mistral:7b - Mistral's 7B model</li>
823
- <li>gemma:2b - Google's Gemma model</li>
824
- <li>qwen2:7b - Alibaba's Qwen2 model</li>
825
- </ul>
826
- </div>
827
- <p className="mt-2">
828
- <span className="text-yellow-500">Note:</span> Copy the exact model name including the tag (e.g.,
829
- 'deepseek-r1:1.5b') from the library to ensure successful installation.
830
- </p>
831
- </div>
832
- </div>
833
 
834
- <div className="flex gap-4">
835
- <div className="flex-1">
836
- <input
837
- type="text"
838
- className="w-full px-3 py-2 rounded-md bg-bolt-elements-background-depth-2 border border-bolt-elements-borderColor text-bolt-elements-textPrimary"
839
- placeholder="deepseek-r1:1.5b"
840
- value={manualInstall.modelString}
841
- onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
842
- setManualInstall((prev) => ({ ...prev, modelString: e.target.value }))
843
- }
844
- />
845
- </div>
846
- <motion.button
847
- onClick={() => handleManualInstall(manualInstall.modelString)}
848
- disabled={!manualInstall.modelString || !!isInstallingModel}
849
- className={classNames(
850
- 'rounded-md px-4 py-2 text-sm',
851
- 'bg-purple-500 text-white',
852
- 'hover:bg-purple-600',
853
- 'dark:bg-purple-500 dark:hover:bg-purple-600',
854
- 'transition-all duration-200',
855
- )}
856
- whileHover={{ scale: 1.02 }}
857
- whileTap={{ scale: 0.98 }}
858
- >
859
- {isInstallingModel ? (
860
- <div className="flex items-center justify-center gap-2">
861
- <div className="i-ph:spinner-gap-bold animate-spin" />
862
- Installing...
863
- </div>
864
- ) : (
865
- <>
866
- <div className="i-ph:download" />
867
- Install Model
868
- </>
869
- )}
870
- </motion.button>
871
- {isInstallingModel && (
872
- <motion.button
873
- onClick={() => {
874
- setIsInstallingModel(null);
875
- setInstallProgress(null);
876
- error('Installation cancelled');
877
- }}
878
- className={classNames(
879
- 'rounded-md px-4 py-2 text-sm',
880
- 'bg-red-500 text-white',
881
- 'hover:bg-red-600',
882
- 'dark:bg-red-500 dark:hover:bg-red-600',
883
- 'transition-all duration-200',
884
- )}
885
- whileHover={{ scale: 1.02 }}
886
- whileTap={{ scale: 0.98 }}
887
- >
888
- <div className="i-ph:x" />
889
- Cancel
890
- </motion.button>
891
- )}
892
- </div>
893
 
894
- {installProgress && (
895
- <div className="mt-2 space-y-2">
896
- <div className="flex items-center justify-between text-sm text-bolt-elements-textSecondary">
897
- <span>{installProgress.status}</span>
898
- <span>{Math.round(installProgress.progress)}%</span>
899
- </div>
900
- <div className="w-full h-2 bg-bolt-elements-background-depth-3 rounded-full overflow-hidden">
901
- <div
902
- className="h-full bg-purple-500 transition-all duration-200"
903
- style={{ width: `${installProgress.progress}%` }}
904
- />
905
- </div>
906
- </div>
907
- )}
908
- </div>
909
- )}
910
- </div>
911
  );
912
  }
913
-
914
- export default LocalProvidersTab;
 
4
  import { LOCAL_PROVIDERS, URL_CONFIGURABLE_PROVIDERS } from '~/lib/stores/settings';
5
  import type { IProviderConfig } from '~/types/model';
6
  import { logStore } from '~/lib/stores/logs';
7
+ import { motion, AnimatePresence } from 'framer-motion';
8
  import { classNames } from '~/utils/classNames';
9
  import { BsRobot } from 'react-icons/bs';
10
  import type { IconType } from 'react-icons';
 
12
  import { TbBrandOpenai } from 'react-icons/tb';
13
  import { providerBaseUrlEnvKeys } from '~/utils/constants';
14
  import { useToast } from '~/components/ui/use-toast';
15
+ import { Progress } from '~/components/ui/Progress';
16
+ import OllamaModelInstaller from './OllamaModelInstaller';
17
 
18
  // Add type for provider names to ensure type safety
19
  type ProviderName = 'Ollama' | 'LMStudio' | 'OpenAILike';
 
55
  };
56
  }
57
 
 
 
 
 
 
 
58
  interface OllamaPullResponse {
59
  status: string;
60
  completed?: number;
 
71
  );
72
  };
73
 
74
+ export default function LocalProvidersTab() {
 
 
 
 
 
 
75
  const { providers, updateProviderSettings } = useSettings();
76
  const [filteredProviders, setFilteredProviders] = useState<IProviderConfig[]>([]);
77
+ const [categoryEnabled, setCategoryEnabled] = useState(false);
 
78
  const [ollamaModels, setOllamaModels] = useState<OllamaModel[]>([]);
79
  const [isLoadingModels, setIsLoadingModels] = useState(false);
80
+ const [editingProvider, setEditingProvider] = useState<string | null>(null);
81
+ const { toast } = useToast();
 
 
 
 
 
 
 
 
 
 
 
 
82
 
83
  // Effect to filter and sort providers
84
  useEffect(() => {
 
143
  setFilteredProviders(sorted);
144
  }, [providers, updateProviderSettings]);
145
 
 
 
 
 
 
 
146
  // Add effect to update category toggle state based on provider states
147
  useEffect(() => {
148
  const newCategoryState = filteredProviders.every((p) => p.settings.enabled);
 
178
  }
179
  };
180
 
181
+ const updateOllamaModel = async (modelName: string): Promise<boolean> => {
182
  try {
183
  const response = await fetch(`${OLLAMA_API_URL}/api/pull`, {
184
  method: 'POST',
 
236
  const updatedData = (await updatedResponse.json()) as { models: OllamaModel[] };
237
  const updatedModel = updatedData.models.find((m) => m.name === modelName);
238
 
239
+ return updatedModel !== undefined;
240
  } catch (error) {
241
  console.error(`Error updating ${modelName}:`, error);
242
+ return false;
243
  }
244
  };
245
 
246
  const handleToggleCategory = useCallback(
247
+ async (enabled: boolean) => {
 
248
  filteredProviders.forEach((provider) => {
249
  updateProviderSettings(provider.name, { ...provider.settings, enabled });
250
  });
251
+ toast(enabled ? 'All local providers enabled' : 'All local providers disabled');
252
  },
253
+ [filteredProviders, updateProviderSettings],
254
  );
255
 
256
  const handleToggleProvider = (provider: IProviderConfig, enabled: boolean) => {
257
+ updateProviderSettings(provider.name, {
258
+ ...provider.settings,
259
+ enabled,
260
+ });
261
 
262
  if (enabled) {
263
  logStore.logProvider(`Provider ${provider.name} enabled`, { provider: provider.name });
264
+ toast(`${provider.name} enabled`);
265
  } else {
266
  logStore.logProvider(`Provider ${provider.name} disabled`, { provider: provider.name });
267
+ toast(`${provider.name} disabled`);
268
  }
269
  };
270
 
271
+ const handleUpdateBaseUrl = (provider: IProviderConfig, newBaseUrl: string) => {
272
+ updateProviderSettings(provider.name, {
273
+ ...provider.settings,
 
 
 
 
 
 
 
274
  baseUrl: newBaseUrl,
275
  });
276
+ toast(`${provider.name} base URL updated`);
277
  setEditingProvider(null);
278
  };
279
 
280
  const handleUpdateOllamaModel = async (modelName: string) => {
281
+ const updateSuccess = await updateOllamaModel(modelName);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
282
 
283
  if (updateSuccess) {
284
+ toast(`Updated ${modelName}`);
285
  } else {
286
+ toast(`Failed to update ${modelName}`);
287
  }
288
  };
289
 
 
302
  }
303
 
304
  setOllamaModels((current) => current.filter((m) => m.name !== modelName));
305
+ toast(`Deleted ${modelName}`);
306
  } catch (err) {
307
  const errorMessage = err instanceof Error ? err.message : 'Unknown error occurred';
308
  console.error(`Error deleting ${modelName}:`, errorMessage);
309
+ toast(`Failed to delete ${modelName}`);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
310
  }
311
  };
312
 
313
+ // Update model details display
314
+ const ModelDetails = ({ model }: { model: OllamaModel }) => (
315
+ <div className="flex items-center gap-3 text-xs text-bolt-elements-textSecondary">
316
+ <div className="flex items-center gap-1">
317
+ <div className="i-ph:code text-purple-500" />
318
+ <span>{model.digest.substring(0, 7)}</span>
319
+ </div>
320
+ {model.details && (
321
+ <>
322
+ <div className="flex items-center gap-1">
323
+ <div className="i-ph:database text-purple-500" />
324
+ <span>{model.details.parameter_size}</span>
325
+ </div>
326
+ <div className="flex items-center gap-1">
327
+ <div className="i-ph:cube text-purple-500" />
328
+ <span>{model.details.quantization_level}</span>
329
+ </div>
330
+ </>
331
+ )}
332
+ </div>
333
+ );
334
 
335
+ // Update model actions to not use Tooltip
336
+ const ModelActions = ({
337
+ model,
338
+ onUpdate,
339
+ onDelete,
340
+ }: {
341
+ model: OllamaModel;
342
+ onUpdate: () => void;
343
+ onDelete: () => void;
344
+ }) => (
345
+ <div className="flex items-center gap-2">
346
+ <motion.button
347
+ onClick={onUpdate}
348
+ disabled={model.status === 'updating'}
349
+ className={classNames(
350
+ 'rounded-lg p-2',
351
+ 'bg-purple-500/10 text-purple-500',
352
+ 'hover:bg-purple-500/20',
353
+ 'transition-all duration-200',
354
+ { 'opacity-50 cursor-not-allowed': model.status === 'updating' },
355
+ )}
356
+ whileHover={{ scale: 1.05 }}
357
+ whileTap={{ scale: 0.95 }}
358
+ title="Update model"
359
+ >
360
+ {model.status === 'updating' ? (
361
+ <div className="flex items-center gap-2">
362
+ <div className="i-ph:spinner-gap-bold animate-spin w-4 h-4" />
363
+ <span className="text-sm">Updating...</span>
364
+ </div>
365
+ ) : (
366
+ <div className="i-ph:arrows-clockwise text-lg" />
367
+ )}
368
+ </motion.button>
369
+ <motion.button
370
+ onClick={onDelete}
371
+ disabled={model.status === 'updating'}
372
+ className={classNames(
373
+ 'rounded-lg p-2',
374
+ 'bg-red-500/10 text-red-500',
375
+ 'hover:bg-red-500/20',
376
+ 'transition-all duration-200',
377
+ { 'opacity-50 cursor-not-allowed': model.status === 'updating' },
378
+ )}
379
+ whileHover={{ scale: 1.05 }}
380
+ whileTap={{ scale: 0.95 }}
381
+ title="Delete model"
382
+ >
383
+ <div className="i-ph:trash text-lg" />
384
+ </motion.button>
385
+ </div>
386
+ );
387
 
388
  return (
389
  <div
390
  className={classNames(
391
+ 'rounded-lg bg-bolt-elements-background text-bolt-elements-textPrimary shadow-sm p-4',
392
  'hover:bg-bolt-elements-background-depth-2',
393
  'transition-all duration-200',
394
  )}
395
+ role="region"
396
+ aria-label="Local Providers Configuration"
397
  >
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
398
  <motion.div
399
+ className="space-y-6"
400
  initial={{ opacity: 0, y: 20 }}
401
  animate={{ opacity: 1, y: 0 }}
402
  transition={{ duration: 0.3 }}
403
  >
404
+ {/* Header section */}
405
+ <div className="flex items-center justify-between gap-4 border-b border-bolt-elements-borderColor pb-4">
406
+ <div className="flex items-center gap-3">
407
+ <motion.div
408
  className={classNames(
409
+ 'w-10 h-10 flex items-center justify-center rounded-xl',
410
+ 'bg-purple-500/10 text-purple-500',
 
411
  )}
412
+ whileHover={{ scale: 1.05 }}
413
  >
414
+ <BiChip className="w-6 h-6" />
415
+ </motion.div>
416
  <div>
417
+ <h2 className="text-lg font-semibold text-bolt-elements-textPrimary">Local AI Models</h2>
418
+ <p className="text-sm text-bolt-elements-textSecondary">Configure and manage your local AI providers</p>
 
 
419
  </div>
420
  </div>
421
 
422
  <div className="flex items-center gap-2">
423
+ <span className="text-sm text-bolt-elements-textSecondary">Enable All</span>
424
+ <Switch
425
+ checked={categoryEnabled}
426
+ onCheckedChange={handleToggleCategory}
427
+ aria-label="Toggle all local providers"
428
+ />
429
  </div>
430
  </div>
431
 
432
+ {/* Ollama Section */}
433
+ {filteredProviders
434
+ .filter((provider) => provider.name === 'Ollama')
435
+ .map((provider) => (
436
  <motion.div
437
  key={provider.name}
438
  className={classNames(
439
+ 'bg-bolt-elements-background-depth-2 rounded-xl',
440
  'hover:bg-bolt-elements-background-depth-3',
441
+ 'transition-all duration-200 p-5',
442
  'relative overflow-hidden group',
 
 
 
 
 
 
 
443
  )}
444
  initial={{ opacity: 0, y: 20 }}
445
  animate={{ opacity: 1, y: 0 }}
446
+ whileHover={{ scale: 1.01 }}
 
447
  >
448
+ {/* Provider Header */}
449
+ <div className="flex items-start justify-between gap-4">
450
+ <div className="flex items-start gap-4">
451
+ <motion.div
452
+ className={classNames(
453
+ 'w-12 h-12 flex items-center justify-center rounded-xl',
454
+ 'bg-bolt-elements-background-depth-3',
455
+ provider.settings.enabled ? 'text-purple-500' : 'text-bolt-elements-textSecondary',
456
+ )}
457
+ whileHover={{ scale: 1.1, rotate: 5 }}
 
 
 
458
  >
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
459
  {React.createElement(PROVIDER_ICONS[provider.name as ProviderName] || BsRobot, {
460
+ className: 'w-7 h-7',
461
+ 'aria-label': `${provider.name} icon`,
462
  })}
463
+ </motion.div>
464
+ <div>
465
+ <div className="flex items-center gap-2">
466
+ <h3 className="text-md font-semibold text-bolt-elements-textPrimary">{provider.name}</h3>
467
+ <span className="px-2 py-0.5 text-xs rounded-full bg-green-500/10 text-green-500">Local</span>
 
 
 
 
 
 
 
468
  </div>
469
+ <p className="text-sm text-bolt-elements-textSecondary mt-1">
470
+ {PROVIDER_DESCRIPTIONS[provider.name as ProviderName]}
471
+ </p>
 
472
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
473
  </div>
474
+ <Switch
475
+ checked={provider.settings.enabled}
476
+ onCheckedChange={(checked) => handleToggleProvider(provider, checked)}
477
+ aria-label={`Toggle ${provider.name} provider`}
478
+ />
479
  </div>
480
 
481
+ {/* Ollama Models Section */}
482
+ {provider.settings.enabled && (
483
+ <motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} className="mt-6 space-y-4">
484
  <div className="flex items-center justify-between">
485
  <div className="flex items-center gap-2">
486
  <div className="i-ph:cube-duotone text-purple-500" />
487
+ <h4 className="text-sm font-medium text-bolt-elements-textPrimary">Installed Models</h4>
488
  </div>
489
  {isLoadingModels ? (
490
+ <div className="flex items-center gap-2">
491
  <div className="i-ph:spinner-gap-bold animate-spin w-4 h-4" />
492
+ <span className="text-sm text-bolt-elements-textSecondary">Loading models...</span>
493
  </div>
494
  ) : (
495
  <span className="text-sm text-bolt-elements-textSecondary">
 
498
  )}
499
  </div>
500
 
501
+ <div className="space-y-3">
502
+ {isLoadingModels ? (
503
+ <div className="space-y-3">
504
+ {Array.from({ length: 3 }).map((_, i) => (
505
+ <div
506
+ key={i}
507
+ className="h-20 w-full bg-bolt-elements-background-depth-3 rounded-lg animate-pulse"
508
+ />
509
+ ))}
510
+ </div>
511
+ ) : ollamaModels.length === 0 ? (
512
+ <div className="text-center py-8 text-bolt-elements-textSecondary">
513
+ <div className="i-ph:cube-transparent text-4xl mx-auto mb-2" />
514
+ <p>No models installed yet</p>
515
+ <p className="text-sm">Install your first model below</p>
516
+ </div>
517
+ ) : (
518
+ ollamaModels.map((model) => (
519
+ <motion.div
520
+ key={model.name}
521
+ className={classNames(
522
+ 'p-4 rounded-xl',
523
+ 'bg-bolt-elements-background-depth-3',
524
+ 'hover:bg-bolt-elements-background-depth-4',
525
+ 'transition-all duration-200',
526
+ )}
527
+ whileHover={{ scale: 1.01 }}
528
+ >
529
+ <div className="flex items-center justify-between">
530
+ <div className="space-y-2">
531
+ <div className="flex items-center gap-2">
532
+ <h5 className="text-sm font-medium text-bolt-elements-textPrimary">{model.name}</h5>
533
+ <ModelStatusBadge status={model.status} />
534
+ </div>
535
+ <ModelDetails model={model} />
536
+ </div>
537
+ <ModelActions
538
+ model={model}
539
+ onUpdate={() => handleUpdateOllamaModel(model.name)}
540
+ onDelete={() => {
541
+ if (window.confirm(`Are you sure you want to delete ${model.name}?`)) {
542
+ handleDeleteOllamaModel(model.name);
543
+ }
544
+ }}
545
+ />
546
  </div>
547
+ {model.progress && (
548
+ <div className="mt-3">
549
+ <Progress
550
+ value={Math.round((model.progress.current / model.progress.total) * 100)}
551
+ className="h-1"
552
+ />
553
+ <div className="flex justify-between mt-1 text-xs text-bolt-elements-textSecondary">
554
+ <span>{model.progress.status}</span>
555
+ <span>{Math.round((model.progress.current / model.progress.total) * 100)}%</span>
556
+ </div>
557
+ </div>
558
+ )}
559
+ </motion.div>
560
+ ))
561
+ )}
562
+ </div>
563
+
564
+ {/* Model Installation Section */}
565
+ <OllamaModelInstaller onModelInstalled={fetchOllamaModels} />
566
+ </motion.div>
567
+ )}
568
+ </motion.div>
569
+ ))}
570
+
571
+ {/* Other Providers Section */}
572
+ <div className="border-t border-bolt-elements-borderColor pt-6 mt-8">
573
+ <h3 className="text-lg font-semibold text-bolt-elements-textPrimary mb-4">Other Local Providers</h3>
574
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
575
+ {filteredProviders
576
+ .filter((provider) => provider.name !== 'Ollama')
577
+ .map((provider, index) => (
578
+ <motion.div
579
+ key={provider.name}
580
+ className={classNames(
581
+ 'bg-bolt-elements-background-depth-2 rounded-xl',
582
+ 'hover:bg-bolt-elements-background-depth-3',
583
+ 'transition-all duration-200 p-5',
584
+ 'relative overflow-hidden group',
585
+ )}
586
+ initial={{ opacity: 0, y: 20 }}
587
+ animate={{ opacity: 1, y: 0 }}
588
+ transition={{ delay: index * 0.1 }}
589
+ whileHover={{ scale: 1.01 }}
590
+ >
591
+ {/* Provider Header */}
592
+ <div className="flex items-start justify-between gap-4">
593
+ <div className="flex items-start gap-4">
594
+ <motion.div
595
+ className={classNames(
596
+ 'w-12 h-12 flex items-center justify-center rounded-xl',
597
+ 'bg-bolt-elements-background-depth-3',
598
+ provider.settings.enabled ? 'text-purple-500' : 'text-bolt-elements-textSecondary',
599
+ )}
600
+ whileHover={{ scale: 1.1, rotate: 5 }}
601
+ >
602
+ {React.createElement(PROVIDER_ICONS[provider.name as ProviderName] || BsRobot, {
603
+ className: 'w-7 h-7',
604
+ 'aria-label': `${provider.name} icon`,
605
+ })}
606
+ </motion.div>
607
+ <div>
608
+ <div className="flex items-center gap-2">
609
+ <h3 className="text-md font-semibold text-bolt-elements-textPrimary">{provider.name}</h3>
610
+ <div className="flex gap-1">
611
+ <span className="px-2 py-0.5 text-xs rounded-full bg-green-500/10 text-green-500">
612
+ Local
613
+ </span>
614
+ {URL_CONFIGURABLE_PROVIDERS.includes(provider.name) && (
615
+ <span className="px-2 py-0.5 text-xs rounded-full bg-purple-500/10 text-purple-500">
616
+ Configurable
617
  </span>
618
  )}
619
  </div>
620
  </div>
621
+ <p className="text-sm text-bolt-elements-textSecondary mt-1">
622
+ {PROVIDER_DESCRIPTIONS[provider.name as ProviderName]}
623
+ </p>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
624
  </div>
625
+ </div>
626
+ <Switch
627
+ checked={provider.settings.enabled}
628
+ onCheckedChange={(checked) => handleToggleProvider(provider, checked)}
629
+ aria-label={`Toggle ${provider.name} provider`}
630
+ />
631
  </div>
 
 
632
 
633
+ {/* URL Configuration Section */}
634
+ <AnimatePresence>
635
+ {provider.settings.enabled && URL_CONFIGURABLE_PROVIDERS.includes(provider.name) && (
636
+ <motion.div
637
+ initial={{ opacity: 0, height: 0 }}
638
+ animate={{ opacity: 1, height: 'auto' }}
639
+ exit={{ opacity: 0, height: 0 }}
640
+ className="mt-4"
641
+ >
642
+ <div className="flex flex-col gap-2">
643
+ <label className="text-sm text-bolt-elements-textSecondary">API Endpoint</label>
644
+ {editingProvider === provider.name ? (
645
+ <input
646
+ type="text"
647
+ defaultValue={provider.settings.baseUrl}
648
+ placeholder={`Enter ${provider.name} base URL`}
649
+ className={classNames(
650
+ 'w-full px-3 py-2 rounded-lg text-sm',
651
+ 'bg-bolt-elements-background-depth-3 border border-bolt-elements-borderColor',
652
+ 'text-bolt-elements-textPrimary placeholder-bolt-elements-textTertiary',
653
+ 'focus:outline-none focus:ring-2 focus:ring-purple-500/30',
654
+ 'transition-all duration-200',
655
+ )}
656
+ onKeyDown={(e) => {
657
+ if (e.key === 'Enter') {
658
+ handleUpdateBaseUrl(provider, e.currentTarget.value);
659
+ } else if (e.key === 'Escape') {
660
+ setEditingProvider(null);
661
+ }
662
+ }}
663
+ onBlur={(e) => handleUpdateBaseUrl(provider, e.target.value)}
664
+ autoFocus
665
+ />
666
+ ) : (
667
+ <div
668
+ onClick={() => setEditingProvider(provider.name)}
669
+ className={classNames(
670
+ 'w-full px-3 py-2 rounded-lg text-sm cursor-pointer',
671
+ 'bg-bolt-elements-background-depth-3 border border-bolt-elements-borderColor',
672
+ 'hover:border-purple-500/30 hover:bg-bolt-elements-background-depth-4',
673
+ 'transition-all duration-200',
674
+ )}
675
+ >
676
+ <div className="flex items-center gap-2 text-bolt-elements-textSecondary">
677
+ <div className="i-ph:link text-sm" />
678
+ <span>{provider.settings.baseUrl || 'Click to set base URL'}</span>
679
+ </div>
680
+ </div>
681
+ )}
682
+ </div>
683
+ </motion.div>
684
+ )}
685
+ </AnimatePresence>
686
+ </motion.div>
687
+ ))}
688
+ </div>
689
  </div>
690
  </motion.div>
691
+ </div>
692
+ );
693
+ }
694
 
695
+ // Helper component for model status badge
696
+ function ModelStatusBadge({ status }: { status?: string }) {
697
+ if (!status || status === 'idle') {
698
+ return null;
699
+ }
 
 
 
 
 
 
700
 
701
+ const statusConfig = {
702
+ updating: { bg: 'bg-yellow-500/10', text: 'text-yellow-500', label: 'Updating' },
703
+ updated: { bg: 'bg-green-500/10', text: 'text-green-500', label: 'Updated' },
704
+ error: { bg: 'bg-red-500/10', text: 'text-red-500', label: 'Error' },
705
+ };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
706
 
707
+ const config = statusConfig[status as keyof typeof statusConfig];
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
708
 
709
+ if (!config) {
710
+ return null;
711
+ }
712
+
713
+ return (
714
+ <span className={classNames('px-2 py-0.5 rounded-full text-xs font-medium', config.bg, config.text)}>
715
+ {config.label}
716
+ </span>
 
 
 
 
 
 
 
 
 
717
  );
718
  }
 
 
app/components/@settings/tabs/providers/local/OllamaModelInstaller.tsx ADDED
@@ -0,0 +1,597 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState, useEffect } from 'react';
2
+ import { motion } from 'framer-motion';
3
+ import { classNames } from '~/utils/classNames';
4
+ import { Progress } from '~/components/ui/Progress';
5
+ import { useToast } from '~/components/ui/use-toast';
6
+
7
+ interface OllamaModelInstallerProps {
8
+ onModelInstalled: () => void;
9
+ }
10
+
11
+ interface InstallProgress {
12
+ status: string;
13
+ progress: number;
14
+ downloadedSize?: string;
15
+ totalSize?: string;
16
+ speed?: string;
17
+ }
18
+
19
+ interface ModelInfo {
20
+ name: string;
21
+ desc: string;
22
+ size: string;
23
+ tags: string[];
24
+ installedVersion?: string;
25
+ latestVersion?: string;
26
+ needsUpdate?: boolean;
27
+ status?: 'idle' | 'installing' | 'updating' | 'updated' | 'error';
28
+ details?: {
29
+ family: string;
30
+ parameter_size: string;
31
+ quantization_level: string;
32
+ };
33
+ }
34
+
35
+ const POPULAR_MODELS: ModelInfo[] = [
36
+ {
37
+ name: 'deepseek-coder:6.7b',
38
+ desc: "DeepSeek's code generation model",
39
+ size: '4.1GB',
40
+ tags: ['coding', 'popular'],
41
+ },
42
+ {
43
+ name: 'llama2:7b',
44
+ desc: "Meta's Llama 2 (7B parameters)",
45
+ size: '3.8GB',
46
+ tags: ['general', 'popular'],
47
+ },
48
+ {
49
+ name: 'mistral:7b',
50
+ desc: "Mistral's 7B model",
51
+ size: '4.1GB',
52
+ tags: ['general', 'popular'],
53
+ },
54
+ {
55
+ name: 'gemma:7b',
56
+ desc: "Google's Gemma model",
57
+ size: '4.0GB',
58
+ tags: ['general', 'new'],
59
+ },
60
+ {
61
+ name: 'codellama:7b',
62
+ desc: "Meta's Code Llama model",
63
+ size: '4.1GB',
64
+ tags: ['coding', 'popular'],
65
+ },
66
+ {
67
+ name: 'neural-chat:7b',
68
+ desc: "Intel's Neural Chat model",
69
+ size: '4.1GB',
70
+ tags: ['chat', 'popular'],
71
+ },
72
+ {
73
+ name: 'phi:latest',
74
+ desc: "Microsoft's Phi-2 model",
75
+ size: '2.7GB',
76
+ tags: ['small', 'fast'],
77
+ },
78
+ {
79
+ name: 'qwen:7b',
80
+ desc: "Alibaba's Qwen model",
81
+ size: '4.1GB',
82
+ tags: ['general'],
83
+ },
84
+ {
85
+ name: 'solar:10.7b',
86
+ desc: "Upstage's Solar model",
87
+ size: '6.1GB',
88
+ tags: ['large', 'powerful'],
89
+ },
90
+ {
91
+ name: 'openchat:7b',
92
+ desc: 'Open-source chat model',
93
+ size: '4.1GB',
94
+ tags: ['chat', 'popular'],
95
+ },
96
+ {
97
+ name: 'dolphin-phi:2.7b',
98
+ desc: 'Lightweight chat model',
99
+ size: '1.6GB',
100
+ tags: ['small', 'fast'],
101
+ },
102
+ {
103
+ name: 'stable-code:3b',
104
+ desc: 'Lightweight coding model',
105
+ size: '1.8GB',
106
+ tags: ['coding', 'small'],
107
+ },
108
+ ];
109
+
110
+ function formatBytes(bytes: number): string {
111
+ if (bytes === 0) {
112
+ return '0 B';
113
+ }
114
+
115
+ const k = 1024;
116
+ const sizes = ['B', 'KB', 'MB', 'GB'];
117
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
118
+
119
+ return `${parseFloat((bytes / Math.pow(k, i)).toFixed(1))} ${sizes[i]}`;
120
+ }
121
+
122
+ function formatSpeed(bytesPerSecond: number): string {
123
+ return `${formatBytes(bytesPerSecond)}/s`;
124
+ }
125
+
126
+ // Add Ollama Icon SVG component
127
+ function OllamaIcon({ className }: { className?: string }) {
128
+ return (
129
+ <svg viewBox="0 0 1024 1024" className={className} fill="currentColor">
130
+ <path d="M684.3 322.2H339.8c-9.5.1-17.7 6.8-19.6 16.1-8.2 41.4-12.4 83.5-12.4 125.7 0 42.2 4.2 84.3 12.4 125.7 1.9 9.3 10.1 16 19.6 16.1h344.5c9.5-.1 17.7-6.8 19.6-16.1 8.2-41.4 12.4-83.5 12.4-125.7 0-42.2-4.2-84.3-12.4-125.7-1.9-9.3-10.1-16-19.6-16.1zM512 640c-176.7 0-320-143.3-320-320S335.3 0 512 0s320 143.3 320 320-143.3 320-320 320z" />
131
+ </svg>
132
+ );
133
+ }
134
+
135
+ export default function OllamaModelInstaller({ onModelInstalled }: OllamaModelInstallerProps) {
136
+ const [modelString, setModelString] = useState('');
137
+ const [searchQuery, setSearchQuery] = useState('');
138
+ const [isInstalling, setIsInstalling] = useState(false);
139
+ const [isChecking, setIsChecking] = useState(false);
140
+ const [installProgress, setInstallProgress] = useState<InstallProgress | null>(null);
141
+ const [selectedTags, setSelectedTags] = useState<string[]>([]);
142
+ const [models, setModels] = useState<ModelInfo[]>(POPULAR_MODELS);
143
+ const { toast } = useToast();
144
+
145
+ // Function to check installed models and their versions
146
+ const checkInstalledModels = async () => {
147
+ try {
148
+ const response = await fetch('http://127.0.0.1:11434/api/tags', {
149
+ method: 'GET',
150
+ });
151
+
152
+ if (!response.ok) {
153
+ throw new Error('Failed to fetch installed models');
154
+ }
155
+
156
+ const data = (await response.json()) as { models: Array<{ name: string; digest: string; latest: string }> };
157
+ const installedModels = data.models || [];
158
+
159
+ // Update models with installed versions
160
+ setModels((prevModels) =>
161
+ prevModels.map((model) => {
162
+ const installed = installedModels.find((m) => m.name.toLowerCase() === model.name.toLowerCase());
163
+
164
+ if (installed) {
165
+ return {
166
+ ...model,
167
+ installedVersion: installed.digest.substring(0, 8),
168
+ needsUpdate: installed.digest !== installed.latest,
169
+ latestVersion: installed.latest?.substring(0, 8),
170
+ };
171
+ }
172
+
173
+ return model;
174
+ }),
175
+ );
176
+ } catch (error) {
177
+ console.error('Error checking installed models:', error);
178
+ }
179
+ };
180
+
181
+ // Check installed models on mount and after installation
182
+ useEffect(() => {
183
+ checkInstalledModels();
184
+ }, []);
185
+
186
+ const handleCheckUpdates = async () => {
187
+ setIsChecking(true);
188
+
189
+ try {
190
+ await checkInstalledModels();
191
+ toast('Model versions checked');
192
+ } catch (err) {
193
+ console.error('Failed to check model versions:', err);
194
+ toast('Failed to check model versions');
195
+ } finally {
196
+ setIsChecking(false);
197
+ }
198
+ };
199
+
200
+ const filteredModels = models.filter((model) => {
201
+ const matchesSearch =
202
+ searchQuery === '' ||
203
+ model.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
204
+ model.desc.toLowerCase().includes(searchQuery.toLowerCase());
205
+ const matchesTags = selectedTags.length === 0 || selectedTags.some((tag) => model.tags.includes(tag));
206
+
207
+ return matchesSearch && matchesTags;
208
+ });
209
+
210
+ const handleInstallModel = async (modelToInstall: string) => {
211
+ if (!modelToInstall) {
212
+ return;
213
+ }
214
+
215
+ try {
216
+ setIsInstalling(true);
217
+ setInstallProgress({
218
+ status: 'Starting download...',
219
+ progress: 0,
220
+ downloadedSize: '0 B',
221
+ totalSize: 'Calculating...',
222
+ speed: '0 B/s',
223
+ });
224
+ setModelString('');
225
+ setSearchQuery('');
226
+
227
+ const response = await fetch('http://127.0.0.1:11434/api/pull', {
228
+ method: 'POST',
229
+ headers: {
230
+ 'Content-Type': 'application/json',
231
+ },
232
+ body: JSON.stringify({ name: modelToInstall }),
233
+ });
234
+
235
+ if (!response.ok) {
236
+ throw new Error(`HTTP error! status: ${response.status}`);
237
+ }
238
+
239
+ const reader = response.body?.getReader();
240
+
241
+ if (!reader) {
242
+ throw new Error('Failed to get response reader');
243
+ }
244
+
245
+ let lastTime = Date.now();
246
+ let lastBytes = 0;
247
+
248
+ while (true) {
249
+ const { done, value } = await reader.read();
250
+
251
+ if (done) {
252
+ break;
253
+ }
254
+
255
+ const text = new TextDecoder().decode(value);
256
+ const lines = text.split('\n').filter(Boolean);
257
+
258
+ for (const line of lines) {
259
+ try {
260
+ const data = JSON.parse(line);
261
+
262
+ if ('status' in data) {
263
+ const currentTime = Date.now();
264
+ const timeDiff = (currentTime - lastTime) / 1000; // Convert to seconds
265
+ const bytesDiff = (data.completed || 0) - lastBytes;
266
+ const speed = bytesDiff / timeDiff;
267
+
268
+ setInstallProgress({
269
+ status: data.status,
270
+ progress: data.completed && data.total ? (data.completed / data.total) * 100 : 0,
271
+ downloadedSize: formatBytes(data.completed || 0),
272
+ totalSize: data.total ? formatBytes(data.total) : 'Calculating...',
273
+ speed: formatSpeed(speed),
274
+ });
275
+
276
+ lastTime = currentTime;
277
+ lastBytes = data.completed || 0;
278
+ }
279
+ } catch (err) {
280
+ console.error('Error parsing progress:', err);
281
+ }
282
+ }
283
+ }
284
+
285
+ toast('Successfully installed ' + modelToInstall + '. The model list will refresh automatically.');
286
+
287
+ // Ensure we call onModelInstalled after successful installation
288
+ setTimeout(() => {
289
+ onModelInstalled();
290
+ }, 1000);
291
+ } catch (err) {
292
+ const errorMessage = err instanceof Error ? err.message : 'Unknown error occurred';
293
+ console.error(`Error installing ${modelToInstall}:`, errorMessage);
294
+ toast(`Failed to install ${modelToInstall}. ${errorMessage}`);
295
+ } finally {
296
+ setIsInstalling(false);
297
+ setInstallProgress(null);
298
+ }
299
+ };
300
+
301
+ const handleUpdateModel = async (modelToUpdate: string) => {
302
+ try {
303
+ setModels((prev) => prev.map((m) => (m.name === modelToUpdate ? { ...m, status: 'updating' } : m)));
304
+
305
+ const response = await fetch('http://127.0.0.1:11434/api/pull', {
306
+ method: 'POST',
307
+ headers: {
308
+ 'Content-Type': 'application/json',
309
+ },
310
+ body: JSON.stringify({ name: modelToUpdate }),
311
+ });
312
+
313
+ if (!response.ok) {
314
+ throw new Error(`HTTP error! status: ${response.status}`);
315
+ }
316
+
317
+ const reader = response.body?.getReader();
318
+
319
+ if (!reader) {
320
+ throw new Error('Failed to get response reader');
321
+ }
322
+
323
+ let lastTime = Date.now();
324
+ let lastBytes = 0;
325
+
326
+ while (true) {
327
+ const { done, value } = await reader.read();
328
+
329
+ if (done) {
330
+ break;
331
+ }
332
+
333
+ const text = new TextDecoder().decode(value);
334
+ const lines = text.split('\n').filter(Boolean);
335
+
336
+ for (const line of lines) {
337
+ try {
338
+ const data = JSON.parse(line);
339
+
340
+ if ('status' in data) {
341
+ const currentTime = Date.now();
342
+ const timeDiff = (currentTime - lastTime) / 1000;
343
+ const bytesDiff = (data.completed || 0) - lastBytes;
344
+ const speed = bytesDiff / timeDiff;
345
+
346
+ setInstallProgress({
347
+ status: data.status,
348
+ progress: data.completed && data.total ? (data.completed / data.total) * 100 : 0,
349
+ downloadedSize: formatBytes(data.completed || 0),
350
+ totalSize: data.total ? formatBytes(data.total) : 'Calculating...',
351
+ speed: formatSpeed(speed),
352
+ });
353
+
354
+ lastTime = currentTime;
355
+ lastBytes = data.completed || 0;
356
+ }
357
+ } catch (err) {
358
+ console.error('Error parsing progress:', err);
359
+ }
360
+ }
361
+ }
362
+
363
+ toast('Successfully updated ' + modelToUpdate);
364
+
365
+ // Refresh model list after update
366
+ await checkInstalledModels();
367
+ } catch (err) {
368
+ const errorMessage = err instanceof Error ? err.message : 'Unknown error occurred';
369
+ console.error(`Error updating ${modelToUpdate}:`, errorMessage);
370
+ toast(`Failed to update ${modelToUpdate}. ${errorMessage}`);
371
+ setModels((prev) => prev.map((m) => (m.name === modelToUpdate ? { ...m, status: 'error' } : m)));
372
+ } finally {
373
+ setInstallProgress(null);
374
+ }
375
+ };
376
+
377
+ const allTags = Array.from(new Set(POPULAR_MODELS.flatMap((model) => model.tags)));
378
+
379
+ return (
380
+ <div className="space-y-6">
381
+ <div className="flex items-center justify-between pt-6">
382
+ <div className="flex items-center gap-3">
383
+ <OllamaIcon className="w-8 h-8 text-purple-500" />
384
+ <div>
385
+ <h3 className="text-lg font-semibold text-bolt-elements-textPrimary">Ollama Models</h3>
386
+ <p className="text-sm text-bolt-elements-textSecondary mt-1">Install and manage your Ollama models</p>
387
+ </div>
388
+ </div>
389
+ <motion.button
390
+ onClick={handleCheckUpdates}
391
+ disabled={isChecking}
392
+ className={classNames(
393
+ 'px-4 py-2 rounded-lg',
394
+ 'bg-purple-500/10 text-purple-500',
395
+ 'hover:bg-purple-500/20',
396
+ 'transition-all duration-200',
397
+ 'flex items-center gap-2',
398
+ )}
399
+ whileHover={{ scale: 1.02 }}
400
+ whileTap={{ scale: 0.98 }}
401
+ >
402
+ {isChecking ? (
403
+ <div className="i-ph:spinner-gap-bold animate-spin" />
404
+ ) : (
405
+ <div className="i-ph:arrows-clockwise" />
406
+ )}
407
+ Check Updates
408
+ </motion.button>
409
+ </div>
410
+
411
+ <div className="flex gap-4">
412
+ <div className="flex-1">
413
+ <div className="space-y-1">
414
+ <input
415
+ type="text"
416
+ className={classNames(
417
+ 'w-full px-4 py-3 rounded-xl',
418
+ 'bg-bolt-elements-background-depth-2 border border-bolt-elements-borderColor',
419
+ 'text-bolt-elements-textPrimary placeholder-bolt-elements-textTertiary',
420
+ 'focus:outline-none focus:ring-2 focus:ring-purple-500/30',
421
+ 'transition-all duration-200',
422
+ )}
423
+ placeholder="Search models or enter custom model name..."
424
+ value={searchQuery || modelString}
425
+ onChange={(e) => {
426
+ const value = e.target.value;
427
+ setSearchQuery(value);
428
+ setModelString(value);
429
+ }}
430
+ disabled={isInstalling}
431
+ />
432
+ <p className="text-xs text-bolt-elements-textTertiary px-1">
433
+ Browse models at{' '}
434
+ <a
435
+ href="https://ollama.com/library"
436
+ target="_blank"
437
+ rel="noopener noreferrer"
438
+ className="text-purple-500 hover:underline inline-flex items-center gap-0.5"
439
+ >
440
+ ollama.com/library
441
+ <div className="i-ph:arrow-square-out text-[10px]" />
442
+ </a>{' '}
443
+ and copy model names to install
444
+ </p>
445
+ </div>
446
+ </div>
447
+ <motion.button
448
+ onClick={() => handleInstallModel(modelString)}
449
+ disabled={!modelString || isInstalling}
450
+ className={classNames(
451
+ 'rounded-xl px-6 py-3',
452
+ 'bg-purple-500 text-white',
453
+ 'hover:bg-purple-600',
454
+ 'transition-all duration-200',
455
+ { 'opacity-50 cursor-not-allowed': !modelString || isInstalling },
456
+ )}
457
+ whileHover={{ scale: 1.02 }}
458
+ whileTap={{ scale: 0.98 }}
459
+ >
460
+ {isInstalling ? (
461
+ <div className="flex items-center gap-2">
462
+ <div className="i-ph:spinner-gap-bold animate-spin" />
463
+ <span>Installing...</span>
464
+ </div>
465
+ ) : (
466
+ <div className="flex items-center gap-2">
467
+ <OllamaIcon className="w-4 h-4" />
468
+ <span>Install Model</span>
469
+ </div>
470
+ )}
471
+ </motion.button>
472
+ </div>
473
+
474
+ <div className="flex flex-wrap gap-2">
475
+ {allTags.map((tag) => (
476
+ <button
477
+ key={tag}
478
+ onClick={() => {
479
+ setSelectedTags((prev) => (prev.includes(tag) ? prev.filter((t) => t !== tag) : [...prev, tag]));
480
+ }}
481
+ className={classNames(
482
+ 'px-3 py-1 rounded-full text-xs font-medium transition-all duration-200',
483
+ selectedTags.includes(tag)
484
+ ? 'bg-purple-500 text-white'
485
+ : 'bg-bolt-elements-background-depth-3 text-bolt-elements-textSecondary hover:bg-bolt-elements-background-depth-4',
486
+ )}
487
+ >
488
+ {tag}
489
+ </button>
490
+ ))}
491
+ </div>
492
+
493
+ <div className="grid grid-cols-1 gap-2">
494
+ {filteredModels.map((model) => (
495
+ <motion.div
496
+ key={model.name}
497
+ className={classNames(
498
+ 'flex items-start gap-2 p-3 rounded-lg',
499
+ 'bg-bolt-elements-background-depth-3',
500
+ 'hover:bg-bolt-elements-background-depth-4',
501
+ 'transition-all duration-200',
502
+ 'relative group',
503
+ )}
504
+ >
505
+ <OllamaIcon className="w-5 h-5 text-purple-500 mt-0.5 flex-shrink-0" />
506
+ <div className="flex-1 space-y-1.5">
507
+ <div className="flex items-start justify-between">
508
+ <div>
509
+ <p className="text-bolt-elements-textPrimary font-mono text-sm">{model.name}</p>
510
+ <p className="text-xs text-bolt-elements-textSecondary mt-0.5">{model.desc}</p>
511
+ </div>
512
+ <div className="text-right">
513
+ <span className="text-xs text-bolt-elements-textTertiary">{model.size}</span>
514
+ {model.installedVersion && (
515
+ <div className="mt-0.5 flex flex-col items-end gap-0.5">
516
+ <span className="text-xs text-bolt-elements-textTertiary">v{model.installedVersion}</span>
517
+ {model.needsUpdate && model.latestVersion && (
518
+ <span className="text-xs text-purple-500">v{model.latestVersion} available</span>
519
+ )}
520
+ </div>
521
+ )}
522
+ </div>
523
+ </div>
524
+ <div className="flex items-center justify-between">
525
+ <div className="flex flex-wrap gap-1">
526
+ {model.tags.map((tag) => (
527
+ <span
528
+ key={tag}
529
+ className="px-1.5 py-0.5 rounded-full text-[10px] bg-bolt-elements-background-depth-4 text-bolt-elements-textTertiary"
530
+ >
531
+ {tag}
532
+ </span>
533
+ ))}
534
+ </div>
535
+ <div className="flex gap-2">
536
+ {model.installedVersion ? (
537
+ model.needsUpdate ? (
538
+ <motion.button
539
+ onClick={() => handleUpdateModel(model.name)}
540
+ className={classNames(
541
+ 'px-2 py-0.5 rounded-lg text-xs',
542
+ 'bg-purple-500 text-white',
543
+ 'hover:bg-purple-600',
544
+ 'transition-all duration-200',
545
+ 'flex items-center gap-1',
546
+ )}
547
+ whileHover={{ scale: 1.02 }}
548
+ whileTap={{ scale: 0.98 }}
549
+ >
550
+ <div className="i-ph:arrows-clockwise text-xs" />
551
+ Update
552
+ </motion.button>
553
+ ) : (
554
+ <span className="px-2 py-0.5 rounded-lg text-xs text-green-500 bg-green-500/10">Up to date</span>
555
+ )
556
+ ) : (
557
+ <motion.button
558
+ onClick={() => handleInstallModel(model.name)}
559
+ className={classNames(
560
+ 'px-2 py-0.5 rounded-lg text-xs',
561
+ 'bg-purple-500 text-white',
562
+ 'hover:bg-purple-600',
563
+ 'transition-all duration-200',
564
+ 'flex items-center gap-1',
565
+ )}
566
+ whileHover={{ scale: 1.02 }}
567
+ whileTap={{ scale: 0.98 }}
568
+ >
569
+ <div className="i-ph:download text-xs" />
570
+ Install
571
+ </motion.button>
572
+ )}
573
+ </div>
574
+ </div>
575
+ </div>
576
+ </motion.div>
577
+ ))}
578
+ </div>
579
+
580
+ {installProgress && (
581
+ <motion.div initial={{ opacity: 0, y: 10 }} animate={{ opacity: 1, y: 0 }} className="space-y-2">
582
+ <div className="flex justify-between text-sm">
583
+ <span className="text-bolt-elements-textSecondary">{installProgress.status}</span>
584
+ <div className="flex items-center gap-4">
585
+ <span className="text-bolt-elements-textTertiary">
586
+ {installProgress.downloadedSize} / {installProgress.totalSize}
587
+ </span>
588
+ <span className="text-bolt-elements-textTertiary">{installProgress.speed}</span>
589
+ <span className="text-bolt-elements-textSecondary">{Math.round(installProgress.progress)}%</span>
590
+ </div>
591
+ </div>
592
+ <Progress value={installProgress.progress} className="h-1" />
593
+ </motion.div>
594
+ )}
595
+ </div>
596
+ );
597
+ }
app/components/{settings β†’ @settings/tabs}/providers/service-status/ServiceStatusTab.tsx RENAMED
File without changes
app/components/{settings β†’ @settings/tabs}/providers/service-status/base-provider.ts RENAMED
File without changes
app/components/{settings β†’ @settings/tabs}/providers/service-status/provider-factory.ts RENAMED
File without changes
app/components/{settings β†’ @settings/tabs}/providers/service-status/providers/amazon-bedrock.ts RENAMED
@@ -1,5 +1,5 @@
1
- import { BaseProviderChecker } from '~/components/settings/providers/service-status/base-provider';
2
- import type { StatusCheckResult } from '~/components/settings/providers/service-status/types';
3
 
4
  export class AmazonBedrockStatusChecker extends BaseProviderChecker {
5
  async checkStatus(): Promise<StatusCheckResult> {
 
1
+ import { BaseProviderChecker } from '~/components/@settings/tabs/providers/service-status/base-provider';
2
+ import type { StatusCheckResult } from '~/components/@settings/tabs/providers/service-status/types';
3
 
4
  export class AmazonBedrockStatusChecker extends BaseProviderChecker {
5
  async checkStatus(): Promise<StatusCheckResult> {
app/components/{settings β†’ @settings/tabs}/providers/service-status/providers/anthropic.ts RENAMED
@@ -1,5 +1,5 @@
1
- import { BaseProviderChecker } from '~/components/settings/providers/service-status/base-provider';
2
- import type { StatusCheckResult } from '~/components/settings/providers/service-status/types';
3
 
4
  export class AnthropicStatusChecker extends BaseProviderChecker {
5
  async checkStatus(): Promise<StatusCheckResult> {
 
1
+ import { BaseProviderChecker } from '~/components/@settings/tabs/providers/service-status/base-provider';
2
+ import type { StatusCheckResult } from '~/components/@settings/tabs/providers/service-status/types';
3
 
4
  export class AnthropicStatusChecker extends BaseProviderChecker {
5
  async checkStatus(): Promise<StatusCheckResult> {
app/components/{settings β†’ @settings/tabs}/providers/service-status/providers/cohere.ts RENAMED
@@ -1,5 +1,5 @@
1
- import { BaseProviderChecker } from '~/components/settings/providers/service-status/base-provider';
2
- import type { StatusCheckResult } from '~/components/settings/providers/service-status/types';
3
 
4
  export class CohereStatusChecker extends BaseProviderChecker {
5
  async checkStatus(): Promise<StatusCheckResult> {
 
1
+ import { BaseProviderChecker } from '~/components/@settings/tabs/providers/service-status/base-provider';
2
+ import type { StatusCheckResult } from '~/components/@settings/tabs/providers/service-status/types';
3
 
4
  export class CohereStatusChecker extends BaseProviderChecker {
5
  async checkStatus(): Promise<StatusCheckResult> {
app/components/{settings β†’ @settings/tabs}/providers/service-status/providers/deepseek.ts RENAMED
@@ -1,5 +1,5 @@
1
- import { BaseProviderChecker } from '~/components/settings/providers/service-status/base-provider';
2
- import type { StatusCheckResult } from '~/components/settings/providers/service-status/types';
3
 
4
  export class DeepseekStatusChecker extends BaseProviderChecker {
5
  async checkStatus(): Promise<StatusCheckResult> {
 
1
+ import { BaseProviderChecker } from '~/components/@settings/tabs/providers/service-status/base-provider';
2
+ import type { StatusCheckResult } from '~/components/@settings/tabs/providers/service-status/types';
3
 
4
  export class DeepseekStatusChecker extends BaseProviderChecker {
5
  async checkStatus(): Promise<StatusCheckResult> {
app/components/{settings β†’ @settings/tabs}/providers/service-status/providers/google.ts RENAMED
@@ -1,5 +1,5 @@
1
- import { BaseProviderChecker } from '~/components/settings/providers/service-status/base-provider';
2
- import type { StatusCheckResult } from '~/components/settings/providers/service-status/types';
3
 
4
  export class GoogleStatusChecker extends BaseProviderChecker {
5
  async checkStatus(): Promise<StatusCheckResult> {
 
1
+ import { BaseProviderChecker } from '~/components/@settings/tabs/providers/service-status/base-provider';
2
+ import type { StatusCheckResult } from '~/components/@settings/tabs/providers/service-status/types';
3
 
4
  export class GoogleStatusChecker extends BaseProviderChecker {
5
  async checkStatus(): Promise<StatusCheckResult> {
app/components/{settings β†’ @settings/tabs}/providers/service-status/providers/groq.ts RENAMED
@@ -1,5 +1,5 @@
1
- import { BaseProviderChecker } from '~/components/settings/providers/service-status/base-provider';
2
- import type { StatusCheckResult } from '~/components/settings/providers/service-status/types';
3
 
4
  export class GroqStatusChecker extends BaseProviderChecker {
5
  async checkStatus(): Promise<StatusCheckResult> {
 
1
+ import { BaseProviderChecker } from '~/components/@settings/tabs/providers/service-status/base-provider';
2
+ import type { StatusCheckResult } from '~/components/@settings/tabs/providers/service-status/types';
3
 
4
  export class GroqStatusChecker extends BaseProviderChecker {
5
  async checkStatus(): Promise<StatusCheckResult> {
app/components/{settings β†’ @settings/tabs}/providers/service-status/providers/huggingface.ts RENAMED
@@ -1,5 +1,5 @@
1
- import { BaseProviderChecker } from '~/components/settings/providers/service-status/base-provider';
2
- import type { StatusCheckResult } from '~/components/settings/providers/service-status/types';
3
 
4
  export class HuggingFaceStatusChecker extends BaseProviderChecker {
5
  async checkStatus(): Promise<StatusCheckResult> {
 
1
+ import { BaseProviderChecker } from '~/components/@settings/tabs/providers/service-status/base-provider';
2
+ import type { StatusCheckResult } from '~/components/@settings/tabs/providers/service-status/types';
3
 
4
  export class HuggingFaceStatusChecker extends BaseProviderChecker {
5
  async checkStatus(): Promise<StatusCheckResult> {
app/components/{settings β†’ @settings/tabs}/providers/service-status/providers/hyperbolic.ts RENAMED
@@ -1,5 +1,5 @@
1
- import { BaseProviderChecker } from '~/components/settings/providers/service-status/base-provider';
2
- import type { StatusCheckResult } from '~/components/settings/providers/service-status/types';
3
 
4
  export class HyperbolicStatusChecker extends BaseProviderChecker {
5
  async checkStatus(): Promise<StatusCheckResult> {
 
1
+ import { BaseProviderChecker } from '~/components/@settings/tabs/providers/service-status/base-provider';
2
+ import type { StatusCheckResult } from '~/components/@settings/tabs/providers/service-status/types';
3
 
4
  export class HyperbolicStatusChecker extends BaseProviderChecker {
5
  async checkStatus(): Promise<StatusCheckResult> {
app/components/{settings β†’ @settings/tabs}/providers/service-status/providers/mistral.ts RENAMED
@@ -1,5 +1,5 @@
1
- import { BaseProviderChecker } from '~/components/settings/providers/service-status/base-provider';
2
- import type { StatusCheckResult } from '~/components/settings/providers/service-status/types';
3
 
4
  export class MistralStatusChecker extends BaseProviderChecker {
5
  async checkStatus(): Promise<StatusCheckResult> {
 
1
+ import { BaseProviderChecker } from '~/components/@settings/tabs/providers/service-status/base-provider';
2
+ import type { StatusCheckResult } from '~/components/@settings/tabs/providers/service-status/types';
3
 
4
  export class MistralStatusChecker extends BaseProviderChecker {
5
  async checkStatus(): Promise<StatusCheckResult> {
app/components/{settings β†’ @settings/tabs}/providers/service-status/providers/openai.ts RENAMED
@@ -1,5 +1,5 @@
1
- import { BaseProviderChecker } from '~/components/settings/providers/service-status/base-provider';
2
- import type { StatusCheckResult } from '~/components/settings/providers/service-status/types';
3
 
4
  export class OpenAIStatusChecker extends BaseProviderChecker {
5
  async checkStatus(): Promise<StatusCheckResult> {
 
1
+ import { BaseProviderChecker } from '~/components/@settings/tabs/providers/service-status/base-provider';
2
+ import type { StatusCheckResult } from '~/components/@settings/tabs/providers/service-status/types';
3
 
4
  export class OpenAIStatusChecker extends BaseProviderChecker {
5
  async checkStatus(): Promise<StatusCheckResult> {
app/components/{settings β†’ @settings/tabs}/providers/service-status/providers/openrouter.ts RENAMED
@@ -1,5 +1,5 @@
1
- import { BaseProviderChecker } from '~/components/settings/providers/service-status/base-provider';
2
- import type { StatusCheckResult } from '~/components/settings/providers/service-status/types';
3
 
4
  export class OpenRouterStatusChecker extends BaseProviderChecker {
5
  async checkStatus(): Promise<StatusCheckResult> {
 
1
+ import { BaseProviderChecker } from '~/components/@settings/tabs/providers/service-status/base-provider';
2
+ import type { StatusCheckResult } from '~/components/@settings/tabs/providers/service-status/types';
3
 
4
  export class OpenRouterStatusChecker extends BaseProviderChecker {
5
  async checkStatus(): Promise<StatusCheckResult> {
app/components/{settings β†’ @settings/tabs}/providers/service-status/providers/perplexity.ts RENAMED
@@ -1,5 +1,5 @@
1
- import { BaseProviderChecker } from '~/components/settings/providers/service-status/base-provider';
2
- import type { StatusCheckResult } from '~/components/settings/providers/service-status/types';
3
 
4
  export class PerplexityStatusChecker extends BaseProviderChecker {
5
  async checkStatus(): Promise<StatusCheckResult> {
 
1
+ import { BaseProviderChecker } from '~/components/@settings/tabs/providers/service-status/base-provider';
2
+ import type { StatusCheckResult } from '~/components/@settings/tabs/providers/service-status/types';
3
 
4
  export class PerplexityStatusChecker extends BaseProviderChecker {
5
  async checkStatus(): Promise<StatusCheckResult> {
app/components/{settings β†’ @settings/tabs}/providers/service-status/providers/together.ts RENAMED
@@ -1,5 +1,5 @@
1
- import { BaseProviderChecker } from '~/components/settings/providers/service-status/base-provider';
2
- import type { StatusCheckResult } from '~/components/settings/providers/service-status/types';
3
 
4
  export class TogetherStatusChecker extends BaseProviderChecker {
5
  async checkStatus(): Promise<StatusCheckResult> {
 
1
+ import { BaseProviderChecker } from '~/components/@settings/tabs/providers/service-status/base-provider';
2
+ import type { StatusCheckResult } from '~/components/@settings/tabs/providers/service-status/types';
3
 
4
  export class TogetherStatusChecker extends BaseProviderChecker {
5
  async checkStatus(): Promise<StatusCheckResult> {
app/components/{settings β†’ @settings/tabs}/providers/service-status/providers/xai.ts RENAMED
@@ -1,5 +1,5 @@
1
- import { BaseProviderChecker } from '~/components/settings/providers/service-status/base-provider';
2
- import type { StatusCheckResult } from '~/components/settings/providers/service-status/types';
3
 
4
  export class XAIStatusChecker extends BaseProviderChecker {
5
  async checkStatus(): Promise<StatusCheckResult> {
 
1
+ import { BaseProviderChecker } from '~/components/@settings/tabs/providers/service-status/base-provider';
2
+ import type { StatusCheckResult } from '~/components/@settings/tabs/providers/service-status/types';
3
 
4
  export class XAIStatusChecker extends BaseProviderChecker {
5
  async checkStatus(): Promise<StatusCheckResult> {
app/components/{settings β†’ @settings/tabs}/providers/service-status/types.ts RENAMED
File without changes
app/components/{settings/providers β†’ @settings/tabs/providers/status}/ServiceStatusTab.tsx RENAMED
File without changes
app/components/{settings β†’ @settings/tabs}/settings/SettingsTab.tsx RENAMED
@@ -4,7 +4,7 @@ import { toast } from 'react-toastify';
4
  import { classNames } from '~/utils/classNames';
5
  import { Switch } from '~/components/ui/Switch';
6
  import { themeStore, kTheme } from '~/lib/stores/theme';
7
- import type { UserProfile } from '~/components/settings/settings.types';
8
  import { useStore } from '@nanostores/react';
9
  import { shortcutsStore } from '~/lib/stores/settings';
10
 
 
4
  import { classNames } from '~/utils/classNames';
5
  import { Switch } from '~/components/ui/Switch';
6
  import { themeStore, kTheme } from '~/lib/stores/theme';
7
+ import type { UserProfile } from '~/components/@settings/core/types';
8
  import { useStore } from '@nanostores/react';
9
  import { shortcutsStore } from '~/lib/stores/settings';
10
 
app/components/{settings β†’ @settings/tabs}/task-manager/TaskManagerTab.tsx RENAMED
@@ -1,4 +1,5 @@
1
- import React, { useEffect, useState, useRef, useCallback } from 'react';
 
2
  import { classNames } from '~/utils/classNames';
3
  import { Line } from 'react-chartjs-2';
4
  import {
@@ -12,6 +13,9 @@ import {
12
  Legend,
13
  } from 'chart.js';
14
  import { toast } from 'react-toastify'; // Import toast
 
 
 
15
 
16
  // Register ChartJS components
17
  ChartJS.register(CategoryScale, LinearScale, PointElement, LineElement, Title, Tooltip, Legend);
@@ -74,12 +78,6 @@ interface SystemMetrics {
74
  lcp: number;
75
  };
76
  };
77
- storage: {
78
- total: number;
79
- used: number;
80
- free: number;
81
- type: string;
82
- };
83
  health: {
84
  score: number;
85
  issues: string[];
@@ -134,37 +132,46 @@ declare global {
134
  }
135
  }
136
 
137
- const MAX_HISTORY_POINTS = 60; // 1 minute of history at 1s intervals
138
- const BATTERY_THRESHOLD = 20; // Enable energy saver when battery below 20%
139
  const UPDATE_INTERVALS = {
140
  normal: {
141
- metrics: 1000, // 1s
 
142
  },
143
  energySaver: {
144
- metrics: 5000, // 5s
 
145
  },
146
  };
147
 
148
- // Energy consumption estimates (milliwatts)
149
- const ENERGY_COSTS = {
150
- update: 2, // mW per update
151
- apiCall: 5, // mW per API call
152
- rendering: 1, // mW per render
 
 
 
 
 
 
 
 
 
153
  };
154
 
155
- const PERFORMANCE_THRESHOLDS = {
156
- cpu: { warning: 70, critical: 90 },
157
- memory: { warning: 80, critical: 95 },
158
- fps: { warning: 30, critical: 15 },
159
- loadTime: { warning: 3000, critical: 5000 },
160
  };
161
 
 
162
  const POWER_PROFILES: PowerProfile[] = [
163
  {
164
  name: 'Performance',
165
- description: 'Maximum performance, higher power consumption',
166
  settings: {
167
- updateInterval: 1000,
168
  enableAnimations: true,
169
  backgroundProcessing: true,
170
  networkThrottling: false,
@@ -172,7 +179,7 @@ const POWER_PROFILES: PowerProfile[] = [
172
  },
173
  {
174
  name: 'Balanced',
175
- description: 'Balance between performance and power saving',
176
  settings: {
177
  updateInterval: 2000,
178
  enableAnimations: true,
@@ -181,10 +188,10 @@ const POWER_PROFILES: PowerProfile[] = [
181
  },
182
  },
183
  {
184
- name: 'Power Saver',
185
- description: 'Maximum power saving, reduced performance',
186
  settings: {
187
- updateInterval: 5000,
188
  enableAnimations: false,
189
  backgroundProcessing: false,
190
  networkThrottling: true,
@@ -192,50 +199,271 @@ const POWER_PROFILES: PowerProfile[] = [
192
  },
193
  ];
194
 
195
- export default function TaskManagerTab() {
196
- const [metrics, setMetrics] = useState<SystemMetrics>({
197
- cpu: { usage: 0, cores: [] },
198
- memory: { used: 0, total: 0, percentage: 0, heap: { used: 0, total: 0, limit: 0 } },
199
- uptime: 0,
200
- network: { downlink: 0, latency: 0, type: 'unknown', bytesReceived: 0, bytesSent: 0 },
201
- performance: {
202
- fps: 0,
203
- pageLoad: 0,
204
- domReady: 0,
205
- resources: { total: 0, size: 0, loadTime: 0 },
206
- timing: { ttfb: 0, fcp: 0, lcp: 0 },
 
 
207
  },
208
- storage: { total: 0, used: 0, free: 0, type: 'unknown' },
209
- health: { score: 0, issues: [], suggestions: [] },
210
- });
211
- const [metricsHistory, setMetricsHistory] = useState<MetricsHistory>({
212
- timestamps: [],
213
- cpu: [],
214
- memory: [],
215
- battery: [],
216
- network: [],
217
- });
218
- const [energySaverMode, setEnergySaverMode] = useState<boolean>(() => {
219
- // Initialize from localStorage, default to false
220
- const saved = localStorage.getItem('energySaverMode');
221
- return saved ? JSON.parse(saved) : false;
222
- });
223
-
224
- const [autoEnergySaver, setAutoEnergySaver] = useState<boolean>(() => {
225
- // Initialize from localStorage, default to false
226
- const saved = localStorage.getItem('autoEnergySaver');
227
- return saved ? JSON.parse(saved) : false;
228
- });
229
-
230
- const [energySavings, setEnergySavings] = useState<EnergySavings>({
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
231
  updatesReduced: 0,
232
  timeInSaverMode: 0,
233
  estimatedEnergySaved: 0,
234
- });
235
-
236
- const saverModeStartTime = useRef<number | null>(null);
237
- const [selectedProfile, setSelectedProfile] = useState<PowerProfile>(POWER_PROFILES[1]); // Default to Balanced
238
  const [alerts, setAlerts] = useState<PerformanceAlert[]>([]);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
239
 
240
  // Handle energy saver mode changes
241
  const handleEnergySaverChange = (checked: boolean) => {
@@ -296,48 +524,6 @@ export default function TaskManagerTab() {
296
  return () => clearInterval(interval);
297
  }, [updateEnergySavings]);
298
 
299
- // Get detailed performance metrics
300
- const getPerformanceMetrics = async (): Promise<Partial<SystemMetrics['performance']>> => {
301
- try {
302
- // Get FPS
303
- const fps = await measureFrameRate();
304
-
305
- // Get page load metrics
306
- const navigation = performance.getEntriesByType('navigation')[0] as PerformanceNavigationTiming;
307
- const pageLoad = navigation.loadEventEnd - navigation.startTime;
308
- const domReady = navigation.domContentLoadedEventEnd - navigation.startTime;
309
-
310
- // Get resource metrics
311
- const resources = performance.getEntriesByType('resource');
312
- const resourceMetrics = {
313
- total: resources.length,
314
- size: resources.reduce((total, r) => total + (r as any).transferSize || 0, 0),
315
- loadTime: Math.max(...resources.map((r) => r.duration)),
316
- };
317
-
318
- // Get Web Vitals
319
- const ttfb = navigation.responseStart - navigation.requestStart;
320
- const paintEntries = performance.getEntriesByType('paint');
321
- const fcp = paintEntries.find((entry) => entry.name === 'first-contentful-paint')?.startTime || 0;
322
- const lcpEntry = await getLargestContentfulPaint();
323
-
324
- return {
325
- fps,
326
- pageLoad,
327
- domReady,
328
- resources: resourceMetrics,
329
- timing: {
330
- ttfb,
331
- fcp,
332
- lcp: lcpEntry?.startTime || 0,
333
- },
334
- };
335
- } catch (error) {
336
- console.error('Failed to get performance metrics:', error);
337
- return {};
338
- }
339
- };
340
-
341
  // Measure frame rate
342
  const measureFrameRate = async (): Promise<number> => {
343
  return new Promise((resolve) => {
@@ -486,12 +672,6 @@ export default function TaskManagerTab() {
486
  battery: batteryInfo,
487
  network: networkInfo,
488
  performance: performanceMetrics as SystemMetrics['performance'],
489
- storage: {
490
- total: 0,
491
- used: 0,
492
- free: 0,
493
- type: 'unknown',
494
- },
495
  health: { score: 0, issues: [], suggestions: [] },
496
  };
497
 
@@ -597,23 +777,6 @@ export default function TaskManagerTab() {
597
  };
598
  }, [energySaverMode]);
599
 
600
- // Initial update effect
601
- useEffect((): (() => void) => {
602
- // Initial update
603
- updateMetrics();
604
-
605
- // Set up intervals for live updates
606
- const metricsInterval = setInterval(
607
- updateMetrics,
608
- energySaverMode ? UPDATE_INTERVALS.energySaver.metrics : UPDATE_INTERVALS.normal.metrics,
609
- );
610
-
611
- // Cleanup on unmount
612
- return () => {
613
- clearInterval(metricsInterval);
614
- };
615
- }, [energySaverMode]); // Re-create intervals when energy saver mode changes
616
-
617
  const getUsageColor = (usage: number): string => {
618
  if (usage > 80) {
619
  return 'text-red-500';
@@ -761,6 +924,7 @@ export default function TaskManagerTab() {
761
  onChange={(e) => handleAutoEnergySaverChange(e.target.checked)}
762
  className="form-checkbox h-4 w-4 text-purple-600 rounded border-gray-300 dark:border-gray-700"
763
  />
 
764
  <label htmlFor="autoEnergySaver" className="text-sm text-bolt-elements-textSecondary">
765
  Auto Energy Saver
766
  </label>
@@ -774,6 +938,7 @@ export default function TaskManagerTab() {
774
  disabled={autoEnergySaver}
775
  className="form-checkbox h-4 w-4 text-purple-600 rounded border-gray-300 dark:border-gray-700 disabled:opacity-50"
776
  />
 
777
  <label
778
  htmlFor="energySaver"
779
  className={classNames('text-sm text-bolt-elements-textSecondary', { 'opacity-50': autoEnergySaver })}
@@ -782,24 +947,43 @@ export default function TaskManagerTab() {
782
  {energySaverMode && <span className="ml-2 text-xs text-bolt-elements-textSecondary">Active</span>}
783
  </label>
784
  </div>
785
- <select
786
- value={selectedProfile.name}
787
- onChange={(e) => {
788
- const profile = POWER_PROFILES.find((p) => p.name === e.target.value);
789
-
790
- if (profile) {
791
- setSelectedProfile(profile);
792
- toast.success(`Switched to ${profile.name} power profile`);
793
- }
794
- }}
795
- className="px-3 py-1 rounded-md bg-[#F8F8F8] dark:bg-[#141414] border border-[#E5E5E5] dark:border-[#1A1A1A] text-sm"
796
- >
797
- {POWER_PROFILES.map((profile) => (
798
- <option key={profile.name} value={profile.name}>
799
- {profile.name}
800
- </option>
801
- ))}
802
- </select>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
803
  </div>
804
  </div>
805
  <div className="text-sm text-bolt-elements-textSecondary">{selectedProfile.description}</div>
@@ -981,30 +1165,6 @@ export default function TaskManagerTab() {
981
  </div>
982
  )}
983
 
984
- {/* Storage Section */}
985
- <div className="flex flex-col gap-2 rounded-lg bg-[#F8F8F8] dark:bg-[#141414] p-4">
986
- <div className="flex items-center justify-between">
987
- <span className="text-sm text-bolt-elements-textSecondary">Storage</span>
988
- <span className="text-sm font-medium text-bolt-elements-textPrimary">
989
- {formatBytes(metrics.storage.used)} / {formatBytes(metrics.storage.total)}
990
- </span>
991
- </div>
992
- <div className="w-full h-2 bg-gray-200 dark:bg-gray-700 rounded-full overflow-hidden">
993
- <div
994
- className={classNames('h-full transition-all duration-300', {
995
- 'bg-green-500': metrics.storage.used / metrics.storage.total < 0.7,
996
- 'bg-yellow-500':
997
- metrics.storage.used / metrics.storage.total >= 0.7 &&
998
- metrics.storage.used / metrics.storage.total < 0.9,
999
- 'bg-red-500': metrics.storage.used / metrics.storage.total >= 0.9,
1000
- })}
1001
- style={{ width: `${(metrics.storage.used / metrics.storage.total) * 100}%` }}
1002
- />
1003
- </div>
1004
- <div className="text-xs text-bolt-elements-textSecondary mt-2">Free: {formatBytes(metrics.storage.free)}</div>
1005
- <div className="text-xs text-bolt-elements-textSecondary">Type: {metrics.storage.type}</div>
1006
- </div>
1007
-
1008
  {/* Performance Alerts */}
1009
  {alerts.length > 0 && (
1010
  <div className="flex flex-col gap-2 rounded-lg bg-[#F8F8F8] dark:bg-[#141414] p-4">
@@ -1071,7 +1231,9 @@ export default function TaskManagerTab() {
1071
  </div>
1072
  </div>
1073
  );
1074
- }
 
 
1075
 
1076
  // Helper function to format bytes
1077
  const formatBytes = (bytes: number): string => {
 
1
+ import * as React from 'react';
2
+ import { useEffect, useState, useRef, useCallback } from 'react';
3
  import { classNames } from '~/utils/classNames';
4
  import { Line } from 'react-chartjs-2';
5
  import {
 
13
  Legend,
14
  } from 'chart.js';
15
  import { toast } from 'react-toastify'; // Import toast
16
+ import { useUpdateCheck } from '~/lib/hooks/useUpdateCheck';
17
+ import { tabConfigurationStore, type TabConfig } from '~/lib/stores/tabConfigurationStore';
18
+ import { useStore } from 'zustand';
19
 
20
  // Register ChartJS components
21
  ChartJS.register(CategoryScale, LinearScale, PointElement, LineElement, Title, Tooltip, Legend);
 
78
  lcp: number;
79
  };
80
  };
 
 
 
 
 
 
81
  health: {
82
  score: number;
83
  issues: string[];
 
132
  }
133
  }
134
 
135
+ // Constants for update intervals
 
136
  const UPDATE_INTERVALS = {
137
  normal: {
138
+ metrics: 1000, // 1 second
139
+ animation: 16, // ~60fps
140
  },
141
  energySaver: {
142
+ metrics: 5000, // 5 seconds
143
+ animation: 32, // ~30fps
144
  },
145
  };
146
 
147
+ // Constants for performance thresholds
148
+ const PERFORMANCE_THRESHOLDS = {
149
+ cpu: {
150
+ warning: 70,
151
+ critical: 90,
152
+ },
153
+ memory: {
154
+ warning: 80,
155
+ critical: 95,
156
+ },
157
+ fps: {
158
+ warning: 30,
159
+ critical: 15,
160
+ },
161
  };
162
 
163
+ // Constants for energy calculations
164
+ const ENERGY_COSTS = {
165
+ update: 0.1, // mWh per update
 
 
166
  };
167
 
168
+ // Default power profiles
169
  const POWER_PROFILES: PowerProfile[] = [
170
  {
171
  name: 'Performance',
172
+ description: 'Maximum performance with frequent updates',
173
  settings: {
174
+ updateInterval: UPDATE_INTERVALS.normal.metrics,
175
  enableAnimations: true,
176
  backgroundProcessing: true,
177
  networkThrottling: false,
 
179
  },
180
  {
181
  name: 'Balanced',
182
+ description: 'Optimal balance between performance and energy efficiency',
183
  settings: {
184
  updateInterval: 2000,
185
  enableAnimations: true,
 
188
  },
189
  },
190
  {
191
+ name: 'Energy Saver',
192
+ description: 'Maximum energy efficiency with reduced updates',
193
  settings: {
194
+ updateInterval: UPDATE_INTERVALS.energySaver.metrics,
195
  enableAnimations: false,
196
  backgroundProcessing: false,
197
  networkThrottling: true,
 
199
  },
200
  ];
201
 
202
+ // Default metrics state
203
+ const DEFAULT_METRICS_STATE: SystemMetrics = {
204
+ cpu: {
205
+ usage: 0,
206
+ cores: [],
207
+ },
208
+ memory: {
209
+ used: 0,
210
+ total: 0,
211
+ percentage: 0,
212
+ heap: {
213
+ used: 0,
214
+ total: 0,
215
+ limit: 0,
216
  },
217
+ },
218
+ uptime: 0,
219
+ network: {
220
+ downlink: 0,
221
+ latency: 0,
222
+ type: 'unknown',
223
+ bytesReceived: 0,
224
+ bytesSent: 0,
225
+ },
226
+ performance: {
227
+ fps: 0,
228
+ pageLoad: 0,
229
+ domReady: 0,
230
+ resources: {
231
+ total: 0,
232
+ size: 0,
233
+ loadTime: 0,
234
+ },
235
+ timing: {
236
+ ttfb: 0,
237
+ fcp: 0,
238
+ lcp: 0,
239
+ },
240
+ },
241
+ health: {
242
+ score: 0,
243
+ issues: [],
244
+ suggestions: [],
245
+ },
246
+ };
247
+
248
+ // Default metrics history
249
+ const DEFAULT_METRICS_HISTORY: MetricsHistory = {
250
+ timestamps: Array(10).fill(new Date().toLocaleTimeString()),
251
+ cpu: Array(10).fill(0),
252
+ memory: Array(10).fill(0),
253
+ battery: Array(10).fill(0),
254
+ network: Array(10).fill(0),
255
+ };
256
+
257
+ // Battery threshold for auto energy saver mode
258
+ const BATTERY_THRESHOLD = 20; // percentage
259
+
260
+ // Maximum number of history points to keep
261
+ const MAX_HISTORY_POINTS = 10;
262
+
263
+ const TaskManagerTab: React.FC = () => {
264
+ // Initialize metrics state with defaults
265
+ const [metrics, setMetrics] = useState<SystemMetrics>(() => DEFAULT_METRICS_STATE);
266
+ const [metricsHistory, setMetricsHistory] = useState<MetricsHistory>(() => DEFAULT_METRICS_HISTORY);
267
+ const [energySaverMode, setEnergySaverMode] = useState<boolean>(false);
268
+ const [autoEnergySaver, setAutoEnergySaver] = useState<boolean>(false);
269
+ const [energySavings, setEnergySavings] = useState<EnergySavings>(() => ({
270
  updatesReduced: 0,
271
  timeInSaverMode: 0,
272
  estimatedEnergySaved: 0,
273
+ }));
274
+ const [selectedProfile, setSelectedProfile] = useState<PowerProfile>(() => POWER_PROFILES[1]);
 
 
275
  const [alerts, setAlerts] = useState<PerformanceAlert[]>([]);
276
+ const saverModeStartTime = useRef<number | null>(null);
277
+
278
+ // Get update status and tab configuration
279
+ const { hasUpdate } = useUpdateCheck();
280
+ const tabConfig = useStore(tabConfigurationStore);
281
+
282
+ const resetTabConfiguration = useCallback(() => {
283
+ tabConfig.reset();
284
+ return tabConfig.get();
285
+ }, [tabConfig]);
286
+
287
+ // Effect to handle tab visibility
288
+ useEffect(() => {
289
+ const handleTabVisibility = () => {
290
+ const currentConfig = tabConfig.get();
291
+ const controlledTabs = ['debug', 'update'];
292
+
293
+ // Update visibility based on conditions
294
+ const updatedTabs = currentConfig.userTabs.map((tab: TabConfig) => {
295
+ if (controlledTabs.includes(tab.id)) {
296
+ return {
297
+ ...tab,
298
+ visible: tab.id === 'debug' ? metrics.cpu.usage > 80 : hasUpdate,
299
+ };
300
+ }
301
+
302
+ return tab;
303
+ });
304
+
305
+ tabConfig.set({
306
+ ...currentConfig,
307
+ userTabs: updatedTabs,
308
+ });
309
+ };
310
+
311
+ const checkInterval = setInterval(handleTabVisibility, 5000);
312
+
313
+ return () => {
314
+ clearInterval(checkInterval);
315
+ };
316
+ }, [metrics.cpu.usage, hasUpdate, tabConfig]);
317
+
318
+ // Effect to handle reset and initialization
319
+ useEffect(() => {
320
+ const resetToDefaults = () => {
321
+ console.log('TaskManagerTab: Resetting to defaults');
322
+
323
+ // Reset metrics and local state
324
+ setMetrics(DEFAULT_METRICS_STATE);
325
+ setMetricsHistory(DEFAULT_METRICS_HISTORY);
326
+ setEnergySaverMode(false);
327
+ setAutoEnergySaver(false);
328
+ setEnergySavings({
329
+ updatesReduced: 0,
330
+ timeInSaverMode: 0,
331
+ estimatedEnergySaved: 0,
332
+ });
333
+ setSelectedProfile(POWER_PROFILES[1]);
334
+ setAlerts([]);
335
+ saverModeStartTime.current = null;
336
+
337
+ // Reset tab configuration to ensure proper visibility
338
+ const defaultConfig = resetTabConfiguration();
339
+ console.log('TaskManagerTab: Reset tab configuration:', defaultConfig);
340
+ };
341
+
342
+ // Listen for both storage changes and custom reset event
343
+ const handleReset = (event: Event | StorageEvent) => {
344
+ if (event instanceof StorageEvent) {
345
+ if (event.key === 'tabConfiguration' && event.newValue === null) {
346
+ resetToDefaults();
347
+ }
348
+ } else if (event instanceof CustomEvent && event.type === 'tabConfigReset') {
349
+ resetToDefaults();
350
+ }
351
+ };
352
+
353
+ // Initial setup
354
+ const initializeTab = async () => {
355
+ try {
356
+ // Load saved preferences
357
+ const savedEnergySaver = localStorage.getItem('energySaverMode');
358
+ const savedAutoSaver = localStorage.getItem('autoEnergySaver');
359
+ const savedProfile = localStorage.getItem('selectedProfile');
360
+
361
+ if (savedEnergySaver) {
362
+ setEnergySaverMode(JSON.parse(savedEnergySaver));
363
+ }
364
+
365
+ if (savedAutoSaver) {
366
+ setAutoEnergySaver(JSON.parse(savedAutoSaver));
367
+ }
368
+
369
+ if (savedProfile) {
370
+ const profile = POWER_PROFILES.find((p) => p.name === savedProfile);
371
+
372
+ if (profile) {
373
+ setSelectedProfile(profile);
374
+ }
375
+ }
376
+
377
+ await updateMetrics();
378
+ } catch (error) {
379
+ console.error('Failed to initialize TaskManagerTab:', error);
380
+ resetToDefaults();
381
+ }
382
+ };
383
+
384
+ window.addEventListener('storage', handleReset);
385
+ window.addEventListener('tabConfigReset', handleReset);
386
+ initializeTab();
387
+
388
+ return () => {
389
+ window.removeEventListener('storage', handleReset);
390
+ window.removeEventListener('tabConfigReset', handleReset);
391
+ };
392
+ }, []);
393
+
394
+ // Get detailed performance metrics
395
+ const getPerformanceMetrics = async (): Promise<Partial<SystemMetrics['performance']>> => {
396
+ try {
397
+ // Get FPS
398
+ const fps = await measureFrameRate();
399
+
400
+ // Get page load metrics
401
+ const navigation = performance.getEntriesByType('navigation')[0] as PerformanceNavigationTiming;
402
+ const pageLoad = navigation.loadEventEnd - navigation.startTime;
403
+ const domReady = navigation.domContentLoadedEventEnd - navigation.startTime;
404
+
405
+ // Get resource metrics
406
+ const resources = performance.getEntriesByType('resource') as PerformanceResourceTiming[];
407
+ const resourceMetrics = {
408
+ total: resources.length,
409
+ size: resources.reduce((total, r) => total + (r.transferSize || 0), 0),
410
+ loadTime: Math.max(0, ...resources.map((r) => r.duration)),
411
+ };
412
+
413
+ // Get Web Vitals
414
+ const ttfb = navigation.responseStart - navigation.requestStart;
415
+ const paintEntries = performance.getEntriesByType('paint');
416
+ const fcp = paintEntries.find((entry) => entry.name === 'first-contentful-paint')?.startTime || 0;
417
+ const lcpEntry = await getLargestContentfulPaint();
418
+
419
+ return {
420
+ fps,
421
+ pageLoad,
422
+ domReady,
423
+ resources: resourceMetrics,
424
+ timing: {
425
+ ttfb,
426
+ fcp,
427
+ lcp: lcpEntry?.startTime || 0,
428
+ },
429
+ };
430
+ } catch (error) {
431
+ console.error('Failed to get performance metrics:', error);
432
+ return {};
433
+ }
434
+ };
435
+
436
+ // Single useEffect for metrics updates
437
+ useEffect(() => {
438
+ let isComponentMounted = true;
439
+
440
+ const updateMetricsWrapper = async () => {
441
+ if (!isComponentMounted) {
442
+ return;
443
+ }
444
+
445
+ try {
446
+ await updateMetrics();
447
+ } catch (error) {
448
+ console.error('Failed to update metrics:', error);
449
+ }
450
+ };
451
+
452
+ // Initial update
453
+ updateMetricsWrapper();
454
+
455
+ // Set up interval with immediate assignment
456
+ const metricsInterval = setInterval(
457
+ updateMetricsWrapper,
458
+ energySaverMode ? UPDATE_INTERVALS.energySaver.metrics : UPDATE_INTERVALS.normal.metrics,
459
+ );
460
+
461
+ // Cleanup function
462
+ return () => {
463
+ isComponentMounted = false;
464
+ clearInterval(metricsInterval);
465
+ };
466
+ }, [energySaverMode]); // Only depend on energySaverMode
467
 
468
  // Handle energy saver mode changes
469
  const handleEnergySaverChange = (checked: boolean) => {
 
524
  return () => clearInterval(interval);
525
  }, [updateEnergySavings]);
526
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
527
  // Measure frame rate
528
  const measureFrameRate = async (): Promise<number> => {
529
  return new Promise((resolve) => {
 
672
  battery: batteryInfo,
673
  network: networkInfo,
674
  performance: performanceMetrics as SystemMetrics['performance'],
 
 
 
 
 
 
675
  health: { score: 0, issues: [], suggestions: [] },
676
  };
677
 
 
777
  };
778
  }, [energySaverMode]);
779
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
780
  const getUsageColor = (usage: number): string => {
781
  if (usage > 80) {
782
  return 'text-red-500';
 
924
  onChange={(e) => handleAutoEnergySaverChange(e.target.checked)}
925
  className="form-checkbox h-4 w-4 text-purple-600 rounded border-gray-300 dark:border-gray-700"
926
  />
927
+ <div className="i-ph:gauge-duotone w-4 h-4 text-bolt-elements-textSecondary" />
928
  <label htmlFor="autoEnergySaver" className="text-sm text-bolt-elements-textSecondary">
929
  Auto Energy Saver
930
  </label>
 
938
  disabled={autoEnergySaver}
939
  className="form-checkbox h-4 w-4 text-purple-600 rounded border-gray-300 dark:border-gray-700 disabled:opacity-50"
940
  />
941
+ <div className="i-ph:leaf-duotone w-4 h-4 text-bolt-elements-textSecondary" />
942
  <label
943
  htmlFor="energySaver"
944
  className={classNames('text-sm text-bolt-elements-textSecondary', { 'opacity-50': autoEnergySaver })}
 
947
  {energySaverMode && <span className="ml-2 text-xs text-bolt-elements-textSecondary">Active</span>}
948
  </label>
949
  </div>
950
+ <div className="relative">
951
+ <select
952
+ value={selectedProfile.name}
953
+ onChange={(e) => {
954
+ const profile = POWER_PROFILES.find((p) => p.name === e.target.value);
955
+
956
+ if (profile) {
957
+ setSelectedProfile(profile);
958
+ toast.success(`Switched to ${profile.name} power profile`);
959
+ }
960
+ }}
961
+ className="pl-8 pr-8 py-1.5 rounded-md bg-bolt-background-secondary dark:bg-[#1E1E1E] border border-bolt-border dark:border-bolt-borderDark text-sm text-bolt-elements-textPrimary dark:text-bolt-elements-textPrimaryDark hover:border-bolt-action-primary dark:hover:border-bolt-action-primary focus:outline-none focus:ring-1 focus:ring-bolt-action-primary appearance-none min-w-[160px] cursor-pointer transition-colors duration-150"
962
+ style={{ WebkitAppearance: 'none', MozAppearance: 'none' }}
963
+ >
964
+ {POWER_PROFILES.map((profile) => (
965
+ <option
966
+ key={profile.name}
967
+ value={profile.name}
968
+ className="py-2 px-3 bg-bolt-background-secondary dark:bg-[#1E1E1E] text-bolt-elements-textPrimary dark:text-bolt-elements-textPrimaryDark hover:bg-bolt-background-tertiary dark:hover:bg-bolt-backgroundDark-tertiary cursor-pointer"
969
+ >
970
+ {profile.name}
971
+ </option>
972
+ ))}
973
+ </select>
974
+ <div className="absolute left-2 top-1/2 -translate-y-1/2 pointer-events-none">
975
+ <div
976
+ className={classNames('w-4 h-4 text-bolt-elements-textSecondary', {
977
+ 'i-ph:lightning-fill text-yellow-500': selectedProfile.name === 'Performance',
978
+ 'i-ph:scales-fill text-blue-500': selectedProfile.name === 'Balanced',
979
+ 'i-ph:leaf-fill text-green-500': selectedProfile.name === 'Energy Saver',
980
+ })}
981
+ />
982
+ </div>
983
+ <div className="absolute right-2 top-1/2 -translate-y-1/2 pointer-events-none">
984
+ <div className="i-ph:caret-down w-4 h-4 text-bolt-elements-textSecondary opacity-75" />
985
+ </div>
986
+ </div>
987
  </div>
988
  </div>
989
  <div className="text-sm text-bolt-elements-textSecondary">{selectedProfile.description}</div>
 
1165
  </div>
1166
  )}
1167
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1168
  {/* Performance Alerts */}
1169
  {alerts.length > 0 && (
1170
  <div className="flex flex-col gap-2 rounded-lg bg-[#F8F8F8] dark:bg-[#141414] p-4">
 
1231
  </div>
1232
  </div>
1233
  );
1234
+ };
1235
+
1236
+ export default React.memo(TaskManagerTab);
1237
 
1238
  // Helper function to format bytes
1239
  const formatBytes = (bytes: number): string => {
app/components/{settings β†’ @settings/tabs}/update/UpdateTab.tsx RENAMED
@@ -35,6 +35,10 @@ interface UpdateInfo {
35
  downloadProgress?: number;
36
  installProgress?: number;
37
  estimatedTimeRemaining?: number;
 
 
 
 
38
  }
39
 
40
  interface UpdateSettings {
@@ -46,11 +50,8 @@ interface UpdateSettings {
46
  interface UpdateResponse {
47
  success: boolean;
48
  error?: string;
49
- progress?: {
50
- downloaded: number;
51
- total: number;
52
- stage: 'download' | 'install' | 'complete';
53
- };
54
  }
55
 
56
  const categorizeChangelog = (messages: string[]) => {
@@ -190,62 +191,29 @@ const UpdateTab = () => {
190
  localStorage.setItem('update_settings', JSON.stringify(updateSettings));
191
  }, [updateSettings]);
192
 
193
- const handleUpdateProgress = async (response: Response): Promise<void> => {
194
- const reader = response.body?.getReader();
195
-
196
- if (!reader) {
197
- return;
198
- }
199
-
200
- const contentLength = +(response.headers.get('Content-Length') ?? 0);
201
- let receivedLength = 0;
202
-
203
- while (true) {
204
- const { done, value } = await reader.read();
205
-
206
- if (done) {
207
- break;
208
- }
209
-
210
- receivedLength += value.length;
211
-
212
- const progress = (receivedLength / contentLength) * 100;
213
-
214
- setUpdateInfo((prev) => (prev ? { ...prev, downloadProgress: progress } : prev));
215
- }
216
- };
217
-
218
  const checkForUpdates = async () => {
219
  console.log('Starting update check...');
220
  setIsChecking(true);
221
  setError(null);
222
  setLastChecked(new Date());
223
 
224
- // Add a minimum delay of 2 seconds to show the spinning animation
225
- const startTime = Date.now();
226
-
227
  try {
228
  console.log('Fetching update info...');
229
 
230
- const githubToken = localStorage.getItem('github_connection');
231
- const headers: HeadersInit = {};
232
-
233
- if (githubToken) {
234
- const { token } = JSON.parse(githubToken);
235
- headers.Authorization = `Bearer ${token}`;
236
- }
237
-
238
  const branchToCheck = isLatestBranch ? 'main' : 'stable';
239
- const info = await GITHUB_URLS.commitJson(branchToCheck, headers);
240
 
241
- // Ensure we show the spinning animation for at least 2 seconds
242
- const elapsedTime = Date.now() - startTime;
243
 
244
- if (elapsedTime < 2000) {
245
- await new Promise((resolve) => setTimeout(resolve, 2000 - elapsedTime));
246
- }
 
 
 
247
 
248
- setUpdateInfo(info);
 
249
 
250
  if (info.hasUpdate) {
251
  const existingLogs = Object.values(logStore.logs.get());
@@ -267,18 +235,23 @@ const UpdateTab = () => {
267
  });
268
 
269
  if (updateSettings.autoUpdate && !hasUserRespondedToUpdate) {
270
- setUpdateChangelog(info.changelog || ['No changelog available']);
 
 
 
 
 
271
  setShowUpdateDialog(true);
272
  }
273
  }
274
  }
275
  } catch (err) {
276
- console.error('Detailed update check error:', err);
277
- setError('Failed to check for updates. Please try again later.');
278
  console.error('Update check failed:', err);
 
 
 
279
  setUpdateFailed(true);
280
  } finally {
281
- console.log('Update check completed');
282
  setIsChecking(false);
283
  }
284
  };
@@ -292,49 +265,45 @@ const UpdateTab = () => {
292
 
293
  const attemptUpdate = async (): Promise<void> => {
294
  try {
295
- const platform = process.platform;
296
-
297
- if (platform === 'darwin' || platform === 'linux') {
298
- const response = await fetch('/api/update', {
299
- method: 'POST',
300
- headers: {
301
- 'Content-Type': 'application/json',
302
- },
303
- body: JSON.stringify({
304
- branch: isLatestBranch ? 'main' : 'stable',
305
- settings: updateSettings,
306
- }),
307
- });
308
-
309
- if (!response.ok) {
310
- throw new Error('Failed to initiate update');
311
- }
312
 
313
- await handleUpdateProgress(response);
 
 
 
314
 
315
- const result = (await response.json()) as UpdateResponse;
316
 
317
- if (result.success) {
318
- logStore.logSuccess('Update downloaded successfully', {
319
- type: 'update',
320
- message: 'Update completed successfully.',
321
- });
322
- toast.success('Update completed successfully!');
323
- setUpdateFailed(false);
324
 
325
- return;
326
- }
 
 
 
 
 
 
 
 
 
327
 
328
- throw new Error(result.error || 'Update failed');
329
  }
330
 
331
- window.open('https://github.com/stackblitz-labs/bolt.diy/releases/latest', '_blank');
332
- logStore.logInfo('Manual update required', {
333
- type: 'update',
334
- message: 'Please download and install the latest version from the GitHub releases page.',
335
- });
336
-
337
- return;
338
  } catch (err) {
339
  currentRetry++;
340
 
@@ -349,13 +318,11 @@ const UpdateTab = () => {
349
  return;
350
  }
351
 
352
- setError('Failed to initiate update. Please try again or update manually.');
353
  console.error('Update failed:', err);
354
  logStore.logSystem('Update failed: ' + errorMessage);
355
  toast.error('Update failed: ' + errorMessage);
356
  setUpdateFailed(true);
357
-
358
- return;
359
  }
360
  };
361
 
@@ -518,7 +485,19 @@ const UpdateTab = () => {
518
  <div className="p-4 rounded-lg bg-red-500/10 border border-red-500/20 text-red-600 dark:text-red-400">
519
  <div className="flex items-center gap-2">
520
  <div className="i-ph:warning-circle" />
521
- {error}
 
 
 
 
 
 
 
 
 
 
 
 
522
  </div>
523
  </div>
524
  )}
@@ -803,7 +782,7 @@ const UpdateTab = () => {
803
  </DialogDescription>
804
 
805
  <div className="mt-3">
806
- <h3 className="text-sm font-medium text-bolt-elements-textPrimary mb-2">Changelog:</h3>
807
  <div
808
  className="bg-[#F5F5F5] dark:bg-[#1A1A1A] rounded-lg p-3 max-h-[300px] overflow-y-auto"
809
  style={{
@@ -814,7 +793,18 @@ const UpdateTab = () => {
814
  <div className="text-sm text-bolt-elements-textSecondary space-y-1.5">
815
  {updateChangelog.map((log, index) => (
816
  <div key={index} className="break-words leading-relaxed">
817
- {log}
 
 
 
 
 
 
 
 
 
 
 
818
  </div>
819
  ))}
820
  </div>
 
35
  downloadProgress?: number;
36
  installProgress?: number;
37
  estimatedTimeRemaining?: number;
38
+ error?: {
39
+ type: string;
40
+ message: string;
41
+ };
42
  }
43
 
44
  interface UpdateSettings {
 
50
  interface UpdateResponse {
51
  success: boolean;
52
  error?: string;
53
+ message?: string;
54
+ instructions?: string[];
 
 
 
55
  }
56
 
57
  const categorizeChangelog = (messages: string[]) => {
 
191
  localStorage.setItem('update_settings', JSON.stringify(updateSettings));
192
  }, [updateSettings]);
193
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
194
  const checkForUpdates = async () => {
195
  console.log('Starting update check...');
196
  setIsChecking(true);
197
  setError(null);
198
  setLastChecked(new Date());
199
 
 
 
 
200
  try {
201
  console.log('Fetching update info...');
202
 
 
 
 
 
 
 
 
 
203
  const branchToCheck = isLatestBranch ? 'main' : 'stable';
204
+ const info = await GITHUB_URLS.commitJson(branchToCheck);
205
 
206
+ setUpdateInfo(info);
 
207
 
208
+ if (info.error) {
209
+ setError(info.error.message);
210
+ logStore.logWarning('Update Check Failed', {
211
+ type: 'update',
212
+ message: info.error.message,
213
+ });
214
 
215
+ return;
216
+ }
217
 
218
  if (info.hasUpdate) {
219
  const existingLogs = Object.values(logStore.logs.get());
 
235
  });
236
 
237
  if (updateSettings.autoUpdate && !hasUserRespondedToUpdate) {
238
+ setUpdateChangelog([
239
+ 'New version available.',
240
+ `Compare changes: https://github.com/stackblitz-labs/bolt.diy/compare/${info.currentVersion}...${info.latestVersion}`,
241
+ '',
242
+ 'Click "Update Now" to start the update process.',
243
+ ]);
244
  setShowUpdateDialog(true);
245
  }
246
  }
247
  }
248
  } catch (err) {
 
 
249
  console.error('Update check failed:', err);
250
+
251
+ const errorMessage = err instanceof Error ? err.message : 'Unknown error occurred';
252
+ setError(`Failed to check for updates: ${errorMessage}`);
253
  setUpdateFailed(true);
254
  } finally {
 
255
  setIsChecking(false);
256
  }
257
  };
 
265
 
266
  const attemptUpdate = async (): Promise<void> => {
267
  try {
268
+ const response = await fetch('/api/update', {
269
+ method: 'POST',
270
+ headers: {
271
+ 'Content-Type': 'application/json',
272
+ },
273
+ body: JSON.stringify({
274
+ branch: isLatestBranch ? 'main' : 'stable',
275
+ }),
276
+ });
 
 
 
 
 
 
 
 
277
 
278
+ if (!response.ok) {
279
+ const errorData = (await response.json()) as { error: string };
280
+ throw new Error(errorData.error || 'Failed to initiate update');
281
+ }
282
 
283
+ const result = (await response.json()) as UpdateResponse;
284
 
285
+ if (result.success) {
286
+ logStore.logSuccess('Update instructions ready', {
287
+ type: 'update',
288
+ message: result.message || 'Update instructions ready',
289
+ });
 
 
290
 
291
+ // Show manual update instructions
292
+ setShowManualInstructions(true);
293
+ setUpdateChangelog(
294
+ result.instructions || [
295
+ 'Failed to get update instructions. Please update manually:',
296
+ '1. git pull origin main',
297
+ '2. pnpm install',
298
+ '3. pnpm build',
299
+ '4. Restart the application',
300
+ ],
301
+ );
302
 
303
+ return;
304
  }
305
 
306
+ throw new Error(result.error || 'Update failed');
 
 
 
 
 
 
307
  } catch (err) {
308
  currentRetry++;
309
 
 
318
  return;
319
  }
320
 
321
+ setError('Failed to get update instructions. Please update manually.');
322
  console.error('Update failed:', err);
323
  logStore.logSystem('Update failed: ' + errorMessage);
324
  toast.error('Update failed: ' + errorMessage);
325
  setUpdateFailed(true);
 
 
326
  }
327
  };
328
 
 
485
  <div className="p-4 rounded-lg bg-red-500/10 border border-red-500/20 text-red-600 dark:text-red-400">
486
  <div className="flex items-center gap-2">
487
  <div className="i-ph:warning-circle" />
488
+ <div className="flex flex-col">
489
+ <span className="font-medium">{error}</span>
490
+ {error.includes('rate limit') && (
491
+ <span className="text-sm mt-1">
492
+ Try adding a GitHub token in the connections tab to increase the rate limit.
493
+ </span>
494
+ )}
495
+ {error.includes('authentication') && (
496
+ <span className="text-sm mt-1">
497
+ Please check your GitHub token configuration in the connections tab.
498
+ </span>
499
+ )}
500
+ </div>
501
  </div>
502
  </div>
503
  )}
 
782
  </DialogDescription>
783
 
784
  <div className="mt-3">
785
+ <h3 className="text-sm font-medium text-bolt-elements-textPrimary mb-2">Update Information:</h3>
786
  <div
787
  className="bg-[#F5F5F5] dark:bg-[#1A1A1A] rounded-lg p-3 max-h-[300px] overflow-y-auto"
788
  style={{
 
793
  <div className="text-sm text-bolt-elements-textSecondary space-y-1.5">
794
  {updateChangelog.map((log, index) => (
795
  <div key={index} className="break-words leading-relaxed">
796
+ {log.startsWith('Compare changes:') ? (
797
+ <a
798
+ href={log.split(': ')[1]}
799
+ target="_blank"
800
+ rel="noopener noreferrer"
801
+ className="text-purple-500 hover:text-purple-600 dark:text-purple-400 dark:hover:text-purple-300"
802
+ >
803
+ View changes on GitHub
804
+ </a>
805
+ ) : (
806
+ log
807
+ )}
808
  </div>
809
  ))}
810
  </div>
app/components/@settings/utils/animations.ts ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { Variants } from 'framer-motion';
2
+
3
+ export const fadeIn: Variants = {
4
+ initial: { opacity: 0 },
5
+ animate: { opacity: 1 },
6
+ exit: { opacity: 0 },
7
+ };
8
+
9
+ export const slideIn: Variants = {
10
+ initial: { opacity: 0, y: 20 },
11
+ animate: { opacity: 1, y: 0 },
12
+ exit: { opacity: 0, y: -20 },
13
+ };
14
+
15
+ export const scaleIn: Variants = {
16
+ initial: { opacity: 0, scale: 0.8 },
17
+ animate: { opacity: 1, scale: 1 },
18
+ exit: { opacity: 0, scale: 0.8 },
19
+ };
20
+
21
+ export const tabAnimation: Variants = {
22
+ initial: { opacity: 0, scale: 0.8, y: 20 },
23
+ animate: { opacity: 1, scale: 1, y: 0 },
24
+ exit: { opacity: 0, scale: 0.8, y: -20 },
25
+ };
26
+
27
+ export const overlayAnimation: Variants = {
28
+ initial: { opacity: 0 },
29
+ animate: { opacity: 1 },
30
+ exit: { opacity: 0 },
31
+ };
32
+
33
+ export const modalAnimation: Variants = {
34
+ initial: { opacity: 0, scale: 0.95, y: 20 },
35
+ animate: { opacity: 1, scale: 1, y: 0 },
36
+ exit: { opacity: 0, scale: 0.95, y: 20 },
37
+ };
38
+
39
+ export const transition = {
40
+ duration: 0.2,
41
+ };
app/components/@settings/utils/tab-helpers.ts ADDED
@@ -0,0 +1,89 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { TabType, TabVisibilityConfig } from '~/components/@settings/core/types';
2
+ import { DEFAULT_TAB_CONFIG } from '~/components/@settings/core/constants';
3
+
4
+ export const getVisibleTabs = (
5
+ tabConfiguration: { userTabs: TabVisibilityConfig[]; developerTabs?: TabVisibilityConfig[] },
6
+ isDeveloperMode: boolean,
7
+ notificationsEnabled: boolean,
8
+ ): TabVisibilityConfig[] => {
9
+ if (!tabConfiguration?.userTabs || !Array.isArray(tabConfiguration.userTabs)) {
10
+ console.warn('Invalid tab configuration, using defaults');
11
+ return DEFAULT_TAB_CONFIG as TabVisibilityConfig[];
12
+ }
13
+
14
+ // In developer mode, show ALL tabs without restrictions
15
+ if (isDeveloperMode) {
16
+ // Combine all unique tabs from both user and developer configurations
17
+ const allTabs = new Set([
18
+ ...DEFAULT_TAB_CONFIG.map((tab) => tab.id),
19
+ ...tabConfiguration.userTabs.map((tab) => tab.id),
20
+ ...(tabConfiguration.developerTabs || []).map((tab) => tab.id),
21
+ 'task-manager' as TabType, // Always include task-manager in developer mode
22
+ ]);
23
+
24
+ // Create a complete tab list with all tabs visible
25
+ const devTabs = Array.from(allTabs).map((tabId) => {
26
+ // Try to find existing configuration for this tab
27
+ const existingTab =
28
+ tabConfiguration.developerTabs?.find((t) => t.id === tabId) ||
29
+ tabConfiguration.userTabs?.find((t) => t.id === tabId) ||
30
+ DEFAULT_TAB_CONFIG.find((t) => t.id === tabId);
31
+
32
+ return {
33
+ id: tabId as TabType,
34
+ visible: true,
35
+ window: 'developer' as const,
36
+ order: existingTab?.order || DEFAULT_TAB_CONFIG.findIndex((t) => t.id === tabId),
37
+ } as TabVisibilityConfig;
38
+ });
39
+
40
+ return devTabs.sort((a, b) => a.order - b.order);
41
+ }
42
+
43
+ // In user mode, only show visible user tabs
44
+ return tabConfiguration.userTabs
45
+ .filter((tab) => {
46
+ if (!tab || typeof tab.id !== 'string') {
47
+ console.warn('Invalid tab entry:', tab);
48
+ return false;
49
+ }
50
+
51
+ // Hide notifications tab if notifications are disabled
52
+ if (tab.id === 'notifications' && !notificationsEnabled) {
53
+ return false;
54
+ }
55
+
56
+ // Always show task-manager in user mode if it's configured as visible
57
+ if (tab.id === 'task-manager') {
58
+ return tab.visible;
59
+ }
60
+
61
+ // Only show tabs that are explicitly visible and assigned to the user window
62
+ return tab.visible && tab.window === 'user';
63
+ })
64
+ .sort((a, b) => a.order - b.order);
65
+ };
66
+
67
+ export const reorderTabs = (
68
+ tabs: TabVisibilityConfig[],
69
+ startIndex: number,
70
+ endIndex: number,
71
+ ): TabVisibilityConfig[] => {
72
+ const result = Array.from(tabs);
73
+ const [removed] = result.splice(startIndex, 1);
74
+ result.splice(endIndex, 0, removed);
75
+
76
+ // Update order property
77
+ return result.map((tab, index) => ({
78
+ ...tab,
79
+ order: index,
80
+ }));
81
+ };
82
+
83
+ export const resetToDefaultConfig = (isDeveloperMode: boolean): TabVisibilityConfig[] => {
84
+ return DEFAULT_TAB_CONFIG.map((tab) => ({
85
+ ...tab,
86
+ visible: isDeveloperMode ? true : tab.window === 'user',
87
+ window: isDeveloperMode ? 'developer' : tab.window,
88
+ })) as TabVisibilityConfig[];
89
+ };
app/components/chat/Chat.client.tsx CHANGED
@@ -23,6 +23,7 @@ import type { ProviderInfo } from '~/types/model';
23
  import { useSearchParams } from '@remix-run/react';
24
  import { createSampler } from '~/utils/sampler';
25
  import { getTemplates, selectStarterTemplate } from '~/utils/selectStarterTemplate';
 
26
 
27
  const toastAnimation = cssTransition({
28
  enter: 'animated fadeInRight',
@@ -114,8 +115,8 @@ export const ChatImpl = memo(
114
 
115
  const textareaRef = useRef<HTMLTextAreaElement>(null);
116
  const [chatStarted, setChatStarted] = useState(initialMessages.length > 0);
117
- const [uploadedFiles, setUploadedFiles] = useState<File[]>([]); // Move here
118
- const [imageDataList, setImageDataList] = useState<string[]>([]); // Move here
119
  const [searchParams, setSearchParams] = useSearchParams();
120
  const [fakeLoading, setFakeLoading] = useState(false);
121
  const files = useStore(workbenchStore.files);
@@ -161,6 +162,11 @@ export const ChatImpl = memo(
161
  sendExtraMessageFields: true,
162
  onError: (e) => {
163
  logger.error('Request failed\n\n', e, error);
 
 
 
 
 
164
  toast.error(
165
  'There was an error processing your request: ' + (e.message ? e.message : 'No details were returned'),
166
  );
@@ -171,8 +177,14 @@ export const ChatImpl = memo(
171
 
172
  if (usage) {
173
  console.log('Token usage:', usage);
174
-
175
- // You can now use the usage data as needed
 
 
 
 
 
 
176
  }
177
 
178
  logger.debug('Finished streaming');
@@ -231,6 +243,13 @@ export const ChatImpl = memo(
231
  stop();
232
  chatStore.setKey('aborted', true);
233
  workbenchStore.abortAllActions();
 
 
 
 
 
 
 
234
  };
235
 
236
  useEffect(() => {
@@ -262,9 +281,9 @@ export const ChatImpl = memo(
262
  };
263
 
264
  const sendMessage = async (_event: React.UIEvent, messageInput?: string) => {
265
- const _input = messageInput || input;
266
 
267
- if (!_input) {
268
  return;
269
  }
270
 
@@ -280,7 +299,7 @@ export const ChatImpl = memo(
280
 
281
  if (autoSelectTemplate) {
282
  const { template, title } = await selectStarterTemplate({
283
- message: _input,
284
  model,
285
  provider,
286
  });
@@ -302,7 +321,7 @@ export const ChatImpl = memo(
302
  {
303
  id: `${new Date().getTime()}`,
304
  role: 'user',
305
- content: _input,
306
  },
307
  {
308
  id: `${new Date().getTime()}`,
@@ -332,7 +351,7 @@ export const ChatImpl = memo(
332
  content: [
333
  {
334
  type: 'text',
335
- text: `[Model: ${model}]\n\n[Provider: ${provider.name}]\n\n${_input}`,
336
  },
337
  ...imageDataList.map((imageData) => ({
338
  type: 'image',
@@ -356,31 +375,20 @@ export const ChatImpl = memo(
356
  chatStore.setKey('aborted', false);
357
 
358
  if (fileModifications !== undefined) {
359
- /**
360
- * If we have file modifications we append a new user message manually since we have to prefix
361
- * the user input with the file modifications and we don't want the new user input to appear
362
- * in the prompt. Using `append` is almost the same as `handleSubmit` except that we have to
363
- * manually reset the input and we'd have to manually pass in file attachments. However, those
364
- * aren't relevant here.
365
- */
366
  append({
367
  role: 'user',
368
  content: [
369
  {
370
  type: 'text',
371
- text: `[Model: ${model}]\n\n[Provider: ${provider.name}]\n\n${_input}`,
372
  },
373
  ...imageDataList.map((imageData) => ({
374
  type: 'image',
375
  image: imageData,
376
  })),
377
- ] as any, // Type assertion to bypass compiler check
378
  });
379
 
380
- /**
381
- * After sending a new message we reset all modifications since the model
382
- * should now be aware of all the changes.
383
- */
384
  workbenchStore.resetAllFileModifications();
385
  } else {
386
  append({
@@ -388,20 +396,19 @@ export const ChatImpl = memo(
388
  content: [
389
  {
390
  type: 'text',
391
- text: `[Model: ${model}]\n\n[Provider: ${provider.name}]\n\n${_input}`,
392
  },
393
  ...imageDataList.map((imageData) => ({
394
  type: 'image',
395
  image: imageData,
396
  })),
397
- ] as any, // Type assertion to bypass compiler check
398
  });
399
  }
400
 
401
  setInput('');
402
  Cookies.remove(PROMPT_COOKIE_KEY);
403
 
404
- // Add file cleanup here
405
  setUploadedFiles([]);
406
  setImageDataList([]);
407
 
 
23
  import { useSearchParams } from '@remix-run/react';
24
  import { createSampler } from '~/utils/sampler';
25
  import { getTemplates, selectStarterTemplate } from '~/utils/selectStarterTemplate';
26
+ import { logStore } from '~/lib/stores/logs';
27
 
28
  const toastAnimation = cssTransition({
29
  enter: 'animated fadeInRight',
 
115
 
116
  const textareaRef = useRef<HTMLTextAreaElement>(null);
117
  const [chatStarted, setChatStarted] = useState(initialMessages.length > 0);
118
+ const [uploadedFiles, setUploadedFiles] = useState<File[]>([]);
119
+ const [imageDataList, setImageDataList] = useState<string[]>([]);
120
  const [searchParams, setSearchParams] = useSearchParams();
121
  const [fakeLoading, setFakeLoading] = useState(false);
122
  const files = useStore(workbenchStore.files);
 
162
  sendExtraMessageFields: true,
163
  onError: (e) => {
164
  logger.error('Request failed\n\n', e, error);
165
+ logStore.logError('Chat request failed', e, {
166
+ component: 'Chat',
167
+ action: 'request',
168
+ error: e.message,
169
+ });
170
  toast.error(
171
  'There was an error processing your request: ' + (e.message ? e.message : 'No details were returned'),
172
  );
 
177
 
178
  if (usage) {
179
  console.log('Token usage:', usage);
180
+ logStore.logProvider('Chat response completed', {
181
+ component: 'Chat',
182
+ action: 'response',
183
+ model,
184
+ provider: provider.name,
185
+ usage,
186
+ messageLength: message.content.length,
187
+ });
188
  }
189
 
190
  logger.debug('Finished streaming');
 
243
  stop();
244
  chatStore.setKey('aborted', true);
245
  workbenchStore.abortAllActions();
246
+
247
+ logStore.logProvider('Chat response aborted', {
248
+ component: 'Chat',
249
+ action: 'abort',
250
+ model,
251
+ provider: provider.name,
252
+ });
253
  };
254
 
255
  useEffect(() => {
 
281
  };
282
 
283
  const sendMessage = async (_event: React.UIEvent, messageInput?: string) => {
284
+ const messageContent = messageInput || input;
285
 
286
+ if (!messageContent?.trim()) {
287
  return;
288
  }
289
 
 
299
 
300
  if (autoSelectTemplate) {
301
  const { template, title } = await selectStarterTemplate({
302
+ message: messageContent,
303
  model,
304
  provider,
305
  });
 
321
  {
322
  id: `${new Date().getTime()}`,
323
  role: 'user',
324
+ content: messageContent,
325
  },
326
  {
327
  id: `${new Date().getTime()}`,
 
351
  content: [
352
  {
353
  type: 'text',
354
+ text: `[Model: ${model}]\n\n[Provider: ${provider.name}]\n\n${messageContent}`,
355
  },
356
  ...imageDataList.map((imageData) => ({
357
  type: 'image',
 
375
  chatStore.setKey('aborted', false);
376
 
377
  if (fileModifications !== undefined) {
 
 
 
 
 
 
 
378
  append({
379
  role: 'user',
380
  content: [
381
  {
382
  type: 'text',
383
+ text: `[Model: ${model}]\n\n[Provider: ${provider.name}]\n\n${messageContent}`,
384
  },
385
  ...imageDataList.map((imageData) => ({
386
  type: 'image',
387
  image: imageData,
388
  })),
389
+ ] as any,
390
  });
391
 
 
 
 
 
392
  workbenchStore.resetAllFileModifications();
393
  } else {
394
  append({
 
396
  content: [
397
  {
398
  type: 'text',
399
+ text: `[Model: ${model}]\n\n[Provider: ${provider.name}]\n\n${messageContent}`,
400
  },
401
  ...imageDataList.map((imageData) => ({
402
  type: 'image',
403
  image: imageData,
404
  })),
405
+ ] as any,
406
  });
407
  }
408
 
409
  setInput('');
410
  Cookies.remove(PROMPT_COOKIE_KEY);
411
 
 
412
  setUploadedFiles([]);
413
  setImageDataList([]);
414
 
app/components/chat/GitCloneButton.tsx CHANGED
@@ -6,7 +6,7 @@ import { generateId } from '~/utils/fileUtils';
6
  import { useState } from 'react';
7
  import { toast } from 'react-toastify';
8
  import { LoadingOverlay } from '~/components/ui/LoadingOverlay';
9
- import { RepositorySelectionDialog } from '~/components/settings/connections/components/RepositorySelectionDialog';
10
  import { classNames } from '~/utils/classNames';
11
  import { Button } from '~/components/ui/Button';
12
  import type { IChatMetadata } from '~/lib/persistence/db';
 
6
  import { useState } from 'react';
7
  import { toast } from 'react-toastify';
8
  import { LoadingOverlay } from '~/components/ui/LoadingOverlay';
9
+ import { RepositorySelectionDialog } from '~/components/@settings/tabs/connections/components/RepositorySelectionDialog';
10
  import { classNames } from '~/utils/classNames';
11
  import { Button } from '~/components/ui/Button';
12
  import type { IChatMetadata } from '~/lib/persistence/db';