Stijnus commited on
Commit
436a8e5
·
1 Parent(s): 9230ef3

ui refactor

Browse files
Files changed (40) hide show
  1. app/components/settings/SettingsWindow.tsx +0 -300
  2. app/components/settings/data/DataTab.tsx +13 -3
  3. app/components/settings/debug/DebugTab.tsx +775 -633
  4. app/components/settings/developer/DeveloperWindow.tsx +378 -0
  5. app/components/settings/developer/TabManagement.tsx +315 -0
  6. app/components/settings/event-logs/EventLogsTab.tsx +425 -307
  7. app/components/settings/features/FeaturesTab.tsx +27 -56
  8. app/components/settings/notifications/NotificationsTab.tsx +143 -0
  9. app/components/settings/profile/ProfileTab.tsx +130 -261
  10. app/components/settings/providers/ProvidersTab.tsx +26 -20
  11. app/components/settings/settings.styles.ts +11 -5
  12. app/components/settings/settings.types.ts +62 -3
  13. app/components/settings/settings/SettingsTab.tsx +215 -0
  14. app/components/settings/shared/DraggableTabList.tsx +158 -0
  15. app/components/settings/shared/TabTile.tsx +230 -0
  16. app/components/settings/update/UpdateTab.tsx +217 -0
  17. app/components/settings/user/ProfileHeader.tsx +25 -0
  18. app/components/settings/user/UsersWindow.tsx +385 -0
  19. app/components/sidebar/Menu.client.tsx +2 -2
  20. app/components/ui/Dropdown.tsx +63 -0
  21. app/components/workbench/Preview.tsx +89 -71
  22. app/lib/api/connection.ts +18 -0
  23. app/lib/api/debug.ts +65 -0
  24. app/lib/api/features.ts +35 -0
  25. app/lib/api/notifications.ts +40 -0
  26. app/lib/api/updates.ts +28 -0
  27. app/lib/hooks/index.ts +5 -0
  28. app/lib/hooks/useConnectionStatus.ts +61 -0
  29. app/lib/hooks/useDebugStatus.ts +89 -0
  30. app/lib/hooks/useFeatures.ts +72 -0
  31. app/lib/hooks/useNotifications.ts +77 -0
  32. app/lib/hooks/{useSettings.tsx → useSettings.ts} +117 -124
  33. app/lib/hooks/useUpdateCheck.ts +58 -0
  34. app/lib/modules/llm/providers/github.ts +2 -1
  35. app/lib/stores/logs.ts +72 -11
  36. app/lib/stores/settings.ts +51 -1
  37. app/root.tsx +4 -2
  38. app/utils/localStorage.ts +17 -0
  39. package.json +2 -0
  40. pnpm-lock.yaml +83 -0
app/components/settings/SettingsWindow.tsx DELETED
@@ -1,300 +0,0 @@
1
- import * as RadixDialog from '@radix-ui/react-dialog';
2
- import { motion, AnimatePresence } from 'framer-motion';
3
- import { useState } from 'react';
4
- import { classNames } from '~/utils/classNames';
5
- import { DialogTitle } from '~/components/ui/Dialog';
6
- import type { SettingCategory, TabType } from './settings.types';
7
- import { categoryLabels, categoryIcons } from './settings.types';
8
- import ProfileTab from './profile/ProfileTab';
9
- import ProvidersTab from './providers/ProvidersTab';
10
- import { useSettings } from '~/lib/hooks/useSettings';
11
- import FeaturesTab from './features/FeaturesTab';
12
- import DebugTab from './debug/DebugTab';
13
- import EventLogsTab from './event-logs/EventLogsTab';
14
- import ConnectionsTab from './connections/ConnectionsTab';
15
- import DataTab from './data/DataTab';
16
-
17
- interface SettingsProps {
18
- open: boolean;
19
- onClose: () => void;
20
- }
21
-
22
- export const SettingsWindow = ({ open, onClose }: SettingsProps) => {
23
- const { debug, eventLogs } = useSettings();
24
- const [searchQuery, setSearchQuery] = useState('');
25
- const [activeTab, setActiveTab] = useState<TabType | null>(null);
26
-
27
- const settingItems = [
28
- {
29
- id: 'profile' as const,
30
- label: 'Profile Settings',
31
- icon: 'i-ph:user-circle',
32
- category: 'profile' as const,
33
- description: 'Manage your personal information and preferences',
34
- component: () => <ProfileTab />,
35
- keywords: ['profile', 'account', 'avatar', 'email', 'name', 'theme', 'notifications'],
36
- },
37
-
38
- {
39
- id: 'data' as const,
40
- label: 'Data Management',
41
- icon: 'i-ph:database',
42
- category: 'file_sharing' as const,
43
- description: 'Manage your chat history and application data',
44
- component: () => <DataTab />,
45
- keywords: ['data', 'export', 'import', 'backup', 'delete'],
46
- },
47
-
48
- {
49
- id: 'providers' as const,
50
- label: 'Providers',
51
- icon: 'i-ph:key',
52
- category: 'file_sharing' as const,
53
- description: 'Configure AI providers and API keys',
54
- component: () => <ProvidersTab />,
55
- keywords: ['api', 'keys', 'providers', 'configuration'],
56
- },
57
-
58
- {
59
- id: 'connection' as const,
60
- label: 'Connection',
61
- icon: 'i-ph:link',
62
- category: 'connectivity' as const,
63
- description: 'Manage network and connection settings',
64
- component: () => <ConnectionsTab />,
65
- keywords: ['network', 'connection', 'proxy', 'ssl'],
66
- },
67
-
68
- {
69
- id: 'features' as const,
70
- label: 'Features',
71
- icon: 'i-ph:star',
72
- category: 'system' as const,
73
- description: 'Configure application features and preferences',
74
- component: () => <FeaturesTab />,
75
- keywords: ['features', 'settings', 'options'],
76
- },
77
- ] as const;
78
-
79
- const debugItems = debug
80
- ? [
81
- {
82
- id: 'debug' as const,
83
- label: 'Debug',
84
- icon: 'i-ph:bug',
85
- category: 'system' as const,
86
- description: 'Advanced debugging tools and options',
87
- component: () => <DebugTab />,
88
- keywords: ['debug', 'logs', 'developer'],
89
- },
90
- ]
91
- : [];
92
-
93
- const eventLogItems = eventLogs
94
- ? [
95
- {
96
- id: 'event-logs' as const,
97
- label: 'Event Logs',
98
- icon: 'i-ph:list-bullets',
99
- category: 'system' as const,
100
- description: 'View system events and application logs',
101
- component: () => <EventLogsTab />,
102
- keywords: ['logs', 'events', 'history'],
103
- },
104
- ]
105
- : [];
106
-
107
- const allSettingItems = [...settingItems, ...debugItems, ...eventLogItems];
108
-
109
- const filteredItems = allSettingItems.filter(
110
- (item) =>
111
- item.label.toLowerCase().includes(searchQuery.toLowerCase()) ||
112
- item.description?.toLowerCase().includes(searchQuery.toLowerCase()) ||
113
- item.keywords?.some((keyword) => keyword.toLowerCase().includes(searchQuery.toLowerCase())),
114
- );
115
-
116
- const groupedItems = filteredItems.reduce(
117
- (acc, item) => {
118
- if (!acc[item.category]) {
119
- acc[item.category] = allSettingItems.filter((i) => i.category === item.category);
120
- }
121
-
122
- return acc;
123
- },
124
- {} as Record<SettingCategory, typeof allSettingItems>,
125
- );
126
-
127
- const handleBackToDashboard = () => {
128
- setActiveTab(null);
129
- onClose();
130
- };
131
-
132
- const activeTabItem = allSettingItems.find((item) => item.id === activeTab);
133
-
134
- return (
135
- <RadixDialog.Root open={open}>
136
- <RadixDialog.Portal>
137
- <div className="fixed inset-0 flex items-center justify-center z-[9999]">
138
- <RadixDialog.Overlay asChild>
139
- <motion.div
140
- className="absolute inset-0 bg-black/50 backdrop-blur-sm"
141
- initial={{ opacity: 0 }}
142
- animate={{ opacity: 1 }}
143
- exit={{ opacity: 0 }}
144
- transition={{ duration: 0.2 }}
145
- />
146
- </RadixDialog.Overlay>
147
- <RadixDialog.Content aria-describedby={undefined} asChild>
148
- <motion.div
149
- className={classNames(
150
- 'relative',
151
- 'w-[1000px] max-h-[90vh] min-h-[700px]',
152
- 'bg-[#FAFAFA] dark:bg-[#0A0A0A]',
153
- 'rounded-2xl overflow-hidden shadow-2xl',
154
- 'border border-[#E5E5E5] dark:border-[#1A1A1A]',
155
- 'overflow-y-auto scrollbar-thin scrollbar-thumb-gray-300 dark:scrollbar-thumb-gray-700 scrollbar-track-transparent',
156
- )}
157
- initial={{ opacity: 0, scale: 0.95, y: 20 }}
158
- animate={{ opacity: 1, scale: 1, y: 0 }}
159
- exit={{ opacity: 0, scale: 0.95, y: 20 }}
160
- transition={{ duration: 0.2 }}
161
- >
162
- <AnimatePresence mode="wait">
163
- {activeTab ? (
164
- <motion.div
165
- className="flex flex-col h-full"
166
- initial={{ opacity: 0, y: 20 }}
167
- animate={{ opacity: 1, y: 0 }}
168
- exit={{ opacity: 0, y: -20 }}
169
- transition={{ duration: 0.2 }}
170
- >
171
- <div className="flex items-center justify-between p-6 border-b border-[#E5E5E5] dark:border-[#1A1A1A] sticky top-0 bg-[#FAFAFA] dark:bg-[#0A0A0A] z-10">
172
- <div className="flex items-center">
173
- <button
174
- onClick={() => setActiveTab(null)}
175
- className="flex items-center gap-2 px-4 py-2 rounded-lg text-sm bg-[#F5F5F5] dark:bg-[#1A1A1A] text-[#666666] dark:text-[#999999] hover:text-[#333333] dark:hover:text-white"
176
- >
177
- <div className="i-ph:arrow-left w-4 h-4" />
178
- Back to Settings
179
- </button>
180
-
181
- <div className="text-bolt-elements-textTertiary mx-6 select-none">|</div>
182
-
183
- {activeTabItem && (
184
- <div className="flex items-center gap-4">
185
- <div className={classNames(activeTabItem.icon, 'w-6 h-6 text-purple-500')} />
186
- <div>
187
- <h2 className="text-lg font-medium text-bolt-elements-textPrimary">
188
- {activeTabItem.label}
189
- </h2>
190
- <p className="text-sm text-bolt-elements-textSecondary">{activeTabItem.description}</p>
191
- </div>
192
- </div>
193
- )}
194
- </div>
195
-
196
- <button
197
- onClick={handleBackToDashboard}
198
- className="flex items-center gap-2 px-4 py-2 rounded-lg text-sm bg-[#F5F5F5] dark:bg-[#1A1A1A] text-[#666666] dark:text-[#999999] hover:text-[#333333] dark:hover:text-white"
199
- >
200
- <div className="i-ph:house w-4 h-4" />
201
- Back to Bolt DIY
202
- </button>
203
- </div>
204
- <div className="flex-1 p-6 overflow-y-auto scrollbar-thin scrollbar-thumb-gray-300 dark:scrollbar-thumb-gray-700 scrollbar-track-transparent">
205
- {allSettingItems.find((item) => item.id === activeTab)?.component()}
206
- </div>
207
- </motion.div>
208
- ) : (
209
- <motion.div
210
- className="flex flex-col h-full"
211
- initial={{ opacity: 0, y: 20 }}
212
- animate={{ opacity: 1, y: 0 }}
213
- exit={{ opacity: 0, y: -20 }}
214
- transition={{ duration: 0.2 }}
215
- >
216
- <div className="flex items-center justify-between p-6 border-b border-[#E5E5E5] dark:border-[#1A1A1A] sticky top-0 bg-[#FAFAFA] dark:bg-[#0A0A0A] z-10">
217
- <div className="flex items-center gap-3">
218
- <div className="i-ph:lightning-fill w-5 h-5 text-purple-500" />
219
- <DialogTitle className="text-lg font-medium text-bolt-elements-textPrimary">
220
- Bolt Control Panel
221
- </DialogTitle>
222
- </div>
223
- <div className="flex items-center gap-4">
224
- <div className="relative w-[320px]">
225
- <input
226
- type="text"
227
- placeholder="Search settings..."
228
- value={searchQuery}
229
- onChange={(e) => setSearchQuery(e.target.value)}
230
- className={classNames(
231
- 'w-full h-10 pl-10 pr-4 rounded-lg text-sm',
232
- 'bg-[#F8F8F8] dark:bg-[#1A1A1A]',
233
- 'border border-[#E5E5E5] dark:border-[#333333]',
234
- 'text-bolt-elements-textPrimary placeholder-bolt-elements-textTertiary',
235
- 'focus:outline-none focus:ring-1 focus:ring-purple-500 transition-all',
236
- )}
237
- />
238
- <div className="absolute left-3.5 top-1/2 -translate-y-1/2">
239
- <div className="i-ph:magnifying-glass w-4 h-4 text-bolt-elements-textTertiary" />
240
- </div>
241
- </div>
242
- <button
243
- onClick={handleBackToDashboard}
244
- className="flex items-center gap-2 px-4 py-2 rounded-lg text-sm bg-[#F5F5F5] dark:bg-[#1A1A1A] text-[#666666] dark:text-[#999999] hover:text-[#333333] dark:hover:text-white"
245
- >
246
- <div className="i-ph:house w-4 h-4" />
247
- Back to Bolt DIY
248
- </button>
249
- </div>
250
- </div>
251
-
252
- <div className="flex-1 p-6 overflow-y-auto scrollbar-thin scrollbar-thumb-gray-300 dark:scrollbar-thumb-gray-700 scrollbar-track-transparent">
253
- <div className="space-y-8">
254
- {(Object.keys(groupedItems) as SettingCategory[]).map((category) => (
255
- <div key={category} className="space-y-4">
256
- <div className="flex items-center gap-3">
257
- <div className={classNames(categoryIcons[category], 'w-5 h-5 text-purple-500')} />
258
- <h2 className="text-base font-medium text-bolt-elements-textPrimary">
259
- {categoryLabels[category]}
260
- </h2>
261
- </div>
262
- <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
263
- {groupedItems[category].map((item) => (
264
- <button
265
- key={item.id}
266
- onClick={() => setActiveTab(item.id)}
267
- className={classNames(
268
- 'flex flex-col gap-2 p-4 rounded-lg text-left',
269
- 'bg-white dark:bg-[#0A0A0A]',
270
- 'border border-[#E5E5E5] dark:border-[#1A1A1A]',
271
- 'hover:bg-[#F8F8F8] dark:hover:bg-[#1A1A1A]',
272
- 'transition-all duration-200',
273
- )}
274
- >
275
- <div className="flex items-center gap-3">
276
- <div className={classNames(item.icon, 'w-5 h-5 text-purple-500')} />
277
- <span className="text-sm font-medium text-bolt-elements-textPrimary">
278
- {item.label}
279
- </span>
280
- </div>
281
- {item.description && (
282
- <p className="text-sm text-bolt-elements-textSecondary">{item.description}</p>
283
- )}
284
- </button>
285
- ))}
286
- </div>
287
- </div>
288
- ))}
289
- </div>
290
- </div>
291
- </motion.div>
292
- )}
293
- </AnimatePresence>
294
- </motion.div>
295
- </RadixDialog.Content>
296
- </div>
297
- </RadixDialog.Portal>
298
- </RadixDialog.Root>
299
- );
300
- };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
app/components/settings/data/DataTab.tsx CHANGED
@@ -185,11 +185,15 @@ export default function DataTab() {
185
  localStorage.removeItem('bolt_settings');
186
  localStorage.removeItem('bolt_chat_history');
187
 
188
- // Reload the page to apply reset
 
 
 
189
  window.location.reload();
190
  toast.success('Settings reset successfully');
191
  } catch (error) {
192
  console.error('Reset error:', error);
 
193
  toast.error('Failed to reset settings');
194
  } finally {
195
  setIsResetting(false);
@@ -202,9 +206,15 @@ export default function DataTab() {
202
  try {
203
  // Clear chat history
204
  localStorage.removeItem('bolt_chat_history');
 
 
 
 
 
205
  toast.success('Chat history deleted successfully');
206
  } catch (error) {
207
  console.error('Delete error:', error);
 
208
  toast.error('Failed to delete chat history');
209
  } finally {
210
  setIsDeleting(false);
@@ -216,7 +226,7 @@ export default function DataTab() {
216
  <input ref={fileInputRef} type="file" accept=".json" onChange={handleImportSettings} className="hidden" />
217
  {/* Reset Settings Dialog */}
218
  <DialogRoot open={showResetInlineConfirm} onOpenChange={setShowResetInlineConfirm}>
219
- <Dialog showCloseButton={false}>
220
  <div className="p-6">
221
  <div className="flex items-center gap-3">
222
  <div className="i-ph:warning-circle-fill w-5 h-5 text-yellow-500" />
@@ -252,7 +262,7 @@ export default function DataTab() {
252
 
253
  {/* Delete Confirmation Dialog */}
254
  <DialogRoot open={showDeleteInlineConfirm} onOpenChange={setShowDeleteInlineConfirm}>
255
- <Dialog showCloseButton={false}>
256
  <div className="p-6">
257
  <div className="flex items-center gap-3">
258
  <div className="i-ph:warning-circle-fill w-5 h-5 text-red-500" />
 
185
  localStorage.removeItem('bolt_settings');
186
  localStorage.removeItem('bolt_chat_history');
187
 
188
+ // Close the dialog first
189
+ setShowResetInlineConfirm(false);
190
+
191
+ // Then reload and show success message
192
  window.location.reload();
193
  toast.success('Settings reset successfully');
194
  } catch (error) {
195
  console.error('Reset error:', error);
196
+ setShowResetInlineConfirm(false);
197
  toast.error('Failed to reset settings');
198
  } finally {
199
  setIsResetting(false);
 
206
  try {
207
  // Clear chat history
208
  localStorage.removeItem('bolt_chat_history');
209
+
210
+ // Close the dialog first
211
+ setShowDeleteInlineConfirm(false);
212
+
213
+ // Then show the success message
214
  toast.success('Chat history deleted successfully');
215
  } catch (error) {
216
  console.error('Delete error:', error);
217
+ setShowDeleteInlineConfirm(false);
218
  toast.error('Failed to delete chat history');
219
  } finally {
220
  setIsDeleting(false);
 
226
  <input ref={fileInputRef} type="file" accept=".json" onChange={handleImportSettings} className="hidden" />
227
  {/* Reset Settings Dialog */}
228
  <DialogRoot open={showResetInlineConfirm} onOpenChange={setShowResetInlineConfirm}>
229
+ <Dialog showCloseButton={false} className="z-[1000]">
230
  <div className="p-6">
231
  <div className="flex items-center gap-3">
232
  <div className="i-ph:warning-circle-fill w-5 h-5 text-yellow-500" />
 
262
 
263
  {/* Delete Confirmation Dialog */}
264
  <DialogRoot open={showDeleteInlineConfirm} onOpenChange={setShowDeleteInlineConfirm}>
265
+ <Dialog showCloseButton={false} className="z-[1000]">
266
  <div className="p-6">
267
  <div className="flex items-center gap-3">
268
  <div className="i-ph:warning-circle-fill w-5 h-5 text-red-500" />
app/components/settings/debug/DebugTab.tsx CHANGED
@@ -1,727 +1,869 @@
1
- import React, { useCallback, useEffect, useState } from 'react';
2
- import { useSettings } from '~/lib/hooks/useSettings';
3
  import { toast } from 'react-toastify';
4
- import { providerBaseUrlEnvKeys } from '~/utils/constants';
5
- import { motion } from 'framer-motion';
6
  import { classNames } from '~/utils/classNames';
7
- import { settingsStyles } from '~/components/settings/settings.styles';
8
 
9
  interface ProviderStatus {
 
10
  name: string;
11
- enabled: boolean;
12
- isLocal: boolean;
13
- isRunning: boolean | null;
14
  error?: string;
15
- lastChecked: Date;
16
- responseTime?: number;
17
- url: string | null;
18
  }
19
 
20
  interface SystemInfo {
21
  os: string;
22
- browser: string;
23
- screen: string;
24
- language: string;
25
- timezone: string;
26
- memory: string;
27
- cores: number;
28
- deviceType: string;
29
- colorDepth: string;
30
- pixelRatio: number;
31
- online: boolean;
32
- cookiesEnabled: boolean;
33
- doNotTrack: boolean;
34
- }
35
-
36
- interface IProviderConfig {
37
- name: string;
38
- settings: {
39
- enabled: boolean;
40
- baseUrl?: string;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
41
  };
42
  }
43
 
44
- interface CommitData {
45
- commit: string;
46
- version?: string;
47
- }
48
-
49
- const connitJson: CommitData = {
50
- commit: __COMMIT_HASH,
51
- version: __APP_VERSION,
52
- };
53
-
54
- const LOCAL_PROVIDERS = ['Ollama', 'LMStudio', 'OpenAILike'];
 
 
 
 
 
 
 
 
 
 
 
55
 
56
- const versionHash = connitJson.commit;
57
- const versionTag = connitJson.version;
 
 
 
 
 
 
 
 
 
 
 
 
 
58
 
59
- const GITHUB_URLS = {
60
- original: 'https://api.github.com/repos/stackblitz-labs/bolt.diy/commits/main',
61
- fork: 'https://api.github.com/repos/Stijnus/bolt.new-any-llm/commits/main',
62
- commitJson: async (branch: string) => {
63
- try {
64
- const response = await fetch(`https://api.github.com/repos/stackblitz-labs/bolt.diy/commits/${branch}`);
65
- const data: { sha: string } = await response.json();
66
 
67
- const packageJsonResp = await fetch(
68
- `https://raw.githubusercontent.com/stackblitz-labs/bolt.diy/${branch}/package.json`,
69
- );
70
- const packageJson: { version: string } = await packageJsonResp.json();
71
 
72
- return {
73
- commit: data.sha.slice(0, 7),
74
- version: packageJson.version,
75
- };
76
- } catch (error) {
77
- console.log('Failed to fetch local commit info:', error);
78
- throw new Error('Failed to fetch local commit info');
79
- }
80
- },
81
- };
82
 
83
- function getSystemInfo(): SystemInfo {
84
- const formatBytes = (bytes: number): string => {
85
- if (bytes === 0) {
86
- return '0 Bytes';
87
- }
88
 
89
- const k = 1024;
90
- const sizes = ['Bytes', 'KB', 'MB', 'GB'];
91
- const i = Math.floor(Math.log(bytes) / Math.log(k));
92
 
93
- return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
94
- };
 
 
 
 
 
 
 
 
 
 
95
 
96
- const getBrowserInfo = (): string => {
97
- const ua = navigator.userAgent;
98
- let browser = 'Unknown';
99
-
100
- if (ua.includes('Firefox/')) {
101
- browser = 'Firefox';
102
- } else if (ua.includes('Chrome/')) {
103
- if (ua.includes('Edg/')) {
104
- browser = 'Edge';
105
- } else if (ua.includes('OPR/')) {
106
- browser = 'Opera';
107
- } else {
108
- browser = 'Chrome';
109
  }
110
- } else if (ua.includes('Safari/')) {
111
- if (!ua.includes('Chrome')) {
112
- browser = 'Safari';
 
 
 
 
 
 
 
 
 
113
  }
114
- }
115
 
116
- // Extract version number
117
- const match = ua.match(new RegExp(`${browser}\\/([\\d.]+)`));
118
- const version = match ? ` ${match[1]}` : '';
 
 
 
 
 
 
 
 
 
119
 
120
- return `${browser}${version}`;
 
 
 
 
 
 
 
121
  };
122
 
123
- const getOperatingSystem = (): string => {
124
- const ua = navigator.userAgent;
125
- const platform = navigator.platform;
126
-
127
- if (ua.includes('Win')) {
128
- return 'Windows';
129
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
130
 
131
- if (ua.includes('Mac')) {
132
- if (ua.includes('iPhone') || ua.includes('iPad')) {
133
- return 'iOS';
 
 
 
 
 
 
 
134
  }
135
 
136
- return 'macOS';
137
- }
 
 
 
 
 
138
 
139
- if (ua.includes('Linux')) {
140
- return 'Linux';
141
- }
 
 
 
 
 
 
 
 
 
142
 
143
- if (ua.includes('Android')) {
144
- return 'Android';
145
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
146
 
147
- return platform || 'Unknown';
 
 
 
 
 
 
 
148
  };
149
 
150
- const getDeviceType = (): string => {
151
- const ua = navigator.userAgent;
 
 
 
152
 
153
- if (ua.includes('Mobile')) {
154
- return 'Mobile';
 
155
  }
156
 
157
- if (ua.includes('Tablet')) {
158
- return 'Tablet';
159
- }
160
-
161
- return 'Desktop';
162
  };
163
 
164
- // Get more detailed memory info if available
165
- const getMemoryInfo = (): string => {
166
- if ('memory' in performance) {
167
- const memory = (performance as any).memory;
168
- return `${formatBytes(memory.jsHeapSizeLimit)} (Used: ${formatBytes(memory.usedJSHeapSize)})`;
169
  }
170
 
171
- return 'Not available';
 
 
 
 
 
 
 
172
  };
173
 
174
- return {
175
- os: getOperatingSystem(),
176
- browser: getBrowserInfo(),
177
- screen: `${window.screen.width}x${window.screen.height}`,
178
- language: navigator.language,
179
- timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
180
- memory: getMemoryInfo(),
181
- cores: navigator.hardwareConcurrency || 0,
182
- deviceType: getDeviceType(),
183
-
184
- // Add new fields
185
- colorDepth: `${window.screen.colorDepth}-bit`,
186
- pixelRatio: window.devicePixelRatio,
187
- online: navigator.onLine,
188
- cookiesEnabled: navigator.cookieEnabled,
189
- doNotTrack: navigator.doNotTrack === '1',
190
- };
191
- }
192
 
193
- const checkProviderStatus = async (url: string | null, providerName: string): Promise<ProviderStatus> => {
194
- if (!url) {
195
- console.log(`[Debug] No URL provided for ${providerName}`);
196
- return {
197
- name: providerName,
198
- enabled: false,
199
- isLocal: true,
200
- isRunning: false,
201
- error: 'No URL configured',
202
- lastChecked: new Date(),
203
- url: null,
204
- };
205
- }
206
 
207
- console.log(`[Debug] Checking status for ${providerName} at ${url}`);
 
 
 
 
 
 
 
 
 
 
208
 
209
- const startTime = performance.now();
 
 
 
 
 
 
210
 
211
- try {
212
- if (providerName.toLowerCase() === 'ollama') {
213
- // Special check for Ollama root endpoint
214
- try {
215
- console.log(`[Debug] Checking Ollama root endpoint: ${url}`);
 
 
 
 
216
 
217
- const controller = new AbortController();
218
- const timeoutId = setTimeout(() => controller.abort(), 5000); // 5 second timeout
219
 
220
- const response = await fetch(url, {
221
- signal: controller.signal,
222
- headers: {
223
- Accept: 'text/plain,application/json',
224
- },
225
- });
226
- clearTimeout(timeoutId);
227
-
228
- const text = await response.text();
229
- console.log(`[Debug] Ollama root response:`, text);
230
-
231
- if (text.includes('Ollama is running')) {
232
- console.log(`[Debug] Ollama running confirmed via root endpoint`);
233
- return {
234
- name: providerName,
235
- enabled: false,
236
- isLocal: true,
237
- isRunning: true,
238
- lastChecked: new Date(),
239
- responseTime: performance.now() - startTime,
240
- url,
241
- };
242
- }
243
- } catch (error) {
244
- console.log(`[Debug] Ollama root check failed:`, error);
245
-
246
- const errorMessage = error instanceof Error ? error.message : 'Unknown error';
247
-
248
- if (errorMessage.includes('aborted')) {
249
- return {
250
- name: providerName,
251
- enabled: false,
252
- isLocal: true,
253
- isRunning: false,
254
- error: 'Connection timeout',
255
- lastChecked: new Date(),
256
- responseTime: performance.now() - startTime,
257
- url,
258
- };
259
- }
260
- }
261
- }
262
 
263
- // Try different endpoints based on provider
264
- const checkUrls = [`${url}/api/health`, url.endsWith('v1') ? `${url}/models` : `${url}/v1/models`];
265
- console.log(`[Debug] Checking additional endpoints:`, checkUrls);
266
-
267
- const results = await Promise.all(
268
- checkUrls.map(async (checkUrl) => {
269
- try {
270
- console.log(`[Debug] Trying endpoint: ${checkUrl}`);
271
-
272
- const controller = new AbortController();
273
- const timeoutId = setTimeout(() => controller.abort(), 5000);
274
-
275
- const response = await fetch(checkUrl, {
276
- signal: controller.signal,
277
- headers: {
278
- Accept: 'application/json',
279
- },
280
- });
281
- clearTimeout(timeoutId);
282
-
283
- const ok = response.ok;
284
- console.log(`[Debug] Endpoint ${checkUrl} response:`, ok);
285
-
286
- if (ok) {
287
- try {
288
- const data = await response.json();
289
- console.log(`[Debug] Endpoint ${checkUrl} data:`, data);
290
- } catch {
291
- console.log(`[Debug] Could not parse JSON from ${checkUrl}`);
292
- }
293
  }
294
 
295
- return ok;
296
- } catch (error) {
297
- console.log(`[Debug] Endpoint ${checkUrl} failed:`, error);
298
- return false;
299
  }
300
- }),
301
- );
302
-
303
- const isRunning = results.some((result) => result);
304
- console.log(`[Debug] Final status for ${providerName}:`, isRunning);
305
-
306
- return {
307
- name: providerName,
308
- enabled: false,
309
- isLocal: true,
310
- isRunning,
311
- lastChecked: new Date(),
312
- responseTime: performance.now() - startTime,
313
- url,
314
- };
315
- } catch (error) {
316
- console.log(`[Debug] Provider check failed for ${providerName}:`, error);
317
- return {
318
- name: providerName,
319
- enabled: false,
320
- isLocal: true,
321
- isRunning: false,
322
- error: error instanceof Error ? error.message : 'Unknown error',
323
- lastChecked: new Date(),
324
- responseTime: performance.now() - startTime,
325
- url,
326
- };
327
- }
328
- };
329
 
330
- export default function DebugTab() {
331
- const { providers, isLatestBranch } = useSettings();
332
- const [activeProviders, setActiveProviders] = useState<ProviderStatus[]>([]);
333
- const [updateMessage, setUpdateMessage] = useState<string>('');
334
- const [systemInfo] = useState<SystemInfo>(getSystemInfo());
335
- const [isCheckingUpdate, setIsCheckingUpdate] = useState(false);
336
-
337
- const updateProviderStatuses = async () => {
338
- if (!providers) {
339
- return;
340
- }
341
 
342
- try {
343
- const entries = Object.entries(providers) as [string, IProviderConfig][];
344
- const statuses = await Promise.all(
345
- entries
346
- .filter(([, provider]) => LOCAL_PROVIDERS.includes(provider.name))
347
- .map(async ([, provider]) => {
348
- const envVarName =
349
- providerBaseUrlEnvKeys[provider.name].baseUrlKey || `REACT_APP_${provider.name.toUpperCase()}_URL`;
350
-
351
- // Access environment variables through import.meta.env
352
- let settingsUrl = provider.settings.baseUrl;
353
-
354
- if (settingsUrl && settingsUrl.trim().length === 0) {
355
- settingsUrl = undefined;
356
- }
357
-
358
- const url = settingsUrl || import.meta.env[envVarName] || null; // Ensure baseUrl is used
359
- console.log(`[Debug] Using URL for ${provider.name}:`, url, `(from ${envVarName})`);
360
-
361
- const status = await checkProviderStatus(url, provider.name);
362
-
363
- return {
364
- ...status,
365
- enabled: provider.settings.enabled ?? false,
366
- };
367
- }),
368
- );
369
-
370
- setActiveProviders(statuses);
371
  } catch (error) {
372
- console.error('[Debug] Failed to update provider statuses:', error);
 
 
 
373
  }
374
  };
375
 
376
- useEffect(() => {
377
- updateProviderStatuses();
378
-
379
- const interval = setInterval(updateProviderStatuses, 30000);
380
-
381
- return () => clearInterval(interval);
382
- }, [providers]);
383
-
384
- const handleCheckForUpdate = useCallback(async () => {
385
- if (isCheckingUpdate) {
386
- return;
387
- }
388
-
389
  try {
390
- setIsCheckingUpdate(true);
391
- setUpdateMessage('Checking for updates...');
392
-
393
- const branchToCheck = isLatestBranch ? 'main' : 'stable';
394
- console.log(`[Debug] Checking for updates against ${branchToCheck} branch`);
395
-
396
- const latestCommitResp = await GITHUB_URLS.commitJson(branchToCheck);
397
-
398
- const remoteCommitHash = latestCommitResp.commit;
399
- const currentCommitHash = versionHash;
400
-
401
- if (remoteCommitHash !== currentCommitHash) {
402
- setUpdateMessage(
403
- `Update available from ${branchToCheck} branch!\n` +
404
- `Current: ${currentCommitHash.slice(0, 7)}\n` +
405
- `Latest: ${remoteCommitHash.slice(0, 7)}`,
406
- );
 
 
 
 
 
 
 
407
  } else {
408
- setUpdateMessage(`You are on the latest version from the ${branchToCheck} branch`);
409
  }
 
 
 
 
 
 
410
  } catch (error) {
411
- setUpdateMessage('Failed to check for updates');
412
- console.error('[Debug] Failed to check for updates:', error);
413
  } finally {
414
- setIsCheckingUpdate(false);
415
  }
416
- }, [isCheckingUpdate, isLatestBranch]);
417
-
418
- const handleCopyToClipboard = useCallback(() => {
419
- const debugInfo = {
420
- System: systemInfo,
421
- Providers: activeProviders.map((provider) => ({
422
- name: provider.name,
423
- enabled: provider.enabled,
424
- isLocal: provider.isLocal,
425
- running: provider.isRunning,
426
- error: provider.error,
427
- lastChecked: provider.lastChecked,
428
- responseTime: provider.responseTime,
429
- url: provider.url,
430
- })),
431
- Version: {
432
- hash: versionHash.slice(0, 7),
433
- branch: isLatestBranch ? 'main' : 'stable',
434
- },
435
- Timestamp: new Date().toISOString(),
436
- };
437
-
438
- navigator.clipboard.writeText(JSON.stringify(debugInfo, null, 2)).then(() => {
439
- toast.success('Debug information copied to clipboard!');
440
- });
441
- }, [activeProviders, systemInfo, isLatestBranch]);
442
 
443
  return (
444
  <div className="space-y-6">
445
- <div className="flex items-center justify-between">
446
- <div className="flex items-center gap-2">
447
- <div className="i-ph:bug-fill text-xl text-purple-500" />
448
- <h3 className="text-lg font-medium text-bolt-elements-textPrimary">Debug Information</h3>
449
- </div>
450
- <div className="flex gap-2">
451
- <motion.button
452
- onClick={handleCopyToClipboard}
453
- className={classNames(settingsStyles.button.base, settingsStyles.button.primary)}
454
- whileHover={{ scale: 1.02 }}
455
- whileTap={{ scale: 0.98 }}
456
- >
457
- <div className="i-ph:copy" />
458
- Copy Debug Info
459
- </motion.button>
460
- <motion.button
461
- onClick={handleCheckForUpdate}
462
- disabled={isCheckingUpdate}
463
- className={classNames(settingsStyles.button.base, settingsStyles.button.primary)}
464
- whileHover={!isCheckingUpdate ? { scale: 1.02 } : undefined}
465
- whileTap={!isCheckingUpdate ? { scale: 0.98 } : undefined}
466
- >
467
- {isCheckingUpdate ? (
468
- <>
469
- <div className={settingsStyles['loading-spinner']} />
470
- Checking...
471
- </>
472
- ) : (
473
- <>
474
- <div className="i-ph:arrow-clockwise" />
475
- Check for Updates
476
- </>
477
- )}
478
- </motion.button>
479
- </div>
480
- </div>
481
-
482
- {updateMessage && (
483
- <motion.div
484
- className={classNames(
485
- settingsStyles.card,
486
- 'bg-bolt-elements-background-depth-2',
487
- updateMessage.includes('Update available') ? 'border-l-4 border-yellow-500' : '',
488
- )}
489
- initial={{ opacity: 0, y: -20 }}
490
- animate={{ opacity: 1, y: 0 }}
491
- >
492
- <div className="flex items-start gap-3">
493
- <div
494
  className={classNames(
495
- updateMessage.includes('Update available')
496
- ? 'i-ph:warning-fill text-yellow-500'
497
- : 'i-ph:info text-bolt-elements-textSecondary',
498
- 'text-xl flex-shrink-0',
499
  )}
500
- />
501
- <div className="flex-1">
502
- <p className="text-bolt-elements-textSecondary whitespace-pre-line">{updateMessage}</p>
503
- {updateMessage.includes('Update available') && (
504
- <div className="mt-3">
505
- <p className="font-medium text-bolt-elements-textPrimary">To update:</p>
506
- <ol className="list-decimal ml-4 mt-1 space-y-2">
507
- <li className="text-bolt-elements-textSecondary">
508
- <div className="flex items-center gap-2">
509
- <div className="i-ph:git-branch text-purple-500" />
510
- Pull the latest changes:{' '}
511
- <code className="px-1.5 py-0.5 rounded bg-bolt-elements-background-depth-3 text-bolt-elements-textPrimary">
512
- git pull upstream main
513
- </code>
514
- </div>
515
- </li>
516
- <li className="text-bolt-elements-textSecondary">
517
- <div className="flex items-center gap-2">
518
- <div className="i-ph:package text-purple-500" />
519
- Install any new dependencies:{' '}
520
- <code className="px-1.5 py-0.5 rounded bg-bolt-elements-background-depth-3 text-bolt-elements-textPrimary">
521
- pnpm install
522
- </code>
523
- </div>
524
- </li>
525
- <li className="text-bolt-elements-textSecondary">
526
- <div className="flex items-center gap-2">
527
- <div className="i-ph:arrows-clockwise text-purple-500" />
528
- Restart the application
529
- </div>
530
- </li>
531
- </ol>
532
- </div>
533
  )}
534
- </div>
535
- </div>
536
- </motion.div>
537
- )}
538
-
539
- <section className="space-y-4">
540
- <motion.div className="space-y-4">
541
- <div className="flex items-center gap-2 mb-4">
542
- <div className="i-ph:desktop text-xl text-purple-500" />
543
- <h4 className="text-md font-medium text-bolt-elements-textPrimary">System Information</h4>
544
  </div>
545
- <motion.div className={classNames(settingsStyles.card, 'bg-bolt-elements-background-depth-2')}>
546
- <div className="grid grid-cols-2 md:grid-cols-3 gap-4">
547
- <div>
548
- <div className="flex items-center gap-2 mb-1">
549
- <div className="i-ph:computer-tower text-bolt-elements-textSecondary" />
550
- <p className="text-xs text-bolt-elements-textSecondary">Operating System</p>
551
- </div>
552
- <p className="text-sm font-medium text-bolt-elements-textPrimary">{systemInfo.os}</p>
553
  </div>
554
- <div>
555
- <div className="flex items-center gap-2 mb-1">
556
- <div className="i-ph:device-mobile text-bolt-elements-textSecondary" />
557
- <p className="text-xs text-bolt-elements-textSecondary">Device Type</p>
558
- </div>
559
- <p className="text-sm font-medium text-bolt-elements-textPrimary">{systemInfo.deviceType}</p>
560
  </div>
561
- <div>
562
- <div className="flex items-center gap-2 mb-1">
563
- <div className="i-ph:browser text-bolt-elements-textSecondary" />
564
- <p className="text-xs text-bolt-elements-textSecondary">Browser</p>
565
- </div>
566
- <p className="text-sm font-medium text-bolt-elements-textPrimary">{systemInfo.browser}</p>
567
  </div>
568
- <div>
569
- <div className="flex items-center gap-2 mb-1">
570
- <div className="i-ph:monitor text-bolt-elements-textSecondary" />
571
- <p className="text-xs text-bolt-elements-textSecondary">Display</p>
572
- </div>
573
- <p className="text-sm font-medium text-bolt-elements-textPrimary">
574
- {systemInfo.screen} ({systemInfo.colorDepth}) @{systemInfo.pixelRatio}x
575
- </p>
576
  </div>
577
- <div>
578
- <div className="flex items-center gap-2 mb-1">
579
- <div className="i-ph:wifi-high text-bolt-elements-textSecondary" />
580
- <p className="text-xs text-bolt-elements-textSecondary">Connection</p>
581
- </div>
582
- <div className="flex items-center gap-2 mt-1">
583
- <span
584
- className={classNames('w-2 h-2 rounded-full', systemInfo.online ? 'bg-green-500' : 'bg-red-500')}
585
- />
586
- <span
587
- className={classNames('text-sm font-medium', systemInfo.online ? 'text-green-500' : 'text-red-500')}
588
- >
589
- {systemInfo.online ? 'Online' : 'Offline'}
 
 
 
 
 
 
 
 
 
 
 
 
590
  </span>
591
  </div>
 
 
 
 
 
 
 
 
592
  </div>
593
- <div>
594
- <div className="flex items-center gap-2 mb-1">
595
- <div className="i-ph:translate text-bolt-elements-textSecondary" />
596
- <p className="text-xs text-bolt-elements-textSecondary">Language</p>
597
- </div>
598
- <p className="text-sm font-medium text-bolt-elements-textPrimary">{systemInfo.language}</p>
 
 
599
  </div>
600
- <div>
601
- <div className="flex items-center gap-2 mb-1">
602
- <div className="i-ph:clock text-bolt-elements-textSecondary" />
603
- <p className="text-xs text-bolt-elements-textSecondary">Timezone</p>
604
- </div>
605
- <p className="text-sm font-medium text-bolt-elements-textPrimary">{systemInfo.timezone}</p>
606
  </div>
607
- <div>
608
- <div className="flex items-center gap-2 mb-1">
609
- <div className="i-ph:cpu text-bolt-elements-textSecondary" />
610
- <p className="text-xs text-bolt-elements-textSecondary">CPU Cores</p>
611
- </div>
612
- <p className="text-sm font-medium text-bolt-elements-textPrimary">{systemInfo.cores}</p>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
613
  </div>
614
  </div>
615
- <div className="mt-3 pt-3 border-t border-bolt-elements-borderColor">
616
- <div className="flex items-center gap-2 mb-1">
617
- <div className="i-ph:git-commit text-bolt-elements-textSecondary" />
618
- <p className="text-xs text-bolt-elements-textSecondary">Version</p>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
619
  </div>
620
- <p className="text-sm font-medium text-bolt-elements-textPrimary font-mono">
621
- {connitJson.commit.slice(0, 7)}
622
- <span className="ml-2 text-xs text-bolt-elements-textSecondary">
623
- (v{versionTag || '0.0.1'}) - {isLatestBranch ? 'nightly' : 'stable'}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
624
  </span>
625
- </p>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
626
  </div>
627
- </motion.div>
628
- </motion.div>
629
-
630
- <motion.div
631
- className="space-y-4"
632
- initial={{ opacity: 0, y: 20 }}
633
- animate={{ opacity: 1, y: 0 }}
634
- transition={{ delay: 0.2 }}
635
- >
636
- <div className="flex items-center gap-2 mb-4">
637
- <div className="i-ph:robot text-xl text-purple-500" />
638
- <h4 className="text-md font-medium text-bolt-elements-textPrimary">Local LLM Status</h4>
639
  </div>
640
- <motion.div className={classNames(settingsStyles.card, 'bg-bolt-elements-background-depth-2')}>
641
- <div className="divide-y divide-bolt-elements-borderColor">
642
- {activeProviders.map((provider) => (
643
- <div key={provider.name} className="p-4 first:pt-0 last:pb-0">
644
- <div className="flex items-center justify-between">
645
- <div className="flex items-center gap-3">
646
- <div className="flex-shrink-0">
647
- <div
648
- className={classNames(
649
- 'w-2 h-2 rounded-full',
650
- !provider.enabled ? 'bg-gray-400' : provider.isRunning ? 'bg-green-500' : 'bg-red-500',
651
- )}
652
- />
653
- </div>
654
- <div>
655
- <p className="text-sm font-medium text-bolt-elements-textPrimary">{provider.name}</p>
656
- {provider.url && (
657
- <p className="text-xs text-bolt-elements-textSecondary truncate max-w-[300px]">
658
- {provider.url}
659
- </p>
660
- )}
661
- </div>
662
- </div>
663
- <div className="flex items-center gap-2">
664
- <span
665
- className={classNames(
666
- 'px-2 py-0.5 text-xs rounded-full',
667
- provider.enabled
668
- ? 'bg-green-500/10 text-green-500'
669
- : 'bg-gray-500/10 text-bolt-elements-textSecondary',
670
- )}
671
- >
672
- {provider.enabled ? 'Enabled' : 'Disabled'}
673
- </span>
674
- {provider.enabled && (
675
- <span
676
- className={classNames(
677
- 'px-2 py-0.5 text-xs rounded-full',
678
- provider.isRunning ? 'bg-green-500/10 text-green-500' : 'bg-red-500/10 text-red-500',
679
- )}
680
- >
681
- {provider.isRunning ? 'Running' : 'Not Running'}
682
- </span>
683
- )}
684
- </div>
685
- </div>
686
 
687
- <div className="pl-5 mt-2 space-y-2">
688
- <div className="flex flex-wrap gap-2">
689
- <span className="text-xs text-bolt-elements-textSecondary">
690
- Last checked: {new Date(provider.lastChecked).toLocaleTimeString()}
691
- </span>
692
- {provider.responseTime && (
693
- <span className="text-xs text-bolt-elements-textSecondary">
694
- Response time: {Math.round(provider.responseTime)}ms
695
- </span>
696
- )}
697
- </div>
698
-
699
- {provider.error && (
700
- <div className="mt-2 text-xs text-red-500 bg-red-500/10 rounded-md p-2">
701
- <span className="font-medium">Error:</span> {provider.error}
702
- </div>
703
- )}
704
-
705
- {provider.url && (
706
- <div className="text-xs text-bolt-elements-textSecondary mt-2">
707
- <span className="font-medium">Endpoints checked:</span>
708
- <ul className="list-disc list-inside pl-2 mt-1 space-y-1">
709
- <li>{provider.url} (root)</li>
710
- <li>{provider.url}/api/health</li>
711
- <li>{provider.url}/v1/models</li>
712
- </ul>
713
- </div>
714
- )}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
715
  </div>
716
- </div>
717
- ))}
718
- {activeProviders.length === 0 && (
719
- <div className="p-4 text-center text-bolt-elements-textSecondary">No local LLMs configured</div>
720
- )}
 
 
721
  </div>
722
- </motion.div>
723
- </motion.div>
724
- </section>
725
  </div>
726
  );
727
  }
 
1
+ import React, { useEffect, useState } from 'react';
 
2
  import { toast } from 'react-toastify';
 
 
3
  import { classNames } from '~/utils/classNames';
4
+ import { logStore } from '~/lib/stores/logs';
5
 
6
  interface ProviderStatus {
7
+ id: string;
8
  name: string;
9
+ status: 'online' | 'offline' | 'error';
 
 
10
  error?: string;
 
 
 
11
  }
12
 
13
  interface SystemInfo {
14
  os: string;
15
+ arch: string;
16
+ platform: string;
17
+ cpus: string;
18
+ memory: {
19
+ total: string;
20
+ free: string;
21
+ used: string;
22
+ percentage: number;
23
+ };
24
+ node: string;
25
+ browser: {
26
+ name: string;
27
+ version: string;
28
+ language: string;
29
+ userAgent: string;
30
+ cookiesEnabled: boolean;
31
+ online: boolean;
32
+ platform: string;
33
+ cores: number;
34
+ };
35
+ screen: {
36
+ width: number;
37
+ height: number;
38
+ colorDepth: number;
39
+ pixelRatio: number;
40
+ };
41
+ time: {
42
+ timezone: string;
43
+ offset: number;
44
+ locale: string;
45
+ };
46
+ performance: {
47
+ memory: {
48
+ jsHeapSizeLimit: number;
49
+ totalJSHeapSize: number;
50
+ usedJSHeapSize: number;
51
+ usagePercentage: number;
52
+ };
53
+ timing: {
54
+ loadTime: number;
55
+ domReadyTime: number;
56
+ readyStart: number;
57
+ redirectTime: number;
58
+ appcacheTime: number;
59
+ unloadEventTime: number;
60
+ lookupDomainTime: number;
61
+ connectTime: number;
62
+ requestTime: number;
63
+ initDomTreeTime: number;
64
+ loadEventTime: number;
65
+ };
66
+ navigation: {
67
+ type: number;
68
+ redirectCount: number;
69
+ };
70
+ };
71
+ network: {
72
+ downlink: number;
73
+ effectiveType: string;
74
+ rtt: number;
75
+ saveData: boolean;
76
+ type: string;
77
+ };
78
+ battery?: {
79
+ charging: boolean;
80
+ chargingTime: number;
81
+ dischargingTime: number;
82
+ level: number;
83
+ };
84
+ storage: {
85
+ quota: number;
86
+ usage: number;
87
+ persistent: boolean;
88
+ temporary: boolean;
89
  };
90
  }
91
 
92
+ export default function DebugTab() {
93
+ const [providerStatuses, setProviderStatuses] = useState<ProviderStatus[]>([]);
94
+ const [systemInfo, setSystemInfo] = useState<SystemInfo | null>(null);
95
+ const [loading, setLoading] = useState({
96
+ systemInfo: false,
97
+ providers: false,
98
+ performance: false,
99
+ errors: false,
100
+ });
101
+ const [errorLog, setErrorLog] = useState<{
102
+ errors: any[];
103
+ lastCheck: string | null;
104
+ }>({
105
+ errors: [],
106
+ lastCheck: null,
107
+ });
108
+
109
+ // Fetch initial data
110
+ useEffect(() => {
111
+ checkProviderStatus();
112
+ getSystemInfo();
113
+ }, []);
114
 
115
+ // Set up error listeners when component mounts
116
+ useEffect(() => {
117
+ const errors: any[] = [];
118
+
119
+ const handleError = (event: ErrorEvent) => {
120
+ errors.push({
121
+ type: 'error',
122
+ message: event.message,
123
+ filename: event.filename,
124
+ lineNumber: event.lineno,
125
+ columnNumber: event.colno,
126
+ error: event.error,
127
+ timestamp: new Date().toISOString(),
128
+ });
129
+ };
130
 
131
+ const handleRejection = (event: PromiseRejectionEvent) => {
132
+ errors.push({
133
+ type: 'unhandledRejection',
134
+ reason: event.reason,
135
+ timestamp: new Date().toISOString(),
136
+ });
137
+ };
138
 
139
+ window.addEventListener('error', handleError);
140
+ window.addEventListener('unhandledrejection', handleRejection);
 
 
141
 
142
+ return () => {
143
+ window.removeEventListener('error', handleError);
144
+ window.removeEventListener('unhandledrejection', handleRejection);
145
+ };
146
+ }, []);
 
 
 
 
 
147
 
148
+ const checkProviderStatus = async () => {
149
+ try {
150
+ setLoading((prev) => ({ ...prev, providers: true }));
 
 
151
 
152
+ // Fetch real provider statuses
153
+ const providers: ProviderStatus[] = [];
 
154
 
155
+ // Check OpenAI status
156
+ try {
157
+ const openaiResponse = await fetch('/api/providers/openai/status');
158
+ providers.push({
159
+ id: 'openai',
160
+ name: 'OpenAI',
161
+ status: openaiResponse.ok ? 'online' : 'error',
162
+ error: !openaiResponse.ok ? 'API Error' : undefined,
163
+ });
164
+ } catch {
165
+ providers.push({ id: 'openai', name: 'OpenAI', status: 'offline' });
166
+ }
167
 
168
+ // Check Anthropic status
169
+ try {
170
+ const anthropicResponse = await fetch('/api/providers/anthropic/status');
171
+ providers.push({
172
+ id: 'anthropic',
173
+ name: 'Anthropic',
174
+ status: anthropicResponse.ok ? 'online' : 'error',
175
+ error: !anthropicResponse.ok ? 'API Error' : undefined,
176
+ });
177
+ } catch {
178
+ providers.push({ id: 'anthropic', name: 'Anthropic', status: 'offline' });
 
 
179
  }
180
+
181
+ // Check Local Models status
182
+ try {
183
+ const localResponse = await fetch('/api/providers/local/status');
184
+ providers.push({
185
+ id: 'local',
186
+ name: 'Local Models',
187
+ status: localResponse.ok ? 'online' : 'error',
188
+ error: !localResponse.ok ? 'API Error' : undefined,
189
+ });
190
+ } catch {
191
+ providers.push({ id: 'local', name: 'Local Models', status: 'offline' });
192
  }
 
193
 
194
+ // Check Ollama status
195
+ try {
196
+ const ollamaResponse = await fetch('/api/providers/ollama/status');
197
+ providers.push({
198
+ id: 'ollama',
199
+ name: 'Ollama',
200
+ status: ollamaResponse.ok ? 'online' : 'error',
201
+ error: !ollamaResponse.ok ? 'API Error' : undefined,
202
+ });
203
+ } catch {
204
+ providers.push({ id: 'ollama', name: 'Ollama', status: 'offline' });
205
+ }
206
 
207
+ setProviderStatuses(providers);
208
+ toast.success('Provider status updated');
209
+ } catch (error) {
210
+ toast.error('Failed to check provider status');
211
+ console.error('Failed to check provider status:', error);
212
+ } finally {
213
+ setLoading((prev) => ({ ...prev, providers: false }));
214
+ }
215
  };
216
 
217
+ const getSystemInfo = async () => {
218
+ try {
219
+ setLoading((prev) => ({ ...prev, systemInfo: true }));
220
+
221
+ // Get browser info
222
+ const ua = navigator.userAgent;
223
+ const browserName = ua.includes('Firefox')
224
+ ? 'Firefox'
225
+ : ua.includes('Chrome')
226
+ ? 'Chrome'
227
+ : ua.includes('Safari')
228
+ ? 'Safari'
229
+ : ua.includes('Edge')
230
+ ? 'Edge'
231
+ : 'Unknown';
232
+ const browserVersion = ua.match(/(Firefox|Chrome|Safari|Edge)\/([0-9.]+)/)?.[2] || 'Unknown';
233
+
234
+ // Get performance metrics
235
+ const memory = (performance as any).memory || {};
236
+ const timing = performance.timing;
237
+ const navigation = performance.navigation;
238
+ const connection = (navigator as any).connection;
239
+
240
+ // Get battery info
241
+ let batteryInfo;
242
 
243
+ try {
244
+ const battery = await (navigator as any).getBattery();
245
+ batteryInfo = {
246
+ charging: battery.charging,
247
+ chargingTime: battery.chargingTime,
248
+ dischargingTime: battery.dischargingTime,
249
+ level: battery.level * 100,
250
+ };
251
+ } catch {
252
+ console.log('Battery API not supported');
253
  }
254
 
255
+ // Get storage info
256
+ let storageInfo = {
257
+ quota: 0,
258
+ usage: 0,
259
+ persistent: false,
260
+ temporary: false,
261
+ };
262
 
263
+ try {
264
+ const storage = await navigator.storage.estimate();
265
+ const persistent = await navigator.storage.persist();
266
+ storageInfo = {
267
+ quota: storage.quota || 0,
268
+ usage: storage.usage || 0,
269
+ persistent,
270
+ temporary: !persistent,
271
+ };
272
+ } catch {
273
+ console.log('Storage API not supported');
274
+ }
275
 
276
+ // Get memory info from browser performance API
277
+ const performanceMemory = (performance as any).memory || {};
278
+ const totalMemory = performanceMemory.jsHeapSizeLimit || 0;
279
+ const usedMemory = performanceMemory.usedJSHeapSize || 0;
280
+ const freeMemory = totalMemory - usedMemory;
281
+ const memoryPercentage = totalMemory ? (usedMemory / totalMemory) * 100 : 0;
282
+
283
+ const systemInfo: SystemInfo = {
284
+ os: navigator.platform,
285
+ arch: navigator.userAgent.includes('x64') ? 'x64' : navigator.userAgent.includes('arm') ? 'arm' : 'unknown',
286
+ platform: navigator.platform,
287
+ cpus: navigator.hardwareConcurrency + ' cores',
288
+ memory: {
289
+ total: formatBytes(totalMemory),
290
+ free: formatBytes(freeMemory),
291
+ used: formatBytes(usedMemory),
292
+ percentage: Math.round(memoryPercentage),
293
+ },
294
+ node: 'browser',
295
+ browser: {
296
+ name: browserName,
297
+ version: browserVersion,
298
+ language: navigator.language,
299
+ userAgent: navigator.userAgent,
300
+ cookiesEnabled: navigator.cookieEnabled,
301
+ online: navigator.onLine,
302
+ platform: navigator.platform,
303
+ cores: navigator.hardwareConcurrency,
304
+ },
305
+ screen: {
306
+ width: window.screen.width,
307
+ height: window.screen.height,
308
+ colorDepth: window.screen.colorDepth,
309
+ pixelRatio: window.devicePixelRatio,
310
+ },
311
+ time: {
312
+ timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
313
+ offset: new Date().getTimezoneOffset(),
314
+ locale: navigator.language,
315
+ },
316
+ performance: {
317
+ memory: {
318
+ jsHeapSizeLimit: memory.jsHeapSizeLimit || 0,
319
+ totalJSHeapSize: memory.totalJSHeapSize || 0,
320
+ usedJSHeapSize: memory.usedJSHeapSize || 0,
321
+ usagePercentage: memory.totalJSHeapSize ? (memory.usedJSHeapSize / memory.totalJSHeapSize) * 100 : 0,
322
+ },
323
+ timing: {
324
+ loadTime: timing.loadEventEnd - timing.navigationStart,
325
+ domReadyTime: timing.domContentLoadedEventEnd - timing.navigationStart,
326
+ readyStart: timing.fetchStart - timing.navigationStart,
327
+ redirectTime: timing.redirectEnd - timing.redirectStart,
328
+ appcacheTime: timing.domainLookupStart - timing.fetchStart,
329
+ unloadEventTime: timing.unloadEventEnd - timing.unloadEventStart,
330
+ lookupDomainTime: timing.domainLookupEnd - timing.domainLookupStart,
331
+ connectTime: timing.connectEnd - timing.connectStart,
332
+ requestTime: timing.responseEnd - timing.requestStart,
333
+ initDomTreeTime: timing.domInteractive - timing.responseEnd,
334
+ loadEventTime: timing.loadEventEnd - timing.loadEventStart,
335
+ },
336
+ navigation: {
337
+ type: navigation.type,
338
+ redirectCount: navigation.redirectCount,
339
+ },
340
+ },
341
+ network: {
342
+ downlink: connection?.downlink || 0,
343
+ effectiveType: connection?.effectiveType || 'unknown',
344
+ rtt: connection?.rtt || 0,
345
+ saveData: connection?.saveData || false,
346
+ type: connection?.type || 'unknown',
347
+ },
348
+ battery: batteryInfo,
349
+ storage: storageInfo,
350
+ };
351
 
352
+ setSystemInfo(systemInfo);
353
+ toast.success('System information updated');
354
+ } catch (error) {
355
+ toast.error('Failed to get system information');
356
+ console.error('Failed to get system information:', error);
357
+ } finally {
358
+ setLoading((prev) => ({ ...prev, systemInfo: false }));
359
+ }
360
  };
361
 
362
+ // Helper function to format bytes to human readable format
363
+ const formatBytes = (bytes: number) => {
364
+ const units = ['B', 'KB', 'MB', 'GB'];
365
+ let size = bytes;
366
+ let unitIndex = 0;
367
 
368
+ while (size >= 1024 && unitIndex < units.length - 1) {
369
+ size /= 1024;
370
+ unitIndex++;
371
  }
372
 
373
+ return `${Math.round(size)} ${units[unitIndex]}`;
 
 
 
 
374
  };
375
 
376
+ const handleLogSystemInfo = () => {
377
+ if (!systemInfo) {
378
+ return;
 
 
379
  }
380
 
381
+ logStore.logSystem('System Information', {
382
+ os: systemInfo.os,
383
+ arch: systemInfo.arch,
384
+ cpus: systemInfo.cpus,
385
+ memory: systemInfo.memory,
386
+ node: systemInfo.node,
387
+ });
388
+ toast.success('System information logged');
389
  };
390
 
391
+ const handleLogPerformance = () => {
392
+ try {
393
+ setLoading((prev) => ({ ...prev, performance: true }));
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
394
 
395
+ // Get performance metrics using modern Performance API
396
+ const performanceEntries = performance.getEntriesByType('navigation')[0] as PerformanceNavigationTiming;
397
+ const memory = (performance as any).memory;
 
 
 
 
 
 
 
 
 
 
398
 
399
+ // Calculate timing metrics
400
+ const timingMetrics = {
401
+ loadTime: performanceEntries.loadEventEnd - performanceEntries.startTime,
402
+ domReadyTime: performanceEntries.domContentLoadedEventEnd - performanceEntries.startTime,
403
+ fetchTime: performanceEntries.responseEnd - performanceEntries.fetchStart,
404
+ redirectTime: performanceEntries.redirectEnd - performanceEntries.redirectStart,
405
+ dnsTime: performanceEntries.domainLookupEnd - performanceEntries.domainLookupStart,
406
+ tcpTime: performanceEntries.connectEnd - performanceEntries.connectStart,
407
+ ttfb: performanceEntries.responseStart - performanceEntries.requestStart,
408
+ processingTime: performanceEntries.loadEventEnd - performanceEntries.responseEnd,
409
+ };
410
 
411
+ // Get resource timing data
412
+ const resourceEntries = performance.getEntriesByType('resource');
413
+ const resourceStats = {
414
+ totalResources: resourceEntries.length,
415
+ totalSize: resourceEntries.reduce((total, entry) => total + (entry as any).transferSize || 0, 0),
416
+ totalTime: Math.max(...resourceEntries.map((entry) => entry.duration)),
417
+ };
418
 
419
+ // Get memory metrics
420
+ const memoryMetrics = memory
421
+ ? {
422
+ jsHeapSizeLimit: memory.jsHeapSizeLimit,
423
+ totalJSHeapSize: memory.totalJSHeapSize,
424
+ usedJSHeapSize: memory.usedJSHeapSize,
425
+ heapUtilization: (memory.usedJSHeapSize / memory.totalJSHeapSize) * 100,
426
+ }
427
+ : null;
428
 
429
+ // Get frame rate metrics
430
+ let fps = 0;
431
 
432
+ if ('requestAnimationFrame' in window) {
433
+ const times: number[] = [];
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
434
 
435
+ function calculateFPS(now: number) {
436
+ times.push(now);
437
+
438
+ if (times.length > 10) {
439
+ const fps = Math.round((1000 * 10) / (now - times[0]));
440
+ times.shift();
441
+
442
+ return fps;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
443
  }
444
 
445
+ requestAnimationFrame(calculateFPS);
446
+
447
+ return 0;
 
448
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
449
 
450
+ fps = calculateFPS(performance.now());
451
+ }
 
 
 
 
 
 
 
 
 
452
 
453
+ // Log all performance metrics
454
+ logStore.logSystem('Performance Metrics', {
455
+ timing: timingMetrics,
456
+ resources: resourceStats,
457
+ memory: memoryMetrics,
458
+ fps,
459
+ timestamp: new Date().toISOString(),
460
+ navigationEntry: {
461
+ type: performanceEntries.type,
462
+ redirectCount: performanceEntries.redirectCount,
463
+ },
464
+ });
465
+
466
+ toast.success('Performance metrics logged');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
467
  } catch (error) {
468
+ toast.error('Failed to log performance metrics');
469
+ console.error('Failed to log performance metrics:', error);
470
+ } finally {
471
+ setLoading((prev) => ({ ...prev, performance: false }));
472
  }
473
  };
474
 
475
+ const handleCheckErrors = () => {
 
 
 
 
 
 
 
 
 
 
 
 
476
  try {
477
+ setLoading((prev) => ({ ...prev, errors: true }));
478
+
479
+ // Get any errors from the performance entries
480
+ const resourceErrors = performance
481
+ .getEntriesByType('resource')
482
+ .filter((entry) => {
483
+ const failedEntry = entry as PerformanceResourceTiming;
484
+ return failedEntry.responseEnd - failedEntry.startTime === 0;
485
+ })
486
+ .map((entry) => ({
487
+ type: 'networkError',
488
+ resource: entry.name,
489
+ timestamp: new Date().toISOString(),
490
+ }));
491
+
492
+ // Combine collected errors with resource errors
493
+ const allErrors = [...errorLog.errors, ...resourceErrors];
494
+
495
+ if (allErrors.length > 0) {
496
+ logStore.logError('JavaScript Errors Found', {
497
+ errors: allErrors,
498
+ timestamp: new Date().toISOString(),
499
+ });
500
+ toast.error(`Found ${allErrors.length} error(s)`);
501
  } else {
502
+ toast.success('No errors found');
503
  }
504
+
505
+ // Update error log
506
+ setErrorLog({
507
+ errors: allErrors,
508
+ lastCheck: new Date().toISOString(),
509
+ });
510
  } catch (error) {
511
+ toast.error('Failed to check for errors');
512
+ console.error('Failed to check for errors:', error);
513
  } finally {
514
+ setLoading((prev) => ({ ...prev, errors: false }));
515
  }
516
+ };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
517
 
518
  return (
519
  <div className="space-y-6">
520
+ {/* System Information */}
521
+ <div className="p-6 rounded-xl bg-white dark:bg-[#0A0A0A] border border-[#E5E5E5] dark:border-[#1A1A1A]">
522
+ <div className="flex items-center justify-between mb-4">
523
+ <div className="flex items-center gap-3">
524
+ <div className="i-ph:cpu text-purple-500 w-5 h-5" />
525
+ <h3 className="text-base font-medium text-bolt-elements-textPrimary">System Information</h3>
526
+ </div>
527
+ <div className="flex items-center gap-2">
528
+ <button
529
+ onClick={handleLogSystemInfo}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
530
  className={classNames(
531
+ 'flex items-center gap-2 px-3 py-2 rounded-lg text-sm',
532
+ 'bg-[#F5F5F5] dark:bg-[#1A1A1A]',
533
+ 'hover:bg-[#E5E5E5] dark:hover:bg-[#252525]',
534
+ 'transition-colors',
535
  )}
536
+ >
537
+ <div className="i-ph:note text-bolt-elements-textSecondary w-4 h-4" />
538
+ Log
539
+ </button>
540
+ <button
541
+ onClick={getSystemInfo}
542
+ className={classNames(
543
+ 'flex items-center gap-2 px-3 py-2 rounded-lg text-sm',
544
+ 'bg-[#F5F5F5] dark:bg-[#1A1A1A]',
545
+ 'hover:bg-[#E5E5E5] dark:hover:bg-[#252525]',
546
+ 'transition-colors',
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
547
  )}
548
+ >
549
+ <div className={classNames('i-ph:arrows-clockwise w-4 h-4', loading.systemInfo ? 'animate-spin' : '')} />
550
+ Refresh
551
+ </button>
 
 
 
 
 
 
552
  </div>
553
+ </div>
554
+ {systemInfo ? (
555
+ <div className="grid grid-cols-2 gap-6">
556
+ <div className="space-y-2">
557
+ <div className="text-sm flex items-center gap-2">
558
+ <div className="i-ph:desktop text-bolt-elements-textSecondary w-4 h-4" />
559
+ <span className="text-bolt-elements-textSecondary">OS: </span>
560
+ <span className="text-bolt-elements-textPrimary">{systemInfo.os}</span>
561
  </div>
562
+ <div className="text-sm flex items-center gap-2">
563
+ <div className="i-ph:device-mobile text-bolt-elements-textSecondary w-4 h-4" />
564
+ <span className="text-bolt-elements-textSecondary">Platform: </span>
565
+ <span className="text-bolt-elements-textPrimary">{systemInfo.platform}</span>
 
 
566
  </div>
567
+ <div className="text-sm flex items-center gap-2">
568
+ <div className="i-ph:microchip text-bolt-elements-textSecondary w-4 h-4" />
569
+ <span className="text-bolt-elements-textSecondary">Architecture: </span>
570
+ <span className="text-bolt-elements-textPrimary">{systemInfo.arch}</span>
 
 
571
  </div>
572
+ <div className="text-sm flex items-center gap-2">
573
+ <div className="i-ph:cpu text-bolt-elements-textSecondary w-4 h-4" />
574
+ <span className="text-bolt-elements-textSecondary">CPU Cores: </span>
575
+ <span className="text-bolt-elements-textPrimary">{systemInfo.cpus}</span>
 
 
 
 
576
  </div>
577
+ <div className="text-sm flex items-center gap-2">
578
+ <div className="i-ph:node text-bolt-elements-textSecondary w-4 h-4" />
579
+ <span className="text-bolt-elements-textSecondary">Node Version: </span>
580
+ <span className="text-bolt-elements-textPrimary">{systemInfo.node}</span>
581
+ </div>
582
+ <div className="text-sm flex items-center gap-2">
583
+ <div className="i-ph:wifi-high text-bolt-elements-textSecondary w-4 h-4" />
584
+ <span className="text-bolt-elements-textSecondary">Network Type: </span>
585
+ <span className="text-bolt-elements-textPrimary">
586
+ {systemInfo.network.type} ({systemInfo.network.effectiveType})
587
+ </span>
588
+ </div>
589
+ <div className="text-sm flex items-center gap-2">
590
+ <div className="i-ph:gauge text-bolt-elements-textSecondary w-4 h-4" />
591
+ <span className="text-bolt-elements-textSecondary">Network Speed: </span>
592
+ <span className="text-bolt-elements-textPrimary">
593
+ {systemInfo.network.downlink}Mbps (RTT: {systemInfo.network.rtt}ms)
594
+ </span>
595
+ </div>
596
+ {systemInfo.battery && (
597
+ <div className="text-sm flex items-center gap-2">
598
+ <div className="i-ph:battery-charging text-bolt-elements-textSecondary w-4 h-4" />
599
+ <span className="text-bolt-elements-textSecondary">Battery: </span>
600
+ <span className="text-bolt-elements-textPrimary">
601
+ {systemInfo.battery.level.toFixed(1)}% {systemInfo.battery.charging ? '(Charging)' : ''}
602
  </span>
603
  </div>
604
+ )}
605
+ <div className="text-sm flex items-center gap-2">
606
+ <div className="i-ph:hard-drive text-bolt-elements-textSecondary w-4 h-4" />
607
+ <span className="text-bolt-elements-textSecondary">Storage: </span>
608
+ <span className="text-bolt-elements-textPrimary">
609
+ {(systemInfo.storage.usage / (1024 * 1024 * 1024)).toFixed(2)}GB /{' '}
610
+ {(systemInfo.storage.quota / (1024 * 1024 * 1024)).toFixed(2)}GB
611
+ </span>
612
  </div>
613
+ </div>
614
+ <div className="space-y-2">
615
+ <div className="text-sm flex items-center gap-2">
616
+ <div className="i-ph:database text-bolt-elements-textSecondary w-4 h-4" />
617
+ <span className="text-bolt-elements-textSecondary">Memory Usage: </span>
618
+ <span className="text-bolt-elements-textPrimary">
619
+ {systemInfo.memory.used} / {systemInfo.memory.total} ({systemInfo.memory.percentage}%)
620
+ </span>
621
  </div>
622
+ <div className="text-sm flex items-center gap-2">
623
+ <div className="i-ph:browser text-bolt-elements-textSecondary w-4 h-4" />
624
+ <span className="text-bolt-elements-textSecondary">Browser: </span>
625
+ <span className="text-bolt-elements-textPrimary">
626
+ {systemInfo.browser.name} {systemInfo.browser.version}
627
+ </span>
628
  </div>
629
+ <div className="text-sm flex items-center gap-2">
630
+ <div className="i-ph:monitor text-bolt-elements-textSecondary w-4 h-4" />
631
+ <span className="text-bolt-elements-textSecondary">Screen: </span>
632
+ <span className="text-bolt-elements-textPrimary">
633
+ {systemInfo.screen.width}x{systemInfo.screen.height} ({systemInfo.screen.pixelRatio}x)
634
+ </span>
635
+ </div>
636
+ <div className="text-sm flex items-center gap-2">
637
+ <div className="i-ph:clock text-bolt-elements-textSecondary w-4 h-4" />
638
+ <span className="text-bolt-elements-textSecondary">Timezone: </span>
639
+ <span className="text-bolt-elements-textPrimary">{systemInfo.time.timezone}</span>
640
+ </div>
641
+ <div className="text-sm flex items-center gap-2">
642
+ <div className="i-ph:translate text-bolt-elements-textSecondary w-4 h-4" />
643
+ <span className="text-bolt-elements-textSecondary">Language: </span>
644
+ <span className="text-bolt-elements-textPrimary">{systemInfo.browser.language}</span>
645
+ </div>
646
+ <div className="text-sm flex items-center gap-2">
647
+ <div className="i-ph:chart-pie text-bolt-elements-textSecondary w-4 h-4" />
648
+ <span className="text-bolt-elements-textSecondary">JS Heap: </span>
649
+ <span className="text-bolt-elements-textPrimary">
650
+ {(systemInfo.performance.memory.usedJSHeapSize / (1024 * 1024)).toFixed(1)}MB /{' '}
651
+ {(systemInfo.performance.memory.totalJSHeapSize / (1024 * 1024)).toFixed(1)}MB (
652
+ {systemInfo.performance.memory.usagePercentage.toFixed(1)}%)
653
+ </span>
654
+ </div>
655
+ <div className="text-sm flex items-center gap-2">
656
+ <div className="i-ph:timer text-bolt-elements-textSecondary w-4 h-4" />
657
+ <span className="text-bolt-elements-textSecondary">Page Load: </span>
658
+ <span className="text-bolt-elements-textPrimary">
659
+ {(systemInfo.performance.timing.loadTime / 1000).toFixed(2)}s
660
+ </span>
661
+ </div>
662
+ <div className="text-sm flex items-center gap-2">
663
+ <div className="i-ph:code text-bolt-elements-textSecondary w-4 h-4" />
664
+ <span className="text-bolt-elements-textSecondary">DOM Ready: </span>
665
+ <span className="text-bolt-elements-textPrimary">
666
+ {(systemInfo.performance.timing.domReadyTime / 1000).toFixed(2)}s
667
+ </span>
668
  </div>
669
  </div>
670
+ </div>
671
+ ) : (
672
+ <div className="text-sm text-bolt-elements-textSecondary">Loading system information...</div>
673
+ )}
674
+ </div>
675
+
676
+ {/* Provider Status */}
677
+ <div className="p-6 rounded-xl bg-white dark:bg-[#0A0A0A] border border-[#E5E5E5] dark:border-[#1A1A1A]">
678
+ <div className="flex items-center justify-between mb-4">
679
+ <div className="flex items-center gap-3">
680
+ <div className="i-ph:robot text-purple-500 w-5 h-5" />
681
+ <h3 className="text-base font-medium text-bolt-elements-textPrimary">Provider Status</h3>
682
+ </div>
683
+ <button
684
+ onClick={checkProviderStatus}
685
+ className={classNames(
686
+ 'flex items-center gap-2 px-3 py-2 rounded-lg text-sm',
687
+ 'bg-[#F5F5F5] dark:bg-[#1A1A1A]',
688
+ 'hover:bg-[#E5E5E5] dark:hover:bg-[#252525]',
689
+ 'transition-colors',
690
+ )}
691
+ >
692
+ <div className={classNames('i-ph:arrows-clockwise w-4 h-4', loading.providers ? 'animate-spin' : '')} />
693
+ Refresh
694
+ </button>
695
+ </div>
696
+ <div className="grid grid-cols-2 gap-4">
697
+ {providerStatuses.map((provider) => (
698
+ <div
699
+ key={provider.id}
700
+ className="flex items-center justify-between p-3 rounded-lg bg-[#F8F8F8] dark:bg-[#141414]"
701
+ >
702
+ <div className="flex items-center gap-3">
703
+ <div
704
+ className={classNames(
705
+ 'w-2 h-2 rounded-full',
706
+ provider.status === 'online'
707
+ ? 'bg-green-500'
708
+ : provider.status === 'offline'
709
+ ? 'bg-red-500'
710
+ : 'bg-yellow-500',
711
+ )}
712
+ />
713
+ <span className="text-sm text-bolt-elements-textPrimary">{provider.name}</span>
714
  </div>
715
+ <span className="text-xs text-bolt-elements-textSecondary capitalize">{provider.status}</span>
716
+ </div>
717
+ ))}
718
+ </div>
719
+ </div>
720
+
721
+ {/* Performance Metrics */}
722
+ <div className="p-6 rounded-xl bg-white dark:bg-[#0A0A0A] border border-[#E5E5E5] dark:border-[#1A1A1A]">
723
+ <div className="flex items-center justify-between mb-4">
724
+ <div className="flex items-center gap-3">
725
+ <div className="i-ph:chart-line text-purple-500 w-5 h-5" />
726
+ <h3 className="text-base font-medium text-bolt-elements-textPrimary">Performance Metrics</h3>
727
+ </div>
728
+ <button
729
+ onClick={handleLogPerformance}
730
+ className={classNames(
731
+ 'flex items-center gap-2 px-3 py-2 rounded-lg text-sm',
732
+ 'bg-[#F5F5F5] dark:bg-[#1A1A1A]',
733
+ 'hover:bg-[#E5E5E5] dark:hover:bg-[#252525]',
734
+ 'transition-colors',
735
+ )}
736
+ >
737
+ <div className={classNames('i-ph:note w-4 h-4', loading.performance ? 'animate-spin' : '')} />
738
+ Log Performance
739
+ </button>
740
+ </div>
741
+ {systemInfo && (
742
+ <div className="grid grid-cols-2 gap-4">
743
+ <div className="space-y-2">
744
+ <div className="text-sm">
745
+ <span className="text-bolt-elements-textSecondary">Page Load Time: </span>
746
+ <span className="text-bolt-elements-textPrimary">
747
+ {(systemInfo.performance.timing.loadTime / 1000).toFixed(2)}s
748
+ </span>
749
+ </div>
750
+ <div className="text-sm">
751
+ <span className="text-bolt-elements-textSecondary">DOM Ready Time: </span>
752
+ <span className="text-bolt-elements-textPrimary">
753
+ {(systemInfo.performance.timing.domReadyTime / 1000).toFixed(2)}s
754
+ </span>
755
+ </div>
756
+ <div className="text-sm">
757
+ <span className="text-bolt-elements-textSecondary">Request Time: </span>
758
+ <span className="text-bolt-elements-textPrimary">
759
+ {(systemInfo.performance.timing.requestTime / 1000).toFixed(2)}s
760
  </span>
761
+ </div>
762
+ <div className="text-sm">
763
+ <span className="text-bolt-elements-textSecondary">Redirect Time: </span>
764
+ <span className="text-bolt-elements-textPrimary">
765
+ {(systemInfo.performance.timing.redirectTime / 1000).toFixed(2)}s
766
+ </span>
767
+ </div>
768
+ </div>
769
+ <div className="space-y-2">
770
+ <div className="text-sm">
771
+ <span className="text-bolt-elements-textSecondary">JS Heap Usage: </span>
772
+ <span className="text-bolt-elements-textPrimary">
773
+ {(systemInfo.performance.memory.usedJSHeapSize / (1024 * 1024)).toFixed(1)}MB /{' '}
774
+ {(systemInfo.performance.memory.totalJSHeapSize / (1024 * 1024)).toFixed(1)}MB
775
+ </span>
776
+ </div>
777
+ <div className="text-sm">
778
+ <span className="text-bolt-elements-textSecondary">Heap Utilization: </span>
779
+ <span className="text-bolt-elements-textPrimary">
780
+ {systemInfo.performance.memory.usagePercentage.toFixed(1)}%
781
+ </span>
782
+ </div>
783
+ <div className="text-sm">
784
+ <span className="text-bolt-elements-textSecondary">Navigation Type: </span>
785
+ <span className="text-bolt-elements-textPrimary">
786
+ {systemInfo.performance.navigation.type === 0
787
+ ? 'Navigate'
788
+ : systemInfo.performance.navigation.type === 1
789
+ ? 'Reload'
790
+ : systemInfo.performance.navigation.type === 2
791
+ ? 'Back/Forward'
792
+ : 'Other'}
793
+ </span>
794
+ </div>
795
+ <div className="text-sm">
796
+ <span className="text-bolt-elements-textSecondary">Redirects: </span>
797
+ <span className="text-bolt-elements-textPrimary">
798
+ {systemInfo.performance.navigation.redirectCount}
799
+ </span>
800
+ </div>
801
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
802
  </div>
803
+ )}
804
+ </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
805
 
806
+ {/* Error Check */}
807
+ <div className="p-6 rounded-xl bg-white dark:bg-[#0A0A0A] border border-[#E5E5E5] dark:border-[#1A1A1A]">
808
+ <div className="flex items-center justify-between mb-4">
809
+ <div className="flex items-center gap-3">
810
+ <div className="i-ph:warning text-purple-500 w-5 h-5" />
811
+ <h3 className="text-base font-medium text-bolt-elements-textPrimary">Error Check</h3>
812
+ </div>
813
+ <button
814
+ onClick={handleCheckErrors}
815
+ className={classNames(
816
+ 'flex items-center gap-2 px-3 py-2 rounded-lg text-sm',
817
+ 'bg-[#F5F5F5] dark:bg-[#1A1A1A]',
818
+ 'hover:bg-[#E5E5E5] dark:hover:bg-[#252525]',
819
+ 'transition-colors',
820
+ )}
821
+ >
822
+ <div className={classNames('i-ph:magnifying-glass w-4 h-4', loading.errors ? 'animate-spin' : '')} />
823
+ Check for Errors
824
+ </button>
825
+ </div>
826
+ <div className="space-y-4">
827
+ <div className="text-sm text-bolt-elements-textSecondary">
828
+ Checks for:
829
+ <ul className="list-disc list-inside mt-2 space-y-1">
830
+ <li>Unhandled JavaScript errors</li>
831
+ <li>Unhandled Promise rejections</li>
832
+ <li>Runtime exceptions</li>
833
+ <li>Network errors</li>
834
+ </ul>
835
+ </div>
836
+ <div className="text-sm">
837
+ <span className="text-bolt-elements-textSecondary">Last Check: </span>
838
+ <span className="text-bolt-elements-textPrimary">
839
+ {loading.errors
840
+ ? 'Checking...'
841
+ : errorLog.lastCheck
842
+ ? `Last checked ${new Date(errorLog.lastCheck).toLocaleString()} (${errorLog.errors.length} errors found)`
843
+ : 'Click to check for errors'}
844
+ </span>
845
+ </div>
846
+ {errorLog.errors.length > 0 && (
847
+ <div className="mt-4">
848
+ <div className="text-sm font-medium text-bolt-elements-textPrimary mb-2">Recent Errors:</div>
849
+ <div className="space-y-2">
850
+ {errorLog.errors.slice(0, 3).map((error, index) => (
851
+ <div key={index} className="text-sm text-red-500 dark:text-red-400">
852
+ {error.type === 'error' && `${error.message} (${error.filename}:${error.lineNumber})`}
853
+ {error.type === 'unhandledRejection' && `Unhandled Promise Rejection: ${error.reason}`}
854
+ {error.type === 'networkError' && `Network Error: Failed to load ${error.resource}`}
855
  </div>
856
+ ))}
857
+ {errorLog.errors.length > 3 && (
858
+ <div className="text-sm text-bolt-elements-textSecondary">
859
+ And {errorLog.errors.length - 3} more errors...
860
+ </div>
861
+ )}
862
+ </div>
863
  </div>
864
+ )}
865
+ </div>
866
+ </div>
867
  </div>
868
  );
869
  }
app/components/settings/developer/DeveloperWindow.tsx ADDED
@@ -0,0 +1,378 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as RadixDialog from '@radix-ui/react-dialog';
2
+ import { motion } from 'framer-motion';
3
+ import { useState } from 'react';
4
+ import { classNames } from '~/utils/classNames';
5
+ import { TabManagement } from './TabManagement';
6
+ import { TabTile } from '~/components/settings/shared/TabTile';
7
+ import type { TabType, TabVisibilityConfig } from '~/components/settings/settings.types';
8
+ import { tabConfigurationStore, updateTabConfiguration } from '~/lib/stores/settings';
9
+ import { useStore } from '@nanostores/react';
10
+ import { DndProvider, useDrag, useDrop } from 'react-dnd';
11
+ import { HTML5Backend } from 'react-dnd-html5-backend';
12
+ import DebugTab from '~/components/settings/debug/DebugTab';
13
+ import { EventLogsTab } from '~/components/settings/event-logs/EventLogsTab';
14
+ import UpdateTab from '~/components/settings/update/UpdateTab';
15
+ import { ProvidersTab } from '~/components/settings/providers/ProvidersTab';
16
+ import DataTab from '~/components/settings/data/DataTab';
17
+ import FeaturesTab from '~/components/settings/features/FeaturesTab';
18
+ import NotificationsTab from '~/components/settings/notifications/NotificationsTab';
19
+ import SettingsTab from '~/components/settings/settings/SettingsTab';
20
+ import ProfileTab from '~/components/settings/profile/ProfileTab';
21
+ import ConnectionsTab from '~/components/settings/connections/ConnectionsTab';
22
+ import { useUpdateCheck, useFeatures, useNotifications, useConnectionStatus, useDebugStatus } from '~/lib/hooks';
23
+
24
+ interface DraggableTabTileProps {
25
+ tab: TabVisibilityConfig;
26
+ index: number;
27
+ moveTab: (dragIndex: number, hoverIndex: number) => void;
28
+ onClick: () => void;
29
+ isActive: boolean;
30
+ hasUpdate: boolean;
31
+ statusMessage: string;
32
+ description: string;
33
+ isLoading?: boolean;
34
+ }
35
+
36
+ const TAB_DESCRIPTIONS: Record<TabType, string> = {
37
+ profile: 'Manage your profile and account settings',
38
+ settings: 'Configure application preferences',
39
+ notifications: 'View and manage your notifications',
40
+ features: 'Explore new and upcoming features',
41
+ data: 'Manage your data and storage',
42
+ providers: 'Configure AI providers and models',
43
+ connection: 'Check connection status and settings',
44
+ debug: 'Debug tools and system information',
45
+ 'event-logs': 'View system events and logs',
46
+ update: 'Check for updates and release notes',
47
+ };
48
+
49
+ const DraggableTabTile = ({
50
+ tab,
51
+ index,
52
+ moveTab,
53
+ onClick,
54
+ isActive,
55
+ hasUpdate,
56
+ statusMessage,
57
+ description,
58
+ isLoading,
59
+ }: DraggableTabTileProps) => {
60
+ const [{ isDragging }, drag] = useDrag({
61
+ type: 'tab',
62
+ item: { index },
63
+ collect: (monitor) => ({
64
+ isDragging: monitor.isDragging(),
65
+ }),
66
+ });
67
+
68
+ const [, drop] = useDrop({
69
+ accept: 'tab',
70
+ hover: (item: { index: number }) => {
71
+ if (item.index === index) {
72
+ return;
73
+ }
74
+
75
+ moveTab(item.index, index);
76
+ item.index = index;
77
+ },
78
+ });
79
+
80
+ return (
81
+ <div ref={(node) => drag(drop(node))} style={{ opacity: isDragging ? 0.5 : 1 }}>
82
+ <TabTile
83
+ tab={tab}
84
+ onClick={onClick}
85
+ isActive={isActive}
86
+ hasUpdate={hasUpdate}
87
+ statusMessage={statusMessage}
88
+ description={description}
89
+ isLoading={isLoading}
90
+ />
91
+ </div>
92
+ );
93
+ };
94
+
95
+ interface DeveloperWindowProps {
96
+ open: boolean;
97
+ onClose: () => void;
98
+ }
99
+
100
+ export const DeveloperWindow = ({ open, onClose }: DeveloperWindowProps) => {
101
+ const tabConfiguration = useStore(tabConfigurationStore);
102
+ const [activeTab, setActiveTab] = useState<TabType | null>(null);
103
+ const [showTabManagement, setShowTabManagement] = useState(false);
104
+ const [loadingTab, setLoadingTab] = useState<TabType | null>(null);
105
+
106
+ // Status hooks
107
+ const { hasUpdate, currentVersion, acknowledgeUpdate } = useUpdateCheck();
108
+ const { hasNewFeatures, unviewedFeatures, acknowledgeAllFeatures } = useFeatures();
109
+ const { hasUnreadNotifications, unreadNotifications, markAllAsRead } = useNotifications();
110
+ const { hasConnectionIssues, currentIssue, acknowledgeIssue } = useConnectionStatus();
111
+ const { hasActiveWarnings, activeIssues, acknowledgeAllIssues } = useDebugStatus();
112
+
113
+ const handleBack = () => {
114
+ if (showTabManagement) {
115
+ setShowTabManagement(false);
116
+ } else if (activeTab) {
117
+ setActiveTab(null);
118
+ }
119
+ };
120
+
121
+ // Only show tabs that are assigned to the developer window AND are visible
122
+ const visibleDeveloperTabs = tabConfiguration.developerTabs
123
+ .filter((tab: TabVisibilityConfig) => tab.window === 'developer' && tab.visible)
124
+ .sort((a: TabVisibilityConfig, b: TabVisibilityConfig) => (a.order || 0) - (b.order || 0));
125
+
126
+ const moveTab = (dragIndex: number, hoverIndex: number) => {
127
+ const draggedTab = visibleDeveloperTabs[dragIndex];
128
+ const targetTab = visibleDeveloperTabs[hoverIndex];
129
+
130
+ // Update the order of the dragged and target tabs
131
+ const updatedDraggedTab = { ...draggedTab, order: targetTab.order };
132
+ const updatedTargetTab = { ...targetTab, order: draggedTab.order };
133
+
134
+ // Update both tabs in the store
135
+ updateTabConfiguration(updatedDraggedTab);
136
+ updateTabConfiguration(updatedTargetTab);
137
+ };
138
+
139
+ const handleTabClick = async (tabId: TabType) => {
140
+ setLoadingTab(tabId);
141
+ setActiveTab(tabId);
142
+
143
+ // Acknowledge the status based on tab type
144
+ switch (tabId) {
145
+ case 'update':
146
+ await acknowledgeUpdate();
147
+ break;
148
+ case 'features':
149
+ await acknowledgeAllFeatures();
150
+ break;
151
+ case 'notifications':
152
+ await markAllAsRead();
153
+ break;
154
+ case 'connection':
155
+ acknowledgeIssue();
156
+ break;
157
+ case 'debug':
158
+ await acknowledgeAllIssues();
159
+ break;
160
+ }
161
+
162
+ // Simulate loading time (remove this in production)
163
+ await new Promise((resolve) => setTimeout(resolve, 1000));
164
+ setLoadingTab(null);
165
+ };
166
+
167
+ const getTabComponent = () => {
168
+ switch (activeTab) {
169
+ case 'profile':
170
+ return <ProfileTab />;
171
+ case 'settings':
172
+ return <SettingsTab />;
173
+ case 'notifications':
174
+ return <NotificationsTab />;
175
+ case 'features':
176
+ return <FeaturesTab />;
177
+ case 'data':
178
+ return <DataTab />;
179
+ case 'providers':
180
+ return <ProvidersTab />;
181
+ case 'connection':
182
+ return <ConnectionsTab />;
183
+ case 'debug':
184
+ return <DebugTab />;
185
+ case 'event-logs':
186
+ return <EventLogsTab />;
187
+ case 'update':
188
+ return <UpdateTab />;
189
+ default:
190
+ return null;
191
+ }
192
+ };
193
+
194
+ const getTabUpdateStatus = (tabId: TabType): boolean => {
195
+ switch (tabId) {
196
+ case 'update':
197
+ return hasUpdate;
198
+ case 'features':
199
+ return hasNewFeatures;
200
+ case 'notifications':
201
+ return hasUnreadNotifications;
202
+ case 'connection':
203
+ return hasConnectionIssues;
204
+ case 'debug':
205
+ return hasActiveWarnings;
206
+ default:
207
+ return false;
208
+ }
209
+ };
210
+
211
+ const getStatusMessage = (tabId: TabType): string => {
212
+ switch (tabId) {
213
+ case 'update':
214
+ return `New update available (v${currentVersion})`;
215
+ case 'features':
216
+ return `${unviewedFeatures.length} new feature${unviewedFeatures.length === 1 ? '' : 's'} to explore`;
217
+ case 'notifications':
218
+ return `${unreadNotifications.length} unread notification${unreadNotifications.length === 1 ? '' : 's'}`;
219
+ case 'connection':
220
+ return currentIssue === 'disconnected'
221
+ ? 'Connection lost'
222
+ : currentIssue === 'high-latency'
223
+ ? 'High latency detected'
224
+ : 'Connection issues detected';
225
+ case 'debug': {
226
+ const warnings = activeIssues.filter((i) => i.type === 'warning').length;
227
+ const errors = activeIssues.filter((i) => i.type === 'error').length;
228
+
229
+ return `${warnings} warning${warnings === 1 ? '' : 's'}, ${errors} error${errors === 1 ? '' : 's'}`;
230
+ }
231
+ default:
232
+ return '';
233
+ }
234
+ };
235
+
236
+ return (
237
+ <DndProvider backend={HTML5Backend}>
238
+ <RadixDialog.Root open={open}>
239
+ <RadixDialog.Portal>
240
+ <div className="fixed inset-0 flex items-center justify-center z-[60]">
241
+ <RadixDialog.Overlay asChild>
242
+ <motion.div
243
+ className="absolute inset-0 bg-black/50 backdrop-blur-sm"
244
+ initial={{ opacity: 0 }}
245
+ animate={{ opacity: 1 }}
246
+ exit={{ opacity: 0 }}
247
+ transition={{ duration: 0.2 }}
248
+ />
249
+ </RadixDialog.Overlay>
250
+ <RadixDialog.Content aria-describedby={undefined} asChild>
251
+ <motion.div
252
+ className={classNames(
253
+ 'relative',
254
+ 'w-[1200px] h-[90vh]',
255
+ 'bg-[#FAFAFA] dark:bg-[#0A0A0A]',
256
+ 'rounded-2xl shadow-2xl',
257
+ 'border border-[#E5E5E5] dark:border-[#1A1A1A]',
258
+ 'flex flex-col overflow-hidden',
259
+ 'z-[61]',
260
+ )}
261
+ initial={{ opacity: 0, scale: 0.95, y: 20 }}
262
+ animate={{ opacity: 1, scale: 1, y: 0 }}
263
+ exit={{ opacity: 0, scale: 0.95, y: 20 }}
264
+ transition={{ duration: 0.2 }}
265
+ >
266
+ {/* Header */}
267
+ <div className="flex-none flex items-center justify-between px-6 py-4 border-b border-[#E5E5E5] dark:border-[#1A1A1A]">
268
+ <div className="flex items-center gap-4">
269
+ {(activeTab || showTabManagement) && (
270
+ <motion.button
271
+ onClick={handleBack}
272
+ className={classNames(
273
+ 'flex items-center justify-center w-8 h-8 rounded-lg',
274
+ 'bg-[#F5F5F5] dark:bg-[#1A1A1A]',
275
+ 'hover:bg-purple-500/10 dark:hover:bg-purple-500/20',
276
+ 'group transition-all duration-200',
277
+ )}
278
+ whileHover={{ scale: 1.05 }}
279
+ whileTap={{ scale: 0.95 }}
280
+ >
281
+ <div className="i-ph:arrow-left w-4 h-4 text-bolt-elements-textSecondary group-hover:text-purple-500 transition-colors" />
282
+ </motion.button>
283
+ )}
284
+ <div className="flex items-center gap-3">
285
+ <motion.div
286
+ className="i-ph:code-fill w-5 h-5 text-purple-500"
287
+ initial={{ rotate: 0 }}
288
+ animate={{ rotate: 360 }}
289
+ transition={{
290
+ repeat: Infinity,
291
+ duration: 8,
292
+ ease: 'linear',
293
+ }}
294
+ />
295
+ <h2 className="text-lg font-medium text-bolt-elements-textPrimary">
296
+ {showTabManagement ? 'Tab Management' : activeTab ? 'Developer Tools' : 'Developer Dashboard'}
297
+ </h2>
298
+ </div>
299
+ </div>
300
+ <div className="flex items-center gap-3">
301
+ {!showTabManagement && !activeTab && (
302
+ <motion.button
303
+ onClick={() => setShowTabManagement(true)}
304
+ className={classNames(
305
+ 'px-3 py-1.5 rounded-lg text-sm',
306
+ 'bg-purple-500/10 text-purple-500',
307
+ 'hover:bg-purple-500/20',
308
+ 'transition-colors duration-200',
309
+ )}
310
+ whileHover={{ scale: 1.02 }}
311
+ whileTap={{ scale: 0.98 }}
312
+ >
313
+ Manage Tabs
314
+ </motion.button>
315
+ )}
316
+ <motion.button
317
+ onClick={onClose}
318
+ className={classNames(
319
+ 'flex items-center justify-center w-8 h-8 rounded-lg',
320
+ 'bg-[#F5F5F5] dark:bg-[#1A1A1A]',
321
+ 'hover:bg-purple-500/10 dark:hover:bg-purple-500/20',
322
+ 'group transition-all duration-200',
323
+ )}
324
+ whileHover={{ scale: 1.05 }}
325
+ whileTap={{ scale: 0.95 }}
326
+ >
327
+ <div className="i-ph:x w-4 h-4 text-bolt-elements-textSecondary group-hover:text-purple-500 transition-colors" />
328
+ </motion.button>
329
+ </div>
330
+ </div>
331
+
332
+ {/* Content */}
333
+ <div
334
+ className={classNames(
335
+ 'flex-1',
336
+ 'overflow-y-auto',
337
+ 'hover:overflow-y-auto',
338
+ 'scrollbar scrollbar-w-2',
339
+ 'scrollbar-track-transparent',
340
+ 'scrollbar-thumb-[#E5E5E5] hover:scrollbar-thumb-[#CCCCCC]',
341
+ 'dark:scrollbar-thumb-[#333333] dark:hover:scrollbar-thumb-[#444444]',
342
+ 'will-change-scroll',
343
+ 'touch-auto',
344
+ )}
345
+ >
346
+ <motion.div initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} className="p-6">
347
+ {showTabManagement ? (
348
+ <TabManagement />
349
+ ) : activeTab ? (
350
+ getTabComponent()
351
+ ) : (
352
+ <div className="grid grid-cols-4 gap-4">
353
+ {visibleDeveloperTabs.map((tab: TabVisibilityConfig, index: number) => (
354
+ <DraggableTabTile
355
+ key={tab.id}
356
+ tab={tab}
357
+ index={index}
358
+ moveTab={moveTab}
359
+ onClick={() => handleTabClick(tab.id)}
360
+ isActive={activeTab === tab.id}
361
+ hasUpdate={getTabUpdateStatus(tab.id)}
362
+ statusMessage={getStatusMessage(tab.id)}
363
+ description={TAB_DESCRIPTIONS[tab.id]}
364
+ isLoading={loadingTab === tab.id}
365
+ />
366
+ ))}
367
+ </div>
368
+ )}
369
+ </motion.div>
370
+ </div>
371
+ </motion.div>
372
+ </RadixDialog.Content>
373
+ </div>
374
+ </RadixDialog.Portal>
375
+ </RadixDialog.Root>
376
+ </DndProvider>
377
+ );
378
+ };
app/components/settings/developer/TabManagement.tsx ADDED
@@ -0,0 +1,315 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { motion } from 'framer-motion';
2
+ import { useState } from 'react';
3
+ import { classNames } from '~/utils/classNames';
4
+ import { tabConfigurationStore, updateTabConfiguration, resetTabConfiguration } from '~/lib/stores/settings';
5
+ import { useStore } from '@nanostores/react';
6
+ import { TAB_LABELS, type TabType, type TabVisibilityConfig } from '~/components/settings/settings.types';
7
+ import { toast } from 'react-toastify';
8
+
9
+ // Define icons for each tab type
10
+ const TAB_ICONS: Record<TabType, string> = {
11
+ profile: 'i-ph:user-circle-fill',
12
+ settings: 'i-ph:gear-six-fill',
13
+ notifications: 'i-ph:bell-fill',
14
+ features: 'i-ph:sparkle-fill',
15
+ data: 'i-ph:database-fill',
16
+ providers: 'i-ph:robot-fill',
17
+ connection: 'i-ph:plug-fill',
18
+ debug: 'i-ph:bug-fill',
19
+ 'event-logs': 'i-ph:list-bullets-fill',
20
+ update: 'i-ph:arrow-clockwise-fill',
21
+ };
22
+
23
+ interface TabGroupProps {
24
+ title: string;
25
+ description?: string;
26
+ tabs: TabVisibilityConfig[];
27
+ onVisibilityChange: (tabId: TabType, enabled: boolean) => void;
28
+ targetWindow: 'user' | 'developer';
29
+ standardTabs: TabType[];
30
+ }
31
+
32
+ const TabGroup = ({ title, description, tabs, onVisibilityChange, targetWindow }: TabGroupProps) => {
33
+ // Split tabs into visible and hidden
34
+ const visibleTabs = tabs.filter((tab) => tab.visible).sort((a, b) => (a.order || 0) - (b.order || 0));
35
+ const hiddenTabs = tabs.filter((tab) => !tab.visible).sort((a, b) => (a.order || 0) - (b.order || 0));
36
+
37
+ return (
38
+ <div className="mb-8 rounded-xl bg-white/5 p-6 backdrop-blur-sm dark:bg-gray-800/30">
39
+ <div className="mb-6">
40
+ <h3 className="flex items-center gap-2 text-lg font-medium text-gray-900 dark:text-white">
41
+ <span className="i-ph:layout-fill h-5 w-5 text-purple-500" />
42
+ {title}
43
+ </h3>
44
+ {description && <p className="mt-1.5 text-sm text-gray-600 dark:text-gray-400">{description}</p>}
45
+ </div>
46
+
47
+ <div className="space-y-6">
48
+ <motion.div layout className="space-y-2">
49
+ {visibleTabs.map((tab) => (
50
+ <motion.div
51
+ key={tab.id}
52
+ layout
53
+ initial={{ opacity: 0, y: 20 }}
54
+ animate={{ opacity: 1, y: 0 }}
55
+ exit={{ opacity: 0, y: -20 }}
56
+ transition={{ duration: 0.2 }}
57
+ className="group relative flex items-center justify-between rounded-lg border border-gray-200 bg-white px-4 py-3 shadow-sm transition-all hover:border-purple-200 hover:shadow-md dark:border-gray-700 dark:bg-gray-800 dark:hover:border-purple-500/30"
58
+ >
59
+ <div className="flex items-center space-x-3">
60
+ <div
61
+ className={classNames(
62
+ TAB_ICONS[tab.id],
63
+ 'h-5 w-5 transition-colors',
64
+ tab.id === 'profile'
65
+ ? 'text-purple-500 dark:text-purple-400'
66
+ : 'text-gray-500 group-hover:text-purple-500 dark:text-gray-400 dark:group-hover:text-purple-400',
67
+ )}
68
+ />
69
+ <span
70
+ className={classNames(
71
+ 'text-sm font-medium transition-colors',
72
+ tab.id === 'profile'
73
+ ? 'text-gray-900 dark:text-white'
74
+ : 'text-gray-700 group-hover:text-gray-900 dark:text-gray-300 dark:group-hover:text-white',
75
+ )}
76
+ >
77
+ {TAB_LABELS[tab.id]}
78
+ </span>
79
+ {tab.id === 'profile' && targetWindow === 'user' && (
80
+ <span className="rounded-full bg-purple-50 px-2 py-0.5 text-xs font-medium text-purple-600 dark:bg-purple-500/10 dark:text-purple-400">
81
+ Standard
82
+ </span>
83
+ )}
84
+ </div>
85
+ <div className="flex items-center space-x-4">
86
+ {targetWindow === 'user' ? (
87
+ <label className="relative inline-flex cursor-pointer items-center">
88
+ <input
89
+ type="checkbox"
90
+ checked={tab.visible}
91
+ onChange={(e) => onVisibilityChange(tab.id, e.target.checked)}
92
+ className="peer sr-only"
93
+ />
94
+ <div
95
+ className={classNames(
96
+ 'h-6 w-11 rounded-full bg-gray-200 transition-colors dark:bg-gray-700',
97
+ 'after:absolute after:left-[2px] after:top-[2px]',
98
+ 'after:h-5 after:w-5 after:rounded-full after:bg-white after:shadow-sm',
99
+ 'after:transition-all after:content-[""]',
100
+ 'peer-checked:bg-purple-500 peer-checked:after:translate-x-full',
101
+ 'peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-purple-500/20',
102
+ )}
103
+ />
104
+ </label>
105
+ ) : (
106
+ <div className="text-sm text-gray-500 dark:text-gray-400">Always visible</div>
107
+ )}
108
+ </div>
109
+ </motion.div>
110
+ ))}
111
+ </motion.div>
112
+
113
+ {hiddenTabs.length > 0 && (
114
+ <motion.div layout className="space-y-2">
115
+ <div className="flex items-center gap-2 text-sm font-medium text-gray-500 dark:text-gray-400">
116
+ <span className="i-ph:eye-slash-fill h-4 w-4" />
117
+ Hidden Tabs
118
+ </div>
119
+ {hiddenTabs.map((tab) => (
120
+ <motion.div
121
+ key={tab.id}
122
+ layout
123
+ initial={{ opacity: 0, y: 20 }}
124
+ animate={{ opacity: 1, y: 0 }}
125
+ exit={{ opacity: 0, y: -20 }}
126
+ transition={{ duration: 0.2 }}
127
+ className="group relative flex items-center justify-between rounded-lg border border-gray-200 bg-white/50 px-4 py-3 transition-all hover:border-purple-200 dark:border-gray-700 dark:bg-gray-800/50 dark:hover:border-purple-500/30"
128
+ >
129
+ <div className="flex items-center space-x-3">
130
+ <div
131
+ className={classNames(
132
+ TAB_ICONS[tab.id],
133
+ 'h-5 w-5 transition-colors',
134
+ 'text-gray-400 group-hover:text-purple-500 dark:text-gray-500 dark:group-hover:text-purple-400',
135
+ )}
136
+ />
137
+ <span className="text-sm font-medium text-gray-500 transition-colors group-hover:text-gray-900 dark:text-gray-400 dark:group-hover:text-white">
138
+ {TAB_LABELS[tab.id]}
139
+ </span>
140
+ </div>
141
+ <div className="flex items-center space-x-4">
142
+ {targetWindow === 'user' && (
143
+ <label className="relative inline-flex cursor-pointer items-center">
144
+ <input
145
+ type="checkbox"
146
+ checked={tab.visible}
147
+ onChange={(e) => onVisibilityChange(tab.id, e.target.checked)}
148
+ className="peer sr-only"
149
+ />
150
+ <div
151
+ className={classNames(
152
+ 'h-6 w-11 rounded-full bg-gray-200 transition-colors dark:bg-gray-700',
153
+ 'after:absolute after:left-[2px] after:top-[2px]',
154
+ 'after:h-5 after:w-5 after:rounded-full after:bg-white after:shadow-sm',
155
+ 'after:transition-all after:content-[""]',
156
+ 'peer-checked:bg-purple-500 peer-checked:after:translate-x-full',
157
+ 'peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-purple-500/20',
158
+ )}
159
+ />
160
+ </label>
161
+ )}
162
+ </div>
163
+ </motion.div>
164
+ ))}
165
+ </motion.div>
166
+ )}
167
+ </div>
168
+ </div>
169
+ );
170
+ };
171
+
172
+ export const TabManagement = () => {
173
+ const config = useStore(tabConfigurationStore);
174
+ const [searchQuery, setSearchQuery] = useState('');
175
+
176
+ // Define standard (visible by default) tabs for each window
177
+ const standardUserTabs: TabType[] = ['features', 'data', 'providers', 'connection', 'debug'];
178
+ const standardDeveloperTabs: TabType[] = [
179
+ 'profile',
180
+ 'settings',
181
+ 'notifications',
182
+ 'features',
183
+ 'data',
184
+ 'providers',
185
+ 'connection',
186
+ 'debug',
187
+ 'event-logs',
188
+ 'update',
189
+ ];
190
+
191
+ const handleVisibilityChange = (tabId: TabType, enabled: boolean, targetWindow: 'user' | 'developer') => {
192
+ const tabs = targetWindow === 'user' ? config.userTabs : config.developerTabs;
193
+ const existingTab = tabs.find((tab) => tab.id === tabId);
194
+
195
+ const updatedTab: TabVisibilityConfig = existingTab
196
+ ? {
197
+ ...existingTab,
198
+ visible: enabled,
199
+ }
200
+ : {
201
+ id: tabId,
202
+ visible: enabled,
203
+ window: targetWindow,
204
+ order: tabs.length,
205
+ };
206
+
207
+ // Update the store
208
+ updateTabConfiguration(updatedTab);
209
+
210
+ // Show toast notification
211
+ toast.success(`${TAB_LABELS[tabId]} ${enabled ? 'enabled' : 'disabled'} in ${targetWindow} window`);
212
+ };
213
+
214
+ const handleResetToDefaults = () => {
215
+ resetTabConfiguration();
216
+ toast.success('Tab settings reset to defaults');
217
+ };
218
+
219
+ // Filter tabs based on search and window
220
+ const userTabs = config.userTabs.filter((tab) =>
221
+ TAB_LABELS[tab.id].toLowerCase().includes(searchQuery.toLowerCase()),
222
+ );
223
+
224
+ const developerTabs = config.developerTabs.filter((tab) =>
225
+ TAB_LABELS[tab.id].toLowerCase().includes(searchQuery.toLowerCase()),
226
+ );
227
+
228
+ return (
229
+ <div className="h-full overflow-y-auto px-6 py-6">
230
+ <div className="mb-8">
231
+ <div className="flex items-center justify-between">
232
+ <div>
233
+ <h2 className="flex items-center gap-2 text-xl font-semibold text-gray-900 dark:text-white">
234
+ <span className="i-ph:squares-four-fill h-6 w-6 text-purple-500" />
235
+ Tab Management
236
+ </h2>
237
+ <p className="mt-1.5 text-sm text-gray-600 dark:text-gray-400">
238
+ Configure which tabs are visible in the user and developer windows
239
+ </p>
240
+ </div>
241
+ <button
242
+ onClick={handleResetToDefaults}
243
+ className="inline-flex items-center gap-1.5 rounded-lg bg-purple-50 px-4 py-2 text-sm font-medium text-purple-600 transition-colors hover:bg-purple-100 focus:outline-none focus:ring-4 focus:ring-purple-500/20 dark:bg-purple-500/10 dark:text-purple-400 dark:hover:bg-purple-500/20"
244
+ >
245
+ <span className="i-ph:arrow-counter-clockwise-fill h-4 w-4" />
246
+ Reset to Defaults
247
+ </button>
248
+ </div>
249
+
250
+ <div className="mt-6 flex items-center gap-4">
251
+ <div className="relative flex-1">
252
+ <div className="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3">
253
+ <span className="i-ph:magnifying-glass h-5 w-5 text-gray-400" />
254
+ </div>
255
+ <input
256
+ type="text"
257
+ value={searchQuery}
258
+ onChange={(e) => setSearchQuery(e.target.value)}
259
+ placeholder="Search tabs..."
260
+ className="block w-full rounded-lg border border-gray-200 bg-white py-2.5 pl-10 pr-4 text-sm text-gray-900 placeholder:text-gray-500 focus:border-purple-500 focus:outline-none focus:ring-4 focus:ring-purple-500/20 dark:border-gray-700 dark:bg-gray-800 dark:text-white dark:placeholder:text-gray-400 dark:focus:border-purple-400"
261
+ />
262
+ </div>
263
+ </div>
264
+ </div>
265
+
266
+ <div className="space-y-8">
267
+ {/* User Window Section */}
268
+ <div className="rounded-xl border border-purple-100 bg-purple-50/50 p-1 dark:border-purple-500/10 dark:bg-purple-500/5">
269
+ <div className="rounded-lg bg-white p-6 dark:bg-gray-800">
270
+ <div className="mb-6 flex items-center gap-3">
271
+ <div className="rounded-lg bg-purple-100 p-2 dark:bg-purple-500/10">
272
+ <span className="i-ph:user-circle-fill h-5 w-5 text-purple-500 dark:text-purple-400" />
273
+ </div>
274
+ <div>
275
+ <h3 className="text-base font-medium text-gray-900 dark:text-white">User Window</h3>
276
+ <p className="text-sm text-gray-600 dark:text-gray-400">Configure tabs visible to regular users</p>
277
+ </div>
278
+ </div>
279
+ <TabGroup
280
+ title="User Interface"
281
+ description="Manage which tabs are visible in the user window"
282
+ tabs={userTabs}
283
+ onVisibilityChange={(tabId, enabled) => handleVisibilityChange(tabId, enabled, 'user')}
284
+ targetWindow="user"
285
+ standardTabs={standardUserTabs}
286
+ />
287
+ </div>
288
+ </div>
289
+
290
+ {/* Developer Window Section */}
291
+ <div className="rounded-xl border border-blue-100 bg-blue-50/50 p-1 dark:border-blue-500/10 dark:bg-blue-500/5">
292
+ <div className="rounded-lg bg-white p-6 dark:bg-gray-800">
293
+ <div className="mb-6 flex items-center gap-3">
294
+ <div className="rounded-lg bg-blue-100 p-2 dark:bg-blue-500/10">
295
+ <span className="i-ph:code-fill h-5 w-5 text-blue-500 dark:text-blue-400" />
296
+ </div>
297
+ <div>
298
+ <h3 className="text-base font-medium text-gray-900 dark:text-white">Developer Window</h3>
299
+ <p className="text-sm text-gray-600 dark:text-gray-400">Configure tabs visible to developers</p>
300
+ </div>
301
+ </div>
302
+ <TabGroup
303
+ title="Developer Interface"
304
+ description="Manage which tabs are visible in the developer window"
305
+ tabs={developerTabs}
306
+ onVisibilityChange={(tabId, enabled) => handleVisibilityChange(tabId, enabled, 'developer')}
307
+ targetWindow="developer"
308
+ standardTabs={standardDeveloperTabs}
309
+ />
310
+ </div>
311
+ </div>
312
+ </div>
313
+ </div>
314
+ );
315
+ };
app/components/settings/event-logs/EventLogsTab.tsx CHANGED
@@ -1,151 +1,304 @@
1
  import React, { useCallback, useEffect, useState, useMemo, useRef } from 'react';
2
- import { useSettings } from '~/lib/hooks/useSettings';
3
  import { toast } from 'react-toastify';
4
  import { Switch } from '~/components/ui/Switch';
5
  import { logStore, type LogEntry } from '~/lib/stores/logs';
6
  import { useStore } from '@nanostores/react';
7
  import { classNames } from '~/utils/classNames';
8
  import { motion } from 'framer-motion';
9
- import { settingsStyles } from '~/components/settings/settings.styles';
10
 
11
- export default function EventLogsTab() {
12
- const {} = useSettings();
13
- const showLogs = useStore(logStore.showLogs);
14
- const logs = useStore(logStore.logs);
15
- const [logLevel, setLogLevel] = useState<LogEntry['level'] | 'all'>('info');
16
- const [autoScroll, setAutoScroll] = useState(true);
17
- const [searchQuery, setSearchQuery] = useState('');
18
- const [, forceUpdate] = useState({});
19
- const logsContainerRef = useRef<HTMLDivElement>(null);
20
- const [isScrolledToBottom, setIsScrolledToBottom] = useState(true);
21
 
22
- const filteredLogs = useMemo(() => {
23
- const allLogs = Object.values(logs);
24
- const filtered = allLogs.filter((log) => {
25
- const matchesLevel = !logLevel || log.level === logLevel || logLevel === 'all';
26
- const matchesSearch =
27
- !searchQuery ||
28
- log.message?.toLowerCase().includes(searchQuery.toLowerCase()) ||
29
- JSON.stringify(log.details)?.toLowerCase()?.includes(searchQuery?.toLowerCase());
30
 
31
- return matchesLevel && matchesSearch;
32
- });
 
 
 
 
 
33
 
34
- return filtered.reverse();
35
- }, [logs, logLevel, searchQuery]);
 
 
 
 
 
 
 
 
 
 
 
36
 
37
- // Effect to initialize showLogs
38
- useEffect(() => {
39
- logStore.showLogs.set(true);
40
- }, []);
41
-
42
- useEffect(() => {
43
- // System info logs
44
- logStore.logSystem('Application initialized', {
45
- version: process.env.NEXT_PUBLIC_APP_VERSION,
46
- environment: process.env.NODE_ENV,
47
- timestamp: new Date().toISOString(),
48
- userAgent: navigator.userAgent,
49
- });
 
 
 
 
 
 
 
50
 
51
- // Debug logs for system state
52
- logStore.logDebug('System configuration loaded', {
53
- runtime: 'Next.js',
54
- features: ['AI Chat', 'Event Logging', 'Provider Management', 'Theme Support'],
55
- locale: navigator.language,
56
- timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone,
57
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
58
 
59
- // Performance metrics
60
- logStore.logSystem('Performance metrics', {
61
- deviceMemory: (navigator as any).deviceMemory || 'unknown',
62
- hardwareConcurrency: navigator.hardwareConcurrency,
63
- connectionType: (navigator as any).connection?.effectiveType || 'unknown',
64
- });
 
 
 
 
 
65
 
66
- // Provider status
67
- logStore.logProvider('Provider status check', {
68
- availableProviders: ['OpenAI', 'Anthropic', 'Mistral', 'Ollama'],
69
- defaultProvider: 'OpenAI',
70
- status: 'operational',
71
- });
72
 
73
- // Theme and accessibility
74
- logStore.logSystem('User preferences loaded', {
75
- theme: document.documentElement.dataset.theme || 'system',
76
- prefersReducedMotion: window.matchMedia('(prefers-reduced-motion: reduce)').matches,
77
- prefersDarkMode: window.matchMedia('(prefers-color-scheme: dark)').matches,
78
- });
79
 
80
- // Warning logs for potential issues
81
- logStore.logWarning('Resource usage threshold approaching', {
82
- memoryUsage: '75%',
83
- cpuLoad: '60%',
84
- timestamp: new Date().toISOString(),
85
  });
 
86
 
87
- // Security checks
88
- logStore.logSystem('Security status', {
89
- httpsEnabled: window.location.protocol === 'https:',
90
- cookiesEnabled: navigator.cookieEnabled,
91
- storageQuota: 'checking...',
92
- });
93
 
94
- // Error logs with detailed context
95
- logStore.logError('API connection failed', new Error('Connection timeout'), {
96
- endpoint: '/api/chat',
97
- retryCount: 3,
98
- lastAttempt: new Date().toISOString(),
99
- statusCode: 408,
100
  });
101
 
102
- // Debug logs for development
103
- if (process.env.NODE_ENV === 'development') {
104
- logStore.logDebug('Development mode active', {
105
- debugFlags: true,
106
- mockServices: false,
107
- apiEndpoint: 'local',
 
 
 
 
 
 
 
 
 
108
  });
 
 
 
 
109
  }
110
- }, []);
111
 
112
- // Scroll handling
113
- useEffect(() => {
114
- const container = logsContainerRef.current;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
115
 
116
- if (!container) {
117
- return undefined;
118
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
119
 
120
- const handleScroll = () => {
121
- const { scrollTop, scrollHeight, clientHeight } = container;
122
- const isBottom = Math.abs(scrollHeight - clientHeight - scrollTop) < 10;
123
- setIsScrolledToBottom(isBottom);
124
- };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
125
 
126
- container.addEventListener('scroll', handleScroll);
 
 
 
 
 
 
 
 
 
 
127
 
128
- const cleanup = () => {
129
- container.removeEventListener('scroll', handleScroll);
130
- };
131
 
132
- return cleanup;
 
 
 
 
 
 
 
 
 
133
  }, []);
134
 
135
- // Auto-scroll effect
136
- useEffect(() => {
137
- const container = logsContainerRef.current;
 
 
 
 
 
 
138
 
139
- if (container && (autoScroll || isScrolledToBottom)) {
140
- container.scrollTop = 0;
141
- }
142
- }, [filteredLogs, autoScroll, isScrolledToBottom]);
 
143
 
144
  const handleClearLogs = useCallback(() => {
145
  if (confirm('Are you sure you want to clear all logs?')) {
146
  logStore.clearLogs();
147
  toast.success('Logs cleared successfully');
148
- forceUpdate({}); // Force a re-render after clearing logs
149
  }
150
  }, []);
151
 
@@ -177,223 +330,188 @@ export default function EventLogsTab() {
177
  }
178
  }, []);
179
 
180
- const getLevelIcon = (level: LogEntry['level']): string => {
181
- switch (level) {
182
- case 'info':
183
- return 'i-ph:info';
184
- case 'warning':
185
- return 'i-ph:warning';
186
- case 'error':
187
- return 'i-ph:x-circle';
188
- case 'debug':
189
- return 'i-ph:bug';
190
- default:
191
- return 'i-ph:circle';
192
  }
 
 
 
 
193
  };
194
 
195
- const getLevelColor = (level: LogEntry['level']) => {
196
- switch (level) {
197
- case 'info':
198
- return 'text-[#1389FD] dark:text-[#1389FD]';
199
- case 'warning':
200
- return 'text-[#FFDB6C] dark:text-[#FFDB6C]';
201
- case 'error':
202
- return 'text-[#EE4744] dark:text-[#EE4744]';
203
- case 'debug':
204
- return 'text-[#77828D] dark:text-[#77828D]';
205
- default:
206
- return 'text-bolt-elements-textPrimary';
207
  }
208
- };
209
 
210
  return (
211
- <div className="space-y-4">
212
- <div className="flex flex-col space-y-4">
213
- {/* Title and Toggles Row */}
214
- <div className="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4">
215
- <div className="flex items-center gap-2">
216
- <div className="i-ph:list-bullets text-xl text-purple-500" />
 
217
  <div>
218
- <h3 className="text-lg font-medium text-bolt-elements-textPrimary">Event Logs</h3>
219
  <p className="text-sm text-bolt-elements-textSecondary">Track system events and debug information</p>
220
  </div>
221
  </div>
222
- <div className="flex flex-wrap items-center gap-4">
223
- <div className="flex items-center gap-2">
224
- <div className="i-ph:eye text-bolt-elements-textSecondary" />
225
- <span className="text-sm text-bolt-elements-textSecondary whitespace-nowrap">Show Actions</span>
226
- <Switch checked={showLogs} onCheckedChange={(checked) => logStore.showLogs.set(checked)} />
 
 
 
 
 
 
 
 
 
 
 
 
 
227
  </div>
228
- <div className="flex items-center gap-2">
229
- <div className="i-ph:arrow-clockwise text-bolt-elements-textSecondary" />
230
- <span className="text-sm text-bolt-elements-textSecondary whitespace-nowrap">Auto-scroll</span>
231
- <Switch checked={autoScroll} onCheckedChange={setAutoScroll} />
232
- </div>
233
- </div>
234
  </div>
235
 
236
- {/* Controls Row */}
237
- <div className="flex flex-wrap items-center gap-4">
238
- <div className="flex-1 min-w-[150px] max-w-[200px]">
239
- <div className="relative group">
240
- <select
241
- value={logLevel}
242
- onChange={(e) => setLogLevel(e.target.value as LogEntry['level'])}
243
- className={classNames(
244
- 'w-full pl-9 pr-3 py-2 rounded-lg',
245
- 'bg-bolt-elements-background-depth-2 border border-bolt-elements-borderColor',
246
- 'text-sm text-bolt-elements-textPrimary',
247
- 'focus:outline-none focus:ring-2 focus:ring-purple-500/30',
248
- 'group-hover:border-purple-500/30',
249
- 'transition-all duration-200',
250
- )}
251
- >
252
- <option value="all">All Levels</option>
253
- <option value="info">Info</option>
254
- <option value="warning">Warning</option>
255
- <option value="error">Error</option>
256
- <option value="debug">Debug</option>
257
- </select>
258
- <div className="i-ph:funnel absolute left-3 top-1/2 -translate-y-1/2 text-bolt-elements-textSecondary group-hover:text-purple-500 transition-colors" />
259
- </div>
260
- </div>
261
- <div className="flex-1 min-w-[200px]">
262
- <div className="relative group">
263
- <input
264
- type="text"
265
- placeholder="Search logs..."
266
- value={searchQuery}
267
- onChange={(e) => setSearchQuery(e.target.value)}
268
- className={classNames(
269
- 'w-full pl-9 pr-3 py-2 rounded-lg',
270
- 'bg-bolt-elements-background-depth-2 border border-bolt-elements-borderColor',
271
- 'text-sm text-bolt-elements-textPrimary placeholder-bolt-elements-textTertiary',
272
- 'focus:outline-none focus:ring-2 focus:ring-purple-500/30',
273
- 'group-hover:border-purple-500/30',
274
- 'transition-all duration-200',
275
- )}
276
  />
277
- <div className="i-ph:magnifying-glass absolute left-3 top-1/2 -translate-y-1/2 text-bolt-elements-textSecondary group-hover:text-purple-500 transition-colors" />
278
- </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
279
  </div>
280
- {showLogs && (
281
- <div className="flex items-center gap-2 flex-nowrap">
282
- <motion.button
283
- onClick={handleExportLogs}
284
- className={classNames(settingsStyles.button.base, settingsStyles.button.primary, 'group')}
285
- whileHover={{ scale: 1.02 }}
286
- whileTap={{ scale: 0.98 }}
287
- >
288
- <div className="i-ph:download-simple group-hover:scale-110 transition-transform" />
289
- Export Logs
290
- </motion.button>
291
- <motion.button
292
- onClick={handleClearLogs}
293
- className={classNames(settingsStyles.button.base, settingsStyles.button.danger, 'group')}
294
- whileHover={{ scale: 1.02 }}
295
- whileTap={{ scale: 0.98 }}
296
- >
297
- <div className="i-ph:trash group-hover:scale-110 transition-transform" />
298
- Clear Logs
299
- </motion.button>
300
- </div>
301
- )}
302
  </div>
303
  </div>
304
 
305
- <motion.div
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
306
  ref={logsContainerRef}
307
- className={classNames(
308
- settingsStyles.card,
309
- 'h-[calc(100vh-250px)] min-h-[400px] overflow-y-auto logs-container',
310
- 'scrollbar-thin scrollbar-thumb-bolt-elements-borderColor scrollbar-track-transparent hover:scrollbar-thumb-purple-500/30',
311
- )}
312
- initial={{ opacity: 0, y: 20 }}
313
- animate={{ opacity: 1, y: 0 }}
314
  >
315
- {filteredLogs.length === 0 ? (
316
- <div className="flex flex-col items-center justify-center h-full text-center p-8">
317
- <motion.div
318
- initial={{ scale: 0.8, opacity: 0 }}
319
- animate={{ scale: 1, opacity: 1 }}
320
- transition={{ type: 'spring', duration: 0.5 }}
321
- className="i-ph:clipboard-text text-6xl text-bolt-elements-textSecondary mb-4"
322
- />
323
- <motion.p
324
- initial={{ y: 10, opacity: 0 }}
325
- animate={{ y: 0, opacity: 1 }}
326
- transition={{ delay: 0.2 }}
327
- className="text-bolt-elements-textSecondary"
328
- >
329
- No logs found
330
- </motion.p>
331
- </div>
332
- ) : (
333
- <div className="divide-y divide-bolt-elements-borderColor">
334
- {filteredLogs.map((log, index) => (
335
- <motion.div
336
- key={index}
337
- className={classNames(
338
- 'p-4 font-mono hover:bg-bolt-elements-background-depth-3 transition-colors duration-200',
339
- { 'border-t border-bolt-elements-borderColor': index === 0 },
340
- )}
341
- initial={{ opacity: 0, y: 20 }}
342
- animate={{ opacity: 1, y: 0 }}
343
- transition={{ delay: index * 0.03 }}
344
- >
345
- <div className="flex items-start gap-3">
346
- <div
347
- className={classNames(
348
- getLevelIcon(log.level),
349
- getLevelColor(log.level),
350
- 'mt-1 flex-shrink-0 text-lg',
351
- )}
352
- />
353
- <div className="flex-1 min-w-0">
354
- <div className="flex items-center gap-2 flex-wrap">
355
- <span
356
- className={classNames(
357
- 'font-bold whitespace-nowrap px-2 py-0.5 rounded-full text-xs',
358
- {
359
- 'bg-blue-500/10': log.level === 'info',
360
- 'bg-yellow-500/10': log.level === 'warning',
361
- 'bg-red-500/10': log.level === 'error',
362
- 'bg-bolt-elements-textSecondary/10': log.level === 'debug',
363
- },
364
- getLevelColor(log.level),
365
- )}
366
- >
367
- {log.level.toUpperCase()}
368
- </span>
369
- <span className="text-bolt-elements-textSecondary whitespace-nowrap text-xs">
370
- {new Date(log.timestamp).toLocaleString()}
371
- </span>
372
- <span className="text-bolt-elements-textPrimary break-all">{log.message}</span>
373
- </div>
374
- {log.details && (
375
- <motion.pre
376
- initial={{ opacity: 0, height: 0 }}
377
- animate={{ opacity: 1, height: 'auto' }}
378
- transition={{ duration: 0.2 }}
379
- className={classNames(
380
- 'mt-2 text-xs',
381
- 'overflow-x-auto whitespace-pre-wrap break-all',
382
- 'bg-[#1A1A1A] dark:bg-[#0A0A0A] rounded-md p-3',
383
- 'border border-[#333333] dark:border-[#1A1A1A]',
384
- 'text-[#666666] dark:text-[#999999]',
385
- )}
386
- >
387
- {JSON.stringify(log.details, null, 2)}
388
- </motion.pre>
389
- )}
390
- </div>
391
- </div>
392
- </motion.div>
393
- ))}
394
- </div>
395
- )}
396
- </motion.div>
397
  </div>
398
  );
399
  }
 
1
  import React, { useCallback, useEffect, useState, useMemo, useRef } from 'react';
 
2
  import { toast } from 'react-toastify';
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 { motion } from 'framer-motion';
 
8
 
9
+ interface SelectOption {
10
+ value: string;
11
+ label: string;
12
+ icon: string;
13
+ color?: string;
14
+ }
 
 
 
 
15
 
16
+ const logLevelOptions: SelectOption[] = [
17
+ { value: 'all', label: 'All Levels', icon: 'i-ph:funnel' },
18
+ { value: 'info', label: 'Info', icon: 'i-ph:info', color: 'text-blue-500' },
19
+ { value: 'warning', label: 'Warning', icon: 'i-ph:warning', color: 'text-yellow-500' },
20
+ { value: 'error', label: 'Error', icon: 'i-ph:x-circle', color: 'text-red-500' },
21
+ { value: 'debug', label: 'Debug', icon: 'i-ph:bug', color: 'text-gray-500' },
22
+ ];
 
23
 
24
+ const logCategoryOptions: SelectOption[] = [
25
+ { value: 'all', label: 'All Categories', icon: 'i-ph:squares-four' },
26
+ { value: 'system', label: 'System', icon: 'i-ph:desktop' },
27
+ { value: 'provider', label: 'Provider', icon: 'i-ph:plug' },
28
+ { value: 'user', label: 'User', icon: 'i-ph:user' },
29
+ { value: 'error', label: 'Error', icon: 'i-ph:warning-octagon' },
30
+ ];
31
 
32
+ const SegmentedGroup = ({
33
+ value,
34
+ onChange,
35
+ options,
36
+ className,
37
+ }: {
38
+ value: string;
39
+ onChange: (value: string) => void;
40
+ options: SelectOption[];
41
+ className?: string;
42
+ }) => {
43
+ const [isExpanded, setIsExpanded] = useState(false);
44
+ const selectedOption = options.find((opt) => opt.value === value);
45
 
46
+ if (!isExpanded) {
47
+ return (
48
+ <button
49
+ type="button"
50
+ onClick={() => setIsExpanded(true)}
51
+ className={classNames(
52
+ 'flex items-center gap-2 px-3 py-1.5 rounded-lg text-sm',
53
+ 'bg-white/50 dark:bg-gray-800/30',
54
+ 'hover:bg-gray-50 dark:hover:bg-gray-800/50',
55
+ 'border border-gray-200/50 dark:border-gray-700/50',
56
+ 'text-bolt-elements-textPrimary',
57
+ className,
58
+ )}
59
+ >
60
+ <div className={classNames(selectedOption?.icon, 'text-base text-purple-500')} />
61
+ <span className="text-sm">{selectedOption?.label}</span>
62
+ <div className="i-ph:caret-right text-sm text-bolt-elements-textTertiary" />
63
+ </button>
64
+ );
65
+ }
66
 
67
+ return (
68
+ <div className="flex items-center gap-0.5 p-0.5 rounded-lg bg-white/50 dark:bg-gray-800/30 border border-gray-200/50 dark:border-gray-700/50">
69
+ {options.map((option) => (
70
+ <button
71
+ key={option.value}
72
+ type="button"
73
+ onClick={() => {
74
+ onChange(option.value);
75
+ setIsExpanded(false);
76
+ }}
77
+ className={classNames(
78
+ 'flex items-center gap-2 px-3 py-1.5 rounded-md text-sm transition-colors',
79
+ option.value === value
80
+ ? 'bg-purple-100 dark:bg-purple-800/40 text-purple-900 dark:text-purple-200'
81
+ : 'text-bolt-elements-textSecondary hover:bg-gray-50 dark:hover:bg-gray-800/50',
82
+ )}
83
+ >
84
+ <div className={classNames(option.icon, 'text-base', option.value === value ? option.color : '')} />
85
+ <span className="truncate">{option.label}</span>
86
+ </button>
87
+ ))}
88
+ </div>
89
+ );
90
+ };
91
 
92
+ const LogEntryItem = ({
93
+ log,
94
+ isExpanded: forceExpanded,
95
+ use24Hour,
96
+ }: {
97
+ log: LogEntry;
98
+ isExpanded: boolean;
99
+ use24Hour: boolean;
100
+ }) => {
101
+ const [isExpanded, setIsExpanded] = useState(forceExpanded);
102
+ const [isCopied, setIsCopied] = useState(false);
103
 
104
+ useEffect(() => {
105
+ setIsExpanded(forceExpanded);
106
+ }, [forceExpanded]);
 
 
 
107
 
108
+ const handleCopy = useCallback(() => {
109
+ const logText = `[${log.level.toUpperCase()}] ${log.message}\nTimestamp: ${new Date(
110
+ log.timestamp,
111
+ ).toLocaleString()}\nCategory: ${log.category}\nDetails: ${JSON.stringify(log.details, null, 2)}`;
 
 
112
 
113
+ navigator.clipboard.writeText(logText).then(() => {
114
+ setIsCopied(true);
115
+ toast.success('Log copied to clipboard');
116
+ setTimeout(() => setIsCopied(false), 2000);
 
117
  });
118
+ }, [log]);
119
 
120
+ const formattedTime = useMemo(() => {
121
+ const date = new Date(log.timestamp);
122
+ const now = new Date();
123
+ const isToday = date.toDateString() === now.toDateString();
124
+ const isYesterday = new Date(now.setDate(now.getDate() - 1)).toDateString() === date.toDateString();
 
125
 
126
+ const timeStr = date.toLocaleTimeString(undefined, {
127
+ hour: '2-digit',
128
+ minute: '2-digit',
129
+ hour12: !use24Hour,
 
 
130
  });
131
 
132
+ if (isToday) {
133
+ return {
134
+ primary: timeStr,
135
+ secondary: 'Today',
136
+ };
137
+ } else if (isYesterday) {
138
+ return {
139
+ primary: timeStr,
140
+ secondary: 'Yesterday',
141
+ };
142
+ } else {
143
+ const dateStr = date.toLocaleDateString(undefined, {
144
+ month: 'short',
145
+ day: 'numeric',
146
+ year: date.getFullYear() !== now.getFullYear() ? 'numeric' : undefined,
147
  });
148
+ return {
149
+ primary: dateStr,
150
+ secondary: timeStr,
151
+ };
152
  }
153
+ }, [log.timestamp, use24Hour]);
154
 
155
+ return (
156
+ <div
157
+ className={classNames('group transition-colors', 'hover:bg-gray-50 dark:hover:bg-gray-800/50', 'py-3', {
158
+ 'bg-red-50/20 dark:bg-red-900/5': log.level === 'error',
159
+ 'bg-yellow-50/20 dark:bg-yellow-900/5': log.level === 'warning',
160
+ 'bg-blue-50/20 dark:bg-blue-900/5': log.level === 'info',
161
+ 'bg-gray-50/20 dark:bg-gray-800/5': log.level === 'debug',
162
+ })}
163
+ >
164
+ <div className="px-3">
165
+ <div className="flex items-center gap-3">
166
+ <span
167
+ className={classNames('px-2 py-0.5 text-xs font-medium rounded-full', {
168
+ 'bg-red-100/80 text-red-800 dark:bg-red-500/10 dark:text-red-400': log.level === 'error',
169
+ 'bg-yellow-100/80 text-yellow-800 dark:bg-yellow-500/10 dark:text-yellow-400': log.level === 'warning',
170
+ 'bg-blue-100/80 text-blue-800 dark:bg-blue-500/10 dark:text-blue-400': log.level === 'info',
171
+ 'bg-gray-100/80 text-gray-800 dark:bg-gray-500/10 dark:text-gray-400': log.level === 'debug',
172
+ })}
173
+ >
174
+ {log.level}
175
+ </span>
176
+ <p className="flex-1 text-sm text-bolt-elements-textPrimary">{log.message}</p>
177
+ <div className="flex items-center gap-2 opacity-0 group-hover:opacity-100 transition-opacity">
178
+ <button onClick={handleCopy} className="p-1 transition-colors rounded focus:outline-none" title="Copy log">
179
+ <div
180
+ className={classNames(
181
+ 'text-base transition-colors',
182
+ isCopied
183
+ ? 'i-ph:check text-green-500'
184
+ : 'i-ph:copy text-bolt-elements-textTertiary hover:text-bolt-elements-textSecondary',
185
+ )}
186
+ />
187
+ </button>
188
+ {log.details && (
189
+ <button
190
+ onClick={() => setIsExpanded(!isExpanded)}
191
+ className="p-1 text-bolt-elements-textTertiary hover:text-bolt-elements-textSecondary transition-colors rounded focus:outline-none"
192
+ title="Toggle details"
193
+ >
194
+ <div
195
+ className={classNames('text-base transition-transform', {
196
+ 'i-ph:caret-down rotate-180': isExpanded,
197
+ 'i-ph:caret-down': !isExpanded,
198
+ })}
199
+ />
200
+ </button>
201
+ )}
202
+ </div>
203
+ </div>
204
+ <div className="flex items-center gap-2 mt-1 text-xs">
205
+ <div className="flex items-center gap-1">
206
+ <div className="i-ph:clock text-bolt-elements-textTertiary" />
207
+ <span className="text-bolt-elements-textSecondary">{formattedTime.primary}</span>
208
+ <span className="text-bolt-elements-textTertiary">·</span>
209
+ <span className="text-bolt-elements-textTertiary">{formattedTime.secondary}</span>
210
+ </div>
211
+ <span className="px-2 py-0.5 rounded-full bg-bolt-elements-background-depth-2 dark:bg-bolt-elements-background-depth-3">
212
+ {log.category}
213
+ </span>
214
+ </div>
215
+ </div>
216
 
217
+ {isExpanded && log.details && (
218
+ <motion.div
219
+ initial={{ height: 0, opacity: 0 }}
220
+ animate={{ height: 'auto', opacity: 1 }}
221
+ exit={{ height: 0, opacity: 0 }}
222
+ transition={{ duration: 0.2 }}
223
+ className="mt-2 px-3"
224
+ >
225
+ <pre className="p-2 text-sm rounded-md overflow-auto bg-bolt-elements-background-depth-2/50 dark:bg-bolt-elements-background-depth-3/50 text-bolt-elements-textSecondary">
226
+ {JSON.stringify(log.details, null, 2)}
227
+ </pre>
228
+ </motion.div>
229
+ )}
230
+ </div>
231
+ );
232
+ };
233
 
234
+ /**
235
+ * TODO: Future Enhancements
236
+ *
237
+ * 1. Advanced Features:
238
+ * - Add export to JSON/CSV functionality
239
+ * - Implement log retention policies
240
+ * - Add custom alert rules and notifications
241
+ * - Add pattern detection and highlighting
242
+ *
243
+ * 2. Visual Improvements:
244
+ * - Add dark/light mode specific styling
245
+ * - Implement collapsible JSON viewer
246
+ * - Add timeline view with zoom capabilities
247
+ *
248
+ * 3. Performance Optimizations:
249
+ * - Implement virtualized scrolling for large logs
250
+ * - Add lazy loading for log details
251
+ * - Optimize search with indexing
252
+ */
253
 
254
+ export function EventLogsTab() {
255
+ const logs = useStore(logStore.logs);
256
+ const [logLevel, setLogLevel] = useState<LogEntry['level'] | 'all'>('all');
257
+ const [logCategory, setLogCategory] = useState<LogEntry['category'] | 'all'>('all');
258
+ const [autoScroll, setAutoScroll] = useState(true);
259
+ const [searchQuery, setSearchQuery] = useState('');
260
+ const [expandAll, setExpandAll] = useState(false);
261
+ const [use24Hour, setUse24Hour] = useState(true);
262
+ const [isRefreshing, setIsRefreshing] = useState(false);
263
+ const logsContainerRef = useRef<HTMLDivElement>(null);
264
+ const [isScrolledToBottom, setIsScrolledToBottom] = useState(true);
265
 
266
+ // Add refresh function
267
+ const handleRefresh = useCallback(async () => {
268
+ setIsRefreshing(true);
269
 
270
+ try {
271
+ // Since logStore doesn't have refresh, we'll re-fetch logs
272
+ await new Promise((resolve) => setTimeout(resolve, 500)); // Simulate refresh
273
+ toast.success('Logs refreshed');
274
+ } catch (err) {
275
+ console.error('Failed to refresh logs:', err);
276
+ toast.error('Failed to refresh logs');
277
+ } finally {
278
+ setIsRefreshing(false);
279
+ }
280
  }, []);
281
 
282
+ const filteredLogs = useMemo(() => {
283
+ const allLogs = Object.values(logs);
284
+ const filtered = allLogs.filter((log) => {
285
+ const matchesLevel = logLevel === 'all' || log.level === logLevel;
286
+ const matchesCategory = logCategory === 'all' || log.category === logCategory;
287
+ const matchesSearch =
288
+ !searchQuery ||
289
+ log.message?.toLowerCase().includes(searchQuery.toLowerCase()) ||
290
+ JSON.stringify(log.details)?.toLowerCase()?.includes(searchQuery?.toLowerCase());
291
 
292
+ return matchesLevel && matchesCategory && matchesSearch;
293
+ });
294
+
295
+ return filtered.reverse();
296
+ }, [logs, logLevel, logCategory, searchQuery]);
297
 
298
  const handleClearLogs = useCallback(() => {
299
  if (confirm('Are you sure you want to clear all logs?')) {
300
  logStore.clearLogs();
301
  toast.success('Logs cleared successfully');
 
302
  }
303
  }, []);
304
 
 
330
  }
331
  }, []);
332
 
333
+ const handleScroll = () => {
334
+ const container = logsContainerRef.current;
335
+
336
+ if (!container) {
337
+ return;
 
 
 
 
 
 
 
338
  }
339
+
340
+ const { scrollTop, scrollHeight, clientHeight } = container;
341
+ const isBottom = Math.abs(scrollHeight - clientHeight - scrollTop) < 10;
342
+ setIsScrolledToBottom(isBottom);
343
  };
344
 
345
+ useEffect(() => {
346
+ const container = logsContainerRef.current;
347
+
348
+ if (container && (autoScroll || isScrolledToBottom)) {
349
+ container.scrollTop = container.scrollHeight;
 
 
 
 
 
 
 
350
  }
351
+ }, [filteredLogs, autoScroll, isScrolledToBottom]);
352
 
353
  return (
354
+ <div className="flex flex-col h-full gap-4 p-6">
355
+ {/* Header Section */}
356
+ <div className="flex flex-col gap-4 pb-4">
357
+ {/* Title and Refresh */}
358
+ <div className="flex items-center justify-between">
359
+ <div className="flex items-center gap-3">
360
+ <div className="i-ph:list text-xl text-purple-500" />
361
  <div>
362
+ <h2 className="text-lg font-semibold text-bolt-elements-textPrimary">Event Logs</h2>
363
  <p className="text-sm text-bolt-elements-textSecondary">Track system events and debug information</p>
364
  </div>
365
  </div>
366
+ <motion.button
367
+ onClick={handleRefresh}
368
+ disabled={isRefreshing}
369
+ className={classNames(
370
+ 'p-2.5 rounded-lg',
371
+ 'bg-purple-50/50 dark:bg-purple-900/10',
372
+ 'text-purple-500 hover:text-purple-600',
373
+ 'hover:bg-purple-100/50 dark:hover:bg-purple-900/20',
374
+ 'focus:outline-none focus:ring-2 focus:ring-purple-500/20',
375
+ 'transition-all duration-200 ease-in-out',
376
+ { 'opacity-50 cursor-not-allowed': isRefreshing },
377
+ )}
378
+ whileHover={{ scale: 1.05 }}
379
+ whileTap={{ scale: 0.95 }}
380
+ title="Refresh logs"
381
+ >
382
+ <div className={classNames('text-lg transition-all duration-300', { 'animate-spin': isRefreshing })}>
383
+ <div className="i-ph:arrows-clockwise" />
384
  </div>
385
+ </motion.button>
 
 
 
 
 
386
  </div>
387
 
388
+ {/* Controls Section */}
389
+ <div className="flex items-center justify-end gap-2 px-1">
390
+ <div className="flex items-center gap-6 p-1.5 rounded-lg bg-white/50 dark:bg-gray-800/30 border border-gray-200/50 dark:border-gray-700/50">
391
+ <motion.div
392
+ className="flex items-center gap-3"
393
+ whileHover={{ scale: 1.02 }}
394
+ transition={{ type: 'spring', stiffness: 400, damping: 20 }}
395
+ >
396
+ <span className="text-sm font-medium text-bolt-elements-textSecondary">Auto-scroll</span>
397
+ <Switch
398
+ checked={autoScroll}
399
+ onCheckedChange={setAutoScroll}
400
+ className="data-[state=checked]:bg-purple-500"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
401
  />
402
+ </motion.div>
403
+
404
+ <div className="h-4 w-px bg-bolt-elements-borderColor" />
405
+
406
+ <motion.div
407
+ className="flex items-center gap-3"
408
+ whileHover={{ scale: 1.02 }}
409
+ transition={{ type: 'spring', stiffness: 400, damping: 20 }}
410
+ >
411
+ <span className="text-sm font-medium text-bolt-elements-textSecondary">24h Time</span>
412
+ <Switch
413
+ checked={use24Hour}
414
+ onCheckedChange={setUse24Hour}
415
+ className="data-[state=checked]:bg-purple-500"
416
+ />
417
+ </motion.div>
418
+
419
+ <div className="h-4 w-px bg-bolt-elements-borderColor" />
420
+
421
+ <motion.div
422
+ className="flex items-center gap-3"
423
+ whileHover={{ scale: 1.02 }}
424
+ transition={{ type: 'spring', stiffness: 400, damping: 20 }}
425
+ >
426
+ <span className="text-sm font-medium text-bolt-elements-textSecondary">Expand All</span>
427
+ <Switch
428
+ checked={expandAll}
429
+ onCheckedChange={setExpandAll}
430
+ className="data-[state=checked]:bg-purple-500"
431
+ />
432
+ </motion.div>
433
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
434
  </div>
435
  </div>
436
 
437
+ {/* Header with Search */}
438
+ <div className="flex flex-col gap-4">
439
+ <div className="relative w-72">
440
+ <div className="absolute left-2.5 top-1/2 -translate-y-1/2 text-bolt-elements-textTertiary">
441
+ <div className="i-ph:magnifying-glass text-base" />
442
+ </div>
443
+ <input
444
+ type="text"
445
+ placeholder="Search logs..."
446
+ className={classNames(
447
+ 'w-full pl-8 pr-3 py-1.5 rounded-md text-sm',
448
+ 'bg-bolt-elements-background-depth-1 dark:bg-bolt-elements-background-depth-2',
449
+ 'border border-bolt-elements-borderColor',
450
+ 'text-bolt-elements-textPrimary placeholder-bolt-elements-textTertiary',
451
+ 'focus:outline-none focus:ring-1 focus:ring-purple-500/30 focus:border-purple-500/30',
452
+ 'transition-all duration-200',
453
+ )}
454
+ value={searchQuery}
455
+ onChange={(e) => setSearchQuery(e.target.value)}
456
+ />
457
+ </div>
458
+
459
+ {/* Filters Row */}
460
+ <div className="flex items-center -ml-1">
461
+ <SegmentedGroup
462
+ value={logLevel}
463
+ onChange={(value) => setLogLevel(value as LogEntry['level'] | 'all')}
464
+ options={logLevelOptions}
465
+ />
466
+ <div className="mx-2 w-px h-4 bg-bolt-elements-borderColor" />
467
+ <SegmentedGroup
468
+ value={logCategory}
469
+ onChange={(value) => setLogCategory(value as LogEntry['category'] | 'all')}
470
+ options={logCategoryOptions}
471
+ />
472
+ </div>
473
+ </div>
474
+
475
+ {/* Logs Display */}
476
+ <div
477
  ref={logsContainerRef}
478
+ className="flex-1 overflow-auto rounded-lg bg-bolt-elements-background-depth-1 dark:bg-bolt-elements-background-depth-2"
479
+ onScroll={handleScroll}
 
 
 
 
 
480
  >
481
+ <div className="divide-y divide-bolt-elements-borderColor">
482
+ {filteredLogs.map((log) => (
483
+ <LogEntryItem key={log.id} log={log} isExpanded={expandAll} use24Hour={use24Hour} />
484
+ ))}
485
+ </div>
486
+ </div>
487
+
488
+ {/* Status Bar */}
489
+ <div className="flex items-center justify-between py-2 px-4 text-sm text-bolt-elements-textSecondary">
490
+ <div className="flex items-center gap-6">
491
+ <span>{filteredLogs.length} logs displayed</span>
492
+ <span>{isScrolledToBottom ? 'Watching for new logs...' : 'Scroll to bottom to watch new logs'}</span>
493
+ </div>
494
+ <div className="flex items-center gap-2">
495
+ <motion.button
496
+ onClick={handleExportLogs}
497
+ className="flex items-center gap-2 px-3 py-1.5 text-sm bg-purple-500/10 text-purple-600 dark:text-purple-400 hover:bg-purple-500/20 rounded-md transition-colors"
498
+ whileHover={{ scale: 1.02 }}
499
+ whileTap={{ scale: 0.98 }}
500
+ >
501
+ <div className="i-ph:download-simple" />
502
+ Export
503
+ </motion.button>
504
+ <motion.button
505
+ onClick={handleClearLogs}
506
+ className="flex items-center gap-2 px-3 py-1.5 text-sm bg-red-500/10 text-red-600 dark:text-red-400 hover:bg-red-500/20 rounded-md transition-colors"
507
+ whileHover={{ scale: 1.02 }}
508
+ whileTap={{ scale: 0.98 }}
509
+ >
510
+ <div className="i-ph:trash" />
511
+ Clear
512
+ </motion.button>
513
+ </div>
514
+ </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
515
  </div>
516
  );
517
  }
app/components/settings/features/FeaturesTab.tsx CHANGED
@@ -1,11 +1,12 @@
1
- import React, { useState } from 'react';
 
2
  import { Switch } from '~/components/ui/Switch';
3
- import { PromptLibrary } from '~/lib/common/prompt-library';
4
  import { useSettings } from '~/lib/hooks/useSettings';
5
- import { motion, AnimatePresence } from 'framer-motion';
6
  import { classNames } from '~/utils/classNames';
7
- import { settingsStyles } from '~/components/settings/settings.styles';
8
  import { toast } from 'react-toastify';
 
 
 
9
 
10
  interface FeatureToggle {
11
  id: string;
@@ -20,11 +21,9 @@ interface FeatureToggle {
20
 
21
  export default function FeaturesTab() {
22
  const {
23
- debug,
24
- enableDebugMode,
25
  isLocalModel,
26
  enableLocalModels,
27
- enableEventLogs,
28
  isLatestBranch,
29
  enableLatestBranch,
30
  promptId,
@@ -35,25 +34,9 @@ export default function FeaturesTab() {
35
  contextOptimizationEnabled,
36
  } = useSettings();
37
 
38
- const [hoveredFeature, setHoveredFeature] = useState<string | null>(null);
39
- const [expandedFeature, setExpandedFeature] = useState<string | null>(null);
40
-
41
- const handleToggle = (enabled: boolean) => {
42
- enableDebugMode(enabled);
43
- enableEventLogs(enabled);
44
- toast.success(`Debug features ${enabled ? 'enabled' : 'disabled'}`);
45
- };
46
 
47
  const features: FeatureToggle[] = [
48
- {
49
- id: 'debug',
50
- title: 'Debug Features',
51
- description: 'Enable debugging tools and detailed logging',
52
- icon: 'i-ph:bug',
53
- enabled: debug,
54
- experimental: true,
55
- tooltip: 'Access advanced debugging tools and view detailed system logs',
56
- },
57
  {
58
  id: 'latestBranch',
59
  title: 'Use Main Branch',
@@ -88,13 +71,18 @@ export default function FeaturesTab() {
88
  experimental: true,
89
  tooltip: 'Try out new AI providers and models in development',
90
  },
 
 
 
 
 
 
 
 
91
  ];
92
 
93
  const handleToggleFeature = (featureId: string, enabled: boolean) => {
94
  switch (featureId) {
95
- case 'debug':
96
- handleToggle(enabled);
97
- break;
98
  case 'latestBranch':
99
  enableLatestBranch(enabled);
100
  toast.success(`Main branch updates ${enabled ? 'enabled' : 'disabled'}`);
@@ -111,13 +99,17 @@ export default function FeaturesTab() {
111
  enableLocalModels(enabled);
112
  toast.success(`Experimental providers ${enabled ? 'enabled' : 'disabled'}`);
113
  break;
 
 
 
 
114
  }
115
  };
116
 
117
  return (
118
- <div className="space-y-6">
119
  <motion.div
120
- className="flex items-center gap-2"
121
  initial={{ opacity: 0, y: -20 }}
122
  animate={{ opacity: 1, y: 0 }}
123
  transition={{ duration: 0.3 }}
@@ -125,7 +117,9 @@ export default function FeaturesTab() {
125
  <div className="i-ph:puzzle-piece text-xl text-purple-500" />
126
  <div>
127
  <h3 className="text-lg font-medium text-bolt-elements-textPrimary">Features</h3>
128
- <p className="text-sm text-bolt-elements-textSecondary">Customize your Bolt experience</p>
 
 
129
  </div>
130
  </motion.div>
131
 
@@ -139,39 +133,16 @@ export default function FeaturesTab() {
139
  <motion.div
140
  key={feature.id}
141
  className={classNames(
142
- settingsStyles.card,
143
  'bg-bolt-elements-background-depth-2',
144
  'hover:bg-bolt-elements-background-depth-3',
145
  'transition-colors duration-200',
146
- 'relative overflow-hidden group cursor-pointer',
147
  )}
148
  initial={{ opacity: 0, y: 20 }}
149
  animate={{ opacity: 1, y: 0 }}
150
  transition={{ delay: index * 0.1 }}
151
- onHoverStart={() => setHoveredFeature(feature.id)}
152
- onHoverEnd={() => setHoveredFeature(null)}
153
- onClick={() => setExpandedFeature(expandedFeature === feature.id ? null : feature.id)}
154
  >
155
- <AnimatePresence>
156
- {hoveredFeature === feature.id && feature.tooltip && (
157
- <motion.div
158
- className={classNames(
159
- 'absolute -top-12 left-1/2 transform -translate-x-1/2',
160
- 'px-3 py-2 rounded-lg text-xs',
161
- 'bg-bolt-elements-background-depth-4 text-bolt-elements-textPrimary',
162
- 'border border-bolt-elements-borderColor',
163
- 'whitespace-nowrap z-10',
164
- )}
165
- initial={{ opacity: 0, y: 10 }}
166
- animate={{ opacity: 1, y: 0 }}
167
- exit={{ opacity: 0, y: 10 }}
168
- >
169
- {feature.tooltip}
170
- <div className="absolute -bottom-1 left-1/2 transform -translate-x-1/2 rotate-45 w-2 h-2 bg-bolt-elements-background-depth-4 border-r border-b border-bolt-elements-borderColor" />
171
- </motion.div>
172
- )}
173
- </AnimatePresence>
174
-
175
  <div className="absolute top-0 right-0 p-2 flex gap-1">
176
  {feature.beta && (
177
  <motion.span
@@ -236,10 +207,10 @@ export default function FeaturesTab() {
236
 
237
  <motion.div
238
  className={classNames(
239
- settingsStyles.card,
240
  'bg-bolt-elements-background-depth-2',
241
  'hover:bg-bolt-elements-background-depth-3',
242
  'transition-all duration-200',
 
243
  'group',
244
  )}
245
  initial={{ opacity: 0, y: 20 }}
 
1
+ import React from 'react';
2
+ import { motion } from 'framer-motion';
3
  import { Switch } from '~/components/ui/Switch';
 
4
  import { useSettings } from '~/lib/hooks/useSettings';
 
5
  import { classNames } from '~/utils/classNames';
 
6
  import { toast } from 'react-toastify';
7
+ import { PromptLibrary } from '~/lib/common/prompt-library';
8
+ import { useStore } from '@nanostores/react';
9
+ import { isEventLogsEnabled } from '~/lib/stores/settings';
10
 
11
  interface FeatureToggle {
12
  id: string;
 
21
 
22
  export default function FeaturesTab() {
23
  const {
24
+ setEventLogs,
 
25
  isLocalModel,
26
  enableLocalModels,
 
27
  isLatestBranch,
28
  enableLatestBranch,
29
  promptId,
 
34
  contextOptimizationEnabled,
35
  } = useSettings();
36
 
37
+ const eventLogs = useStore(isEventLogsEnabled);
 
 
 
 
 
 
 
38
 
39
  const features: FeatureToggle[] = [
 
 
 
 
 
 
 
 
 
40
  {
41
  id: 'latestBranch',
42
  title: 'Use Main Branch',
 
71
  experimental: true,
72
  tooltip: 'Try out new AI providers and models in development',
73
  },
74
+ {
75
+ id: 'eventLogs',
76
+ title: 'Event Logging',
77
+ description: 'Enable detailed event logging and history',
78
+ icon: 'i-ph:list-bullets',
79
+ enabled: eventLogs,
80
+ tooltip: 'Record detailed logs of system events and user actions',
81
+ },
82
  ];
83
 
84
  const handleToggleFeature = (featureId: string, enabled: boolean) => {
85
  switch (featureId) {
 
 
 
86
  case 'latestBranch':
87
  enableLatestBranch(enabled);
88
  toast.success(`Main branch updates ${enabled ? 'enabled' : 'disabled'}`);
 
99
  enableLocalModels(enabled);
100
  toast.success(`Experimental providers ${enabled ? 'enabled' : 'disabled'}`);
101
  break;
102
+ case 'eventLogs':
103
+ setEventLogs(enabled);
104
+ toast.success(`Event logging ${enabled ? 'enabled' : 'disabled'}`);
105
+ break;
106
  }
107
  };
108
 
109
  return (
110
+ <div className="flex flex-col gap-6">
111
  <motion.div
112
+ className="flex items-center gap-3"
113
  initial={{ opacity: 0, y: -20 }}
114
  animate={{ opacity: 1, y: 0 }}
115
  transition={{ duration: 0.3 }}
 
117
  <div className="i-ph:puzzle-piece text-xl text-purple-500" />
118
  <div>
119
  <h3 className="text-lg font-medium text-bolt-elements-textPrimary">Features</h3>
120
+ <p className="text-sm text-bolt-elements-textSecondary">
121
+ Customize your Bolt experience with experimental features
122
+ </p>
123
  </div>
124
  </motion.div>
125
 
 
133
  <motion.div
134
  key={feature.id}
135
  className={classNames(
136
+ 'relative group cursor-pointer',
137
  'bg-bolt-elements-background-depth-2',
138
  'hover:bg-bolt-elements-background-depth-3',
139
  'transition-colors duration-200',
140
+ 'rounded-lg overflow-hidden',
141
  )}
142
  initial={{ opacity: 0, y: 20 }}
143
  animate={{ opacity: 1, y: 0 }}
144
  transition={{ delay: index * 0.1 }}
 
 
 
145
  >
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
146
  <div className="absolute top-0 right-0 p-2 flex gap-1">
147
  {feature.beta && (
148
  <motion.span
 
207
 
208
  <motion.div
209
  className={classNames(
 
210
  'bg-bolt-elements-background-depth-2',
211
  'hover:bg-bolt-elements-background-depth-3',
212
  'transition-all duration-200',
213
+ 'rounded-lg',
214
  'group',
215
  )}
216
  initial={{ opacity: 0, y: 20 }}
app/components/settings/notifications/NotificationsTab.tsx ADDED
@@ -0,0 +1,143 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState } from 'react';
2
+ import { motion } from 'framer-motion';
3
+ import { logStore } from '~/lib/stores/logs';
4
+ import { useStore } from '@nanostores/react';
5
+ import { formatDistanceToNow } from 'date-fns';
6
+ import { classNames } from '~/utils/classNames';
7
+
8
+ interface NotificationDetails {
9
+ type?: string;
10
+ message?: string;
11
+ currentVersion?: string;
12
+ latestVersion?: string;
13
+ branch?: string;
14
+ updateUrl?: string;
15
+ }
16
+
17
+ const NotificationsTab = () => {
18
+ const [filter, setFilter] = useState<'all' | 'error' | 'warning'>('all');
19
+ const logs = useStore(logStore.logs);
20
+
21
+ const handleClearNotifications = () => {
22
+ logStore.clearLogs();
23
+ };
24
+
25
+ const handleUpdateAction = (updateUrl: string) => {
26
+ window.open(updateUrl, '_blank');
27
+ };
28
+
29
+ const filteredLogs = Object.values(logs)
30
+ .filter((log) => {
31
+ if (filter === 'all') {
32
+ return log.level === 'error' || log.level === 'warning';
33
+ }
34
+
35
+ return log.level === filter;
36
+ })
37
+ .sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime());
38
+
39
+ const renderNotificationDetails = (details: NotificationDetails) => {
40
+ if (details.type === 'update') {
41
+ return (
42
+ <div className="flex flex-col gap-2">
43
+ <p className="text-sm text-gray-600 dark:text-gray-400">{details.message}</p>
44
+ <div className="flex flex-col gap-1 text-xs text-gray-500 dark:text-gray-500">
45
+ <p>Current Version: {details.currentVersion}</p>
46
+ <p>Latest Version: {details.latestVersion}</p>
47
+ <p>Branch: {details.branch}</p>
48
+ </div>
49
+ <button
50
+ onClick={() => details.updateUrl && handleUpdateAction(details.updateUrl)}
51
+ className="mt-2 inline-flex items-center gap-2 rounded-md bg-blue-50 px-3 py-1.5 text-sm font-medium text-blue-600 hover:bg-blue-100 dark:bg-blue-900/20 dark:text-blue-400 dark:hover:bg-blue-900/30"
52
+ >
53
+ <span className="i-ph:git-branch text-lg" />
54
+ View Changes
55
+ </button>
56
+ </div>
57
+ );
58
+ }
59
+
60
+ return details.message ? <p className="text-sm text-gray-600 dark:text-gray-400">{details.message}</p> : null;
61
+ };
62
+
63
+ return (
64
+ <div className="flex h-full flex-col gap-6">
65
+ <div className="flex items-center justify-between">
66
+ <div className="flex items-center gap-2">
67
+ <select
68
+ value={filter}
69
+ onChange={(e) => setFilter(e.target.value as 'all' | 'error' | 'warning')}
70
+ className="rounded-md border border-gray-300 bg-white px-3 py-1.5 text-sm shadow-sm dark:border-gray-700 dark:bg-gray-800"
71
+ >
72
+ <option value="all">All Notifications</option>
73
+ <option value="error">Errors</option>
74
+ <option value="warning">Warnings</option>
75
+ </select>
76
+ </div>
77
+ <button
78
+ onClick={handleClearNotifications}
79
+ className="rounded-md bg-gray-50 px-3 py-1.5 text-sm font-medium text-gray-600 hover:bg-gray-100 dark:bg-gray-800 dark:text-gray-400 dark:hover:bg-gray-700"
80
+ >
81
+ Clear All
82
+ </button>
83
+ </div>
84
+
85
+ <div className="flex flex-col gap-4">
86
+ {filteredLogs.length === 0 ? (
87
+ <div className="flex flex-col items-center justify-center gap-4 rounded-lg border border-gray-200 p-8 text-center dark:border-gray-700">
88
+ <span className="i-ph:bell-slash text-4xl text-gray-400 dark:text-gray-600" />
89
+ <div className="flex flex-col gap-1">
90
+ <h3 className="text-sm font-medium text-gray-900 dark:text-gray-100">No Notifications</h3>
91
+ <p className="text-sm text-gray-500 dark:text-gray-400">You're all caught up!</p>
92
+ </div>
93
+ </div>
94
+ ) : (
95
+ filteredLogs.map((log) => (
96
+ <motion.div
97
+ key={log.id}
98
+ initial={{ opacity: 0, y: 20 }}
99
+ animate={{ opacity: 1, y: 0 }}
100
+ className={classNames(
101
+ 'flex flex-col gap-2 rounded-lg border p-4',
102
+ log.level === 'error'
103
+ ? 'border-red-200 bg-red-50 dark:border-red-900/50 dark:bg-red-900/20'
104
+ : 'border-yellow-200 bg-yellow-50 dark:border-yellow-900/50 dark:bg-yellow-900/20',
105
+ )}
106
+ >
107
+ <div className="flex items-start justify-between gap-4">
108
+ <div className="flex items-center gap-3">
109
+ <span
110
+ className={classNames(
111
+ 'text-lg',
112
+ log.level === 'error'
113
+ ? 'i-ph:warning-circle text-red-600 dark:text-red-400'
114
+ : 'i-ph:warning text-yellow-600 dark:text-yellow-400',
115
+ )}
116
+ />
117
+ <div>
118
+ <h3
119
+ className={classNames(
120
+ 'text-sm font-medium',
121
+ log.level === 'error'
122
+ ? 'text-red-900 dark:text-red-300'
123
+ : 'text-yellow-900 dark:text-yellow-300',
124
+ )}
125
+ >
126
+ {log.message}
127
+ </h3>
128
+ {log.details && renderNotificationDetails(log.details as NotificationDetails)}
129
+ </div>
130
+ </div>
131
+ <time className="shrink-0 text-xs text-gray-500 dark:text-gray-400">
132
+ {formatDistanceToNow(new Date(log.timestamp), { addSuffix: true })}
133
+ </time>
134
+ </div>
135
+ </motion.div>
136
+ ))
137
+ )}
138
+ </div>
139
+ </div>
140
+ );
141
+ };
142
+
143
+ export default NotificationsTab;
app/components/settings/profile/ProfileTab.tsx CHANGED
@@ -1,10 +1,8 @@
1
- import React, { useState, useRef, useEffect } from 'react';
2
  import { AnimatePresence } from 'framer-motion';
3
  import { toast } from 'react-toastify';
4
  import { classNames } from '~/utils/classNames';
5
- import { Switch } from '~/components/ui/Switch';
6
  import type { UserProfile } from '~/components/settings/settings.types';
7
- import { themeStore, kTheme } from '~/lib/stores/theme';
8
  import { motion } from 'framer-motion';
9
 
10
  const MAX_FILE_SIZE = 5 * 1024 * 1024; // 5MB
@@ -15,7 +13,6 @@ export default function ProfileTab() {
15
  const fileInputRef = useRef<HTMLInputElement>(null);
16
  const [isLoading, setIsLoading] = useState(false);
17
  const [showPassword, setShowPassword] = useState(false);
18
- const [currentTimezone, setCurrentTimezone] = useState('');
19
  const [profile, setProfile] = useState<UserProfile>(() => {
20
  const saved = localStorage.getItem('bolt_user_profile');
21
  return saved
@@ -23,35 +20,11 @@ export default function ProfileTab() {
23
  : {
24
  name: '',
25
  email: '',
26
- theme: 'system',
27
- notifications: true,
28
- language: 'en',
29
- timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
30
  password: '',
31
  bio: '',
32
  };
33
  });
34
 
35
- useEffect(() => {
36
- setCurrentTimezone(Intl.DateTimeFormat().resolvedOptions().timeZone);
37
- }, []);
38
-
39
- // Apply theme when profile changes
40
- useEffect(() => {
41
- if (profile.theme === 'system') {
42
- // Remove theme override
43
- localStorage.removeItem(kTheme);
44
-
45
- const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
46
- document.querySelector('html')?.setAttribute('data-theme', prefersDark ? 'dark' : 'light');
47
- } else {
48
- // Set specific theme
49
- localStorage.setItem(kTheme, profile.theme);
50
- document.querySelector('html')?.setAttribute('data-theme', profile.theme);
51
- themeStore.set(profile.theme);
52
- }
53
- }, [profile.theme]);
54
-
55
  const handleAvatarUpload = async (event: React.ChangeEvent<HTMLInputElement>) => {
56
  const file = event.target.files?.[0];
57
 
@@ -105,7 +78,20 @@ export default function ProfileTab() {
105
  setIsLoading(true);
106
 
107
  try {
108
- localStorage.setItem('bolt_user_profile', JSON.stringify(profile));
 
 
 
 
 
 
 
 
 
 
 
 
 
109
  toast.success('Profile settings saved successfully');
110
  } catch (error) {
111
  console.error('Error saving profile:', error);
@@ -117,259 +103,142 @@ export default function ProfileTab() {
117
 
118
  return (
119
  <div className="space-y-4">
120
- <div className="grid grid-cols-1 gap-4">
121
- {/* Profile Information */}
122
- <motion.div
123
- className="bg-white dark:bg-[#0A0A0A] rounded-lg shadow-sm dark:shadow-none"
124
- initial={{ opacity: 0, y: 20 }}
125
- animate={{ opacity: 1, y: 0 }}
126
- transition={{ delay: 0.1 }}
127
- >
128
- <div className="flex items-center gap-2 px-4 pt-4 pb-2">
129
- <div className="i-ph:user-circle-fill w-4 h-4 text-purple-500" />
130
- <span className="text-sm font-medium text-bolt-elements-textPrimary">Personal Information</span>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
131
  </div>
132
- <div className="flex items-start gap-4 p-4">
133
- {/* Avatar */}
134
- <div className="relative group">
135
- <div className="w-12 h-12 rounded-lg bg-[#F5F5F5] dark:bg-[#1A1A1A] flex items-center justify-center overflow-hidden">
136
- <AnimatePresence mode="wait">
137
- {isLoading ? (
138
- <div className="i-ph:spinner-gap-bold animate-spin text-purple-500" />
139
- ) : profile.avatar ? (
140
- <img src={profile.avatar} alt="Profile" className="w-full h-full object-cover" />
141
- ) : (
142
- <div className="i-ph:user-circle-fill text-bolt-elements-textSecondary" />
143
- )}
144
- </AnimatePresence>
145
  </div>
146
- <button
147
- onClick={() => fileInputRef.current?.click()}
148
- disabled={isLoading}
149
- className="absolute inset-0 flex items-center justify-center bg-black/50 opacity-0 group-hover:opacity-100 transition-opacity rounded-lg"
150
- >
151
- <div className="i-ph:camera-fill text-white" />
152
- </button>
153
  <input
154
- ref={fileInputRef}
155
- type="file"
156
- accept={ALLOWED_FILE_TYPES.join(',')}
157
- onChange={handleAvatarUpload}
158
- className="hidden"
 
 
 
 
 
 
159
  />
160
  </div>
161
 
162
- {/* Profile Fields */}
163
- <div className="flex-1 space-y-3">
164
- <div className="relative">
165
- <div className="absolute left-3 top-1/2 -translate-y-1/2">
166
- <div className="i-ph:user-fill w-4 h-4 text-bolt-elements-textTertiary" />
167
- </div>
168
- <input
169
- type="text"
170
- value={profile.name}
171
- onChange={(e) => setProfile((prev) => ({ ...prev, name: e.target.value }))}
172
- placeholder="Enter your name"
173
- className={classNames(
174
- 'w-full px-3 py-1.5 rounded-lg text-sm',
175
- 'pl-10',
176
- 'bg-[#F5F5F5] dark:bg-[#1A1A1A] border border-[#E5E5E5] dark:border-[#333333]',
177
- 'text-bolt-elements-textPrimary placeholder-bolt-elements-textTertiary',
178
- 'focus:outline-none focus:ring-1 focus:ring-purple-500',
179
- )}
180
- />
181
  </div>
182
-
183
- <div className="relative">
184
- <div className="absolute left-3 top-1/2 -translate-y-1/2">
185
- <div className="i-ph:envelope-fill w-4 h-4 text-bolt-elements-textTertiary" />
186
- </div>
187
- <input
188
- type="email"
189
- value={profile.email}
190
- onChange={(e) => setProfile((prev) => ({ ...prev, email: e.target.value }))}
191
- placeholder="Enter your email"
192
- className={classNames(
193
- 'w-full px-3 py-1.5 rounded-lg text-sm',
194
- 'pl-10',
195
- 'bg-[#F5F5F5] dark:bg-[#1A1A1A] border border-[#E5E5E5] dark:border-[#333333]',
196
- 'text-bolt-elements-textPrimary placeholder-bolt-elements-textTertiary',
197
- 'focus:outline-none focus:ring-1 focus:ring-purple-500',
198
- )}
199
- />
200
- </div>
201
-
202
- <div className="relative">
203
- <input
204
- type={showPassword ? 'text' : 'password'}
205
- value={profile.password}
206
- onChange={(e) => setProfile((prev) => ({ ...prev, password: e.target.value }))}
207
- placeholder="Enter new password"
208
- className={classNames(
209
- 'w-full px-3 py-1.5 rounded-lg text-sm',
210
- 'bg-[#F5F5F5] dark:bg-[#1A1A1A] border border-[#E5E5E5] dark:border-[#333333]',
211
- 'text-bolt-elements-textPrimary placeholder-bolt-elements-textTertiary',
212
- 'focus:outline-none focus:ring-1 focus:ring-purple-500',
213
- )}
214
- />
215
- <button
216
- type="button"
217
- onClick={() => setShowPassword(!showPassword)}
218
- className={classNames(
219
- 'absolute right-3 top-1/2 -translate-y-1/2',
220
- 'flex items-center justify-center',
221
- 'w-6 h-6 rounded-md',
222
- 'text-bolt-elements-textSecondary',
223
- 'hover:text-bolt-elements-item-contentActive',
224
- 'hover:bg-bolt-elements-item-backgroundActive',
225
- 'transition-colors',
226
- )}
227
- >
228
- <div className={classNames(showPassword ? 'i-ph:eye-slash-fill' : 'i-ph:eye-fill', 'w-4 h-4')} />
229
- </button>
230
- </div>
231
- </div>
232
- </div>
233
- </motion.div>
234
-
235
- {/* Theme & Language */}
236
- <motion.div
237
- className="bg-white dark:bg-[#0A0A0A] rounded-lg shadow-sm dark:shadow-none p-4 space-y-4"
238
- initial={{ opacity: 0, y: 20 }}
239
- animate={{ opacity: 1, y: 0 }}
240
- transition={{ delay: 0.2 }}
241
- >
242
- <div className="flex items-center gap-2 mb-4">
243
- <div className="i-ph:palette-fill w-4 h-4 text-purple-500" />
244
- <span className="text-sm font-medium text-bolt-elements-textPrimary">Appearance</span>
245
- </div>
246
-
247
- <div>
248
- <div className="flex items-center gap-2 mb-2">
249
- <div className="i-ph:paint-brush-fill w-4 h-4 text-bolt-elements-textSecondary" />
250
- <label className="block text-sm text-bolt-elements-textSecondary">Theme</label>
251
- </div>
252
- <div className="flex gap-2">
253
- {(['light', 'dark', 'system'] as const).map((theme) => (
254
- <button
255
- key={theme}
256
- onClick={() => setProfile((prev) => ({ ...prev, theme }))}
257
- className={classNames(
258
- 'px-3 py-1.5 rounded-lg text-sm flex items-center gap-2 transition-colors',
259
- profile.theme === theme
260
- ? 'bg-purple-500 text-white hover:bg-purple-600'
261
- : 'bg-[#F5F5F5] dark:bg-[#1A1A1A] text-bolt-elements-textSecondary hover:bg-[#E5E5E5] dark:hover:bg-[#252525] hover:text-bolt-elements-textPrimary',
262
- )}
263
- >
264
- <div
265
- className={`w-4 h-4 ${
266
- theme === 'light'
267
- ? 'i-ph:sun-fill'
268
- : theme === 'dark'
269
- ? 'i-ph:moon-stars-fill'
270
- : 'i-ph:monitor-fill'
271
- }`}
272
- />
273
- <span className="capitalize">{theme}</span>
274
- </button>
275
- ))}
276
  </div>
277
- </div>
278
 
279
- <div>
280
- <div className="flex items-center gap-2 mb-2">
281
- <div className="i-ph:translate-fill w-4 h-4 text-bolt-elements-textSecondary" />
282
- <label className="block text-sm text-bolt-elements-textSecondary">Language</label>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
283
  </div>
284
- <select
285
- value={profile.language}
286
- onChange={(e) => setProfile((prev) => ({ ...prev, language: e.target.value }))}
287
- className={classNames(
288
- 'w-full px-3 py-1.5 rounded-lg text-sm',
289
- 'bg-[#F5F5F5] dark:bg-[#1A1A1A] border border-[#E5E5E5] dark:border-[#333333]',
290
- 'text-bolt-elements-textPrimary',
291
- 'focus:outline-none focus:ring-1 focus:ring-purple-500',
292
- )}
293
- >
294
- <option value="en">English</option>
295
- <option value="es">Español</option>
296
- <option value="fr">Français</option>
297
- <option value="de">Deutsch</option>
298
- <option value="it">Italiano</option>
299
- <option value="pt">Português</option>
300
- <option value="ru">Русский</option>
301
- <option value="zh">中文</option>
302
- <option value="ja">日本語</option>
303
- <option value="ko">한국어</option>
304
- </select>
305
- </div>
306
 
307
- <div>
308
- <div className="flex items-center gap-2 mb-2">
309
- <div className="i-ph:bell-fill w-4 h-4 text-bolt-elements-textSecondary" />
310
- <label className="block text-sm text-bolt-elements-textSecondary">Notifications</label>
311
- </div>
312
- <div className="flex items-center justify-between">
313
- <span className="text-sm text-bolt-elements-textSecondary">
314
- {profile.notifications ? 'Notifications are enabled' : 'Notifications are disabled'}
315
- </span>
316
- <Switch
317
- checked={profile.notifications}
318
- onCheckedChange={(checked) => setProfile((prev) => ({ ...prev, notifications: checked }))}
 
319
  />
320
  </div>
321
  </div>
322
- </motion.div>
323
-
324
- {/* Timezone */}
325
- <div className="bg-white dark:bg-[#0A0A0A] rounded-lg shadow-sm dark:shadow-none p-4">
326
- <div className="flex items-center gap-2 mb-4">
327
- <div className="i-ph:clock-fill w-4 h-4 text-purple-500" />
328
- <span className="text-sm font-medium text-bolt-elements-textPrimary">Time Settings</span>
329
- </div>
330
-
331
- <div className="flex items-center gap-2 mb-2">
332
- <div className="i-ph:globe-fill w-4 h-4 text-bolt-elements-textSecondary" />
333
- <label className="block text-sm text-bolt-elements-textSecondary">Timezone</label>
334
- </div>
335
- <div className="flex gap-2">
336
- <select
337
- value={profile.timezone}
338
- onChange={(e) => setProfile((prev) => ({ ...prev, timezone: e.target.value }))}
339
- className={classNames(
340
- 'flex-1 px-3 py-1.5 rounded-lg text-sm',
341
- 'bg-[#F5F5F5] dark:bg-[#1A1A1A] border border-[#E5E5E5] dark:border-[#333333]',
342
- 'text-bolt-elements-textPrimary',
343
- 'focus:outline-none focus:ring-1 focus:ring-purple-500',
344
- )}
345
- >
346
- {Intl.supportedValuesOf('timeZone').map((tz) => (
347
- <option key={tz} value={tz}>
348
- {tz.replace(/_/g, ' ')}
349
- </option>
350
- ))}
351
- </select>
352
- <button
353
- onClick={() => setProfile((prev) => ({ ...prev, timezone: currentTimezone }))}
354
- className={classNames(
355
- 'px-3 py-1.5 rounded-lg text-sm flex items-center gap-2',
356
- 'bg-[#F5F5F5] dark:bg-[#1A1A1A] text-bolt-elements-textSecondary',
357
- 'hover:text-bolt-elements-textPrimary',
358
- )}
359
- >
360
- <div className="i-ph:crosshair-simple-fill" />
361
- Auto-detect
362
- </button>
363
- </div>
364
  </div>
365
- </div>
366
 
367
  {/* Save Button */}
368
  <motion.div
369
  className="flex justify-end mt-6"
370
  initial={{ opacity: 0, y: 20 }}
371
  animate={{ opacity: 1, y: 0 }}
372
- transition={{ delay: 0.3 }}
373
  >
374
  <button
375
  onClick={handleSave}
 
1
+ import React, { useState, useRef } from 'react';
2
  import { AnimatePresence } from 'framer-motion';
3
  import { toast } from 'react-toastify';
4
  import { classNames } from '~/utils/classNames';
 
5
  import type { UserProfile } from '~/components/settings/settings.types';
 
6
  import { motion } from 'framer-motion';
7
 
8
  const MAX_FILE_SIZE = 5 * 1024 * 1024; // 5MB
 
13
  const fileInputRef = useRef<HTMLInputElement>(null);
14
  const [isLoading, setIsLoading] = useState(false);
15
  const [showPassword, setShowPassword] = useState(false);
 
16
  const [profile, setProfile] = useState<UserProfile>(() => {
17
  const saved = localStorage.getItem('bolt_user_profile');
18
  return saved
 
20
  : {
21
  name: '',
22
  email: '',
 
 
 
 
23
  password: '',
24
  bio: '',
25
  };
26
  });
27
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
28
  const handleAvatarUpload = async (event: React.ChangeEvent<HTMLInputElement>) => {
29
  const file = event.target.files?.[0];
30
 
 
78
  setIsLoading(true);
79
 
80
  try {
81
+ // Get existing profile data to preserve settings
82
+ const existingProfile = JSON.parse(localStorage.getItem('bolt_user_profile') || '{}');
83
+
84
+ // Merge with new profile data
85
+ const updatedProfile = {
86
+ ...existingProfile,
87
+ name: profile.name,
88
+ email: profile.email,
89
+ password: profile.password,
90
+ bio: profile.bio,
91
+ avatar: profile.avatar,
92
+ };
93
+
94
+ localStorage.setItem('bolt_user_profile', JSON.stringify(updatedProfile));
95
  toast.success('Profile settings saved successfully');
96
  } catch (error) {
97
  console.error('Error saving profile:', error);
 
103
 
104
  return (
105
  <div className="space-y-4">
106
+ {/* Profile Information */}
107
+ <motion.div
108
+ className="bg-white dark:bg-[#0A0A0A] rounded-lg shadow-sm dark:shadow-none"
109
+ initial={{ opacity: 0, y: 20 }}
110
+ animate={{ opacity: 1, y: 0 }}
111
+ transition={{ delay: 0.1 }}
112
+ >
113
+ <div className="flex items-center gap-2 px-4 pt-4 pb-2">
114
+ <div className="i-ph:user-circle-fill w-4 h-4 text-purple-500" />
115
+ <span className="text-sm font-medium text-bolt-elements-textPrimary">Personal Information</span>
116
+ </div>
117
+ <div className="flex items-start gap-4 p-4">
118
+ {/* Avatar */}
119
+ <div className="relative group">
120
+ <div className="w-12 h-12 rounded-lg bg-[#F5F5F5] dark:bg-[#1A1A1A] flex items-center justify-center overflow-hidden">
121
+ <AnimatePresence mode="wait">
122
+ {isLoading ? (
123
+ <div className="i-ph:spinner-gap-bold animate-spin text-purple-500" />
124
+ ) : profile.avatar ? (
125
+ <img src={profile.avatar} alt="Profile" className="w-full h-full object-cover" />
126
+ ) : (
127
+ <div className="i-ph:user-circle-fill text-bolt-elements-textSecondary" />
128
+ )}
129
+ </AnimatePresence>
130
+ </div>
131
+ <button
132
+ onClick={() => fileInputRef.current?.click()}
133
+ disabled={isLoading}
134
+ className="absolute inset-0 flex items-center justify-center bg-black/50 opacity-0 group-hover:opacity-100 transition-opacity rounded-lg"
135
+ >
136
+ <div className="i-ph:camera-fill text-white" />
137
+ </button>
138
+ <input
139
+ ref={fileInputRef}
140
+ type="file"
141
+ accept={ALLOWED_FILE_TYPES.join(',')}
142
+ onChange={handleAvatarUpload}
143
+ className="hidden"
144
+ />
145
  </div>
146
+
147
+ {/* Profile Fields */}
148
+ <div className="flex-1 space-y-3">
149
+ <div className="relative">
150
+ <div className="absolute left-3 top-1/2 -translate-y-1/2">
151
+ <div className="i-ph:user-fill w-4 h-4 text-bolt-elements-textTertiary" />
 
 
 
 
 
 
 
152
  </div>
 
 
 
 
 
 
 
153
  <input
154
+ type="text"
155
+ value={profile.name}
156
+ onChange={(e) => setProfile((prev) => ({ ...prev, name: e.target.value }))}
157
+ placeholder="Enter your name"
158
+ className={classNames(
159
+ 'w-full px-3 py-1.5 rounded-lg text-sm',
160
+ 'pl-10',
161
+ 'bg-[#F5F5F5] dark:bg-[#1A1A1A] border border-[#E5E5E5] dark:border-[#333333]',
162
+ 'text-bolt-elements-textPrimary placeholder-bolt-elements-textTertiary',
163
+ 'focus:outline-none focus:ring-1 focus:ring-purple-500',
164
+ )}
165
  />
166
  </div>
167
 
168
+ <div className="relative">
169
+ <div className="absolute left-3 top-1/2 -translate-y-1/2">
170
+ <div className="i-ph:envelope-fill w-4 h-4 text-bolt-elements-textTertiary" />
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
171
  </div>
172
+ <input
173
+ type="email"
174
+ value={profile.email}
175
+ onChange={(e) => setProfile((prev) => ({ ...prev, email: e.target.value }))}
176
+ placeholder="Enter your email"
177
+ className={classNames(
178
+ 'w-full px-3 py-1.5 rounded-lg text-sm',
179
+ 'pl-10',
180
+ 'bg-[#F5F5F5] dark:bg-[#1A1A1A] border border-[#E5E5E5] dark:border-[#333333]',
181
+ 'text-bolt-elements-textPrimary placeholder-bolt-elements-textTertiary',
182
+ 'focus:outline-none focus:ring-1 focus:ring-purple-500',
183
+ )}
184
+ />
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
185
  </div>
 
186
 
187
+ <div className="relative">
188
+ <input
189
+ type={showPassword ? 'text' : 'password'}
190
+ value={profile.password}
191
+ onChange={(e) => setProfile((prev) => ({ ...prev, password: e.target.value }))}
192
+ placeholder="Enter new password"
193
+ className={classNames(
194
+ 'w-full px-3 py-1.5 rounded-lg text-sm',
195
+ 'bg-[#F5F5F5] dark:bg-[#1A1A1A] border border-[#E5E5E5] dark:border-[#333333]',
196
+ 'text-bolt-elements-textPrimary placeholder-bolt-elements-textTertiary',
197
+ 'focus:outline-none focus:ring-1 focus:ring-purple-500',
198
+ )}
199
+ />
200
+ <button
201
+ type="button"
202
+ onClick={() => setShowPassword(!showPassword)}
203
+ className={classNames(
204
+ 'absolute right-3 top-1/2 -translate-y-1/2',
205
+ 'flex items-center justify-center',
206
+ 'w-6 h-6 rounded-md',
207
+ 'text-bolt-elements-textSecondary',
208
+ 'hover:text-bolt-elements-item-contentActive',
209
+ 'hover:bg-bolt-elements-item-backgroundActive',
210
+ 'transition-colors',
211
+ )}
212
+ >
213
+ <div className={classNames(showPassword ? 'i-ph:eye-slash-fill' : 'i-ph:eye-fill', 'w-4 h-4')} />
214
+ </button>
215
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
216
 
217
+ <div className="relative">
218
+ <textarea
219
+ value={profile.bio}
220
+ onChange={(e) => setProfile((prev) => ({ ...prev, bio: e.target.value }))}
221
+ placeholder="Tell us about yourself"
222
+ rows={3}
223
+ className={classNames(
224
+ 'w-full px-3 py-2 rounded-lg text-sm',
225
+ 'bg-[#F5F5F5] dark:bg-[#1A1A1A] border border-[#E5E5E5] dark:border-[#333333]',
226
+ 'text-bolt-elements-textPrimary placeholder-bolt-elements-textTertiary',
227
+ 'focus:outline-none focus:ring-1 focus:ring-purple-500',
228
+ 'resize-none',
229
+ )}
230
  />
231
  </div>
232
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
233
  </div>
234
+ </motion.div>
235
 
236
  {/* Save Button */}
237
  <motion.div
238
  className="flex justify-end mt-6"
239
  initial={{ opacity: 0, y: 20 }}
240
  animate={{ opacity: 1, y: 0 }}
241
+ transition={{ delay: 0.2 }}
242
  >
243
  <button
244
  onClick={handleSave}
app/components/settings/providers/ProvidersTab.tsx CHANGED
@@ -85,8 +85,8 @@ interface CategoryToggleState {
85
  local: boolean;
86
  }
87
 
88
- export default function ProvidersTab() {
89
- const { providers, updateProviderSettings, isLocalModel } = useSettings();
90
  const [editingProvider, setEditingProvider] = useState<string | null>(null);
91
  const [filteredProviders, setFilteredProviders] = useState<IProviderConfig[]>([]);
92
  const [categoryEnabled, setCategoryEnabled] = useState<CategoryToggleState>({
@@ -128,12 +128,12 @@ export default function ProvidersTab() {
128
  // Get providers for this category
129
  const categoryProviders = groupedProviders[category].providers;
130
  categoryProviders.forEach((provider) => {
131
- updateProviderSettings(provider.name, { ...provider.settings, enabled });
132
  });
133
 
134
  toast.success(enabled ? `All ${category} providers enabled` : `All ${category} providers disabled`);
135
  },
136
- [groupedProviders, updateProviderSettings],
137
  );
138
 
139
  // Add effect to update category toggle states based on provider states
@@ -147,26 +147,32 @@ export default function ProvidersTab() {
147
 
148
  // Effect to filter and sort providers
149
  useEffect(() => {
150
- let newFilteredProviders: IProviderConfig[] = Object.entries(providers).map(([key, value]) => ({
151
- ...value,
152
- name: key,
153
- }));
154
-
155
- if (!isLocalModel) {
156
- newFilteredProviders = newFilteredProviders.filter((provider) => !LOCAL_PROVIDERS.includes(provider.name));
157
- }
 
 
 
 
158
 
159
- newFilteredProviders.sort((a, b) => a.name.localeCompare(b.name));
 
 
160
 
161
- // Split providers into regular and URL-configurable
162
- const regular = newFilteredProviders.filter((p) => !URL_CONFIGURABLE_PROVIDERS.includes(p.name));
163
- const urlConfigurable = newFilteredProviders.filter((p) => URL_CONFIGURABLE_PROVIDERS.includes(p.name));
164
 
165
  setFilteredProviders([...regular, ...urlConfigurable]);
166
- }, [providers, isLocalModel]);
167
 
168
  const handleToggleProvider = (provider: IProviderConfig, enabled: boolean) => {
169
- updateProviderSettings(provider.name, { ...provider.settings, enabled });
170
 
171
  if (enabled) {
172
  logStore.logProvider(`Provider ${provider.name} enabled`, { provider: provider.name });
@@ -184,7 +190,7 @@ export default function ProvidersTab() {
184
  newBaseUrl = undefined;
185
  }
186
 
187
- updateProviderSettings(provider.name, { ...provider.settings, baseUrl: newBaseUrl });
188
  logStore.logProvider(`Base URL updated for ${provider.name}`, {
189
  provider: provider.name,
190
  baseUrl: newBaseUrl,
@@ -404,4 +410,4 @@ export default function ProvidersTab() {
404
  ))}
405
  </div>
406
  );
407
- }
 
85
  local: boolean;
86
  }
87
 
88
+ export const ProvidersTab = () => {
89
+ const settings = useSettings();
90
  const [editingProvider, setEditingProvider] = useState<string | null>(null);
91
  const [filteredProviders, setFilteredProviders] = useState<IProviderConfig[]>([]);
92
  const [categoryEnabled, setCategoryEnabled] = useState<CategoryToggleState>({
 
128
  // Get providers for this category
129
  const categoryProviders = groupedProviders[category].providers;
130
  categoryProviders.forEach((provider) => {
131
+ settings.updateProviderSettings(provider.name, { ...provider.settings, enabled });
132
  });
133
 
134
  toast.success(enabled ? `All ${category} providers enabled` : `All ${category} providers disabled`);
135
  },
136
+ [groupedProviders, settings.updateProviderSettings],
137
  );
138
 
139
  // Add effect to update category toggle states based on provider states
 
147
 
148
  // Effect to filter and sort providers
149
  useEffect(() => {
150
+ const newFilteredProviders = Object.entries(settings.providers || {}).map(([key, value]) => {
151
+ const provider = value as IProviderConfig;
152
+ return {
153
+ name: key,
154
+ settings: provider.settings,
155
+ staticModels: provider.staticModels || [],
156
+ getDynamicModels: provider.getDynamicModels,
157
+ getApiKeyLink: provider.getApiKeyLink,
158
+ labelForGetApiKey: provider.labelForGetApiKey,
159
+ icon: provider.icon,
160
+ } as IProviderConfig;
161
+ });
162
 
163
+ const filtered = !settings.isLocalModel
164
+ ? newFilteredProviders.filter((provider) => !LOCAL_PROVIDERS.includes(provider.name))
165
+ : newFilteredProviders;
166
 
167
+ const sorted = filtered.sort((a, b) => a.name.localeCompare(b.name));
168
+ const regular = sorted.filter((p) => !URL_CONFIGURABLE_PROVIDERS.includes(p.name));
169
+ const urlConfigurable = sorted.filter((p) => URL_CONFIGURABLE_PROVIDERS.includes(p.name));
170
 
171
  setFilteredProviders([...regular, ...urlConfigurable]);
172
+ }, [settings.providers, settings.isLocalModel]);
173
 
174
  const handleToggleProvider = (provider: IProviderConfig, enabled: boolean) => {
175
+ settings.updateProviderSettings(provider.name, { ...provider.settings, enabled });
176
 
177
  if (enabled) {
178
  logStore.logProvider(`Provider ${provider.name} enabled`, { provider: provider.name });
 
190
  newBaseUrl = undefined;
191
  }
192
 
193
+ settings.updateProviderSettings(provider.name, { ...provider.settings, baseUrl: newBaseUrl });
194
  logStore.logProvider(`Base URL updated for ${provider.name}`, {
195
  provider: provider.name,
196
  baseUrl: newBaseUrl,
 
410
  ))}
411
  </div>
412
  );
413
+ };
app/components/settings/settings.styles.ts CHANGED
@@ -7,14 +7,14 @@ export function cn(...inputs: ClassValue[]) {
7
 
8
  export const settingsStyles = {
9
  // Card styles
10
- card: 'bg-white dark:bg-[#0A0A0A] rounded-lg p-6 border border-[#E5E5E5] dark:border-[#1A1A1A]',
11
 
12
  // Button styles
13
  button: {
14
  base: 'inline-flex items-center gap-2 px-4 py-2 rounded-lg text-sm disabled:opacity-50 disabled:cursor-not-allowed',
15
  primary: 'bg-purple-500 text-white hover:bg-purple-600',
16
  secondary:
17
- 'bg-[#F5F5F5] dark:bg-[#1A1A1A] text-[#666666] dark:text-[#999999] hover:text-[#333333] dark:hover:text-white',
18
  danger: 'bg-red-50 text-red-500 hover:bg-red-100 dark:bg-red-500/10 dark:hover:bg-red-500/20',
19
  warning: 'bg-yellow-50 text-yellow-600 hover:bg-yellow-100 dark:bg-yellow-500/10 dark:hover:bg-yellow-500/20',
20
  success: 'bg-green-50 text-green-600 hover:bg-green-100 dark:bg-green-500/10 dark:hover:bg-green-500/20',
@@ -22,15 +22,21 @@ export const settingsStyles = {
22
 
23
  // Form styles
24
  form: {
25
- label: 'block text-sm text-bolt-elements-textSecondary mb-2',
26
  input:
27
- 'w-full px-3 py-2 rounded-lg text-sm bg-[#F8F8F8] dark:bg-[#1A1A1A] border border-[#E5E5E5] dark:border-[#333333] text-bolt-elements-textPrimary placeholder-bolt-elements-textTertiary focus:outline-none focus:ring-1 focus:ring-purple-500',
28
  },
29
 
30
  // Search container
31
  search: {
32
  input:
33
- 'w-full h-10 pl-10 pr-4 rounded-lg text-sm bg-[#F8F8F8] dark:bg-[#1A1A1A] border border-[#E5E5E5] dark:border-[#333333] text-bolt-elements-textPrimary placeholder-bolt-elements-textTertiary focus:outline-none focus:ring-1 focus:ring-purple-500 transition-all',
 
 
 
 
 
 
34
  },
35
 
36
  'loading-spinner': 'i-ph:spinner-gap-bold animate-spin w-4 h-4',
 
7
 
8
  export const settingsStyles = {
9
  // Card styles
10
+ card: 'bg-bolt-elements-background dark:bg-bolt-elements-backgroundDark rounded-lg p-6 border border-bolt-elements-border dark:border-bolt-elements-borderDark',
11
 
12
  // Button styles
13
  button: {
14
  base: 'inline-flex items-center gap-2 px-4 py-2 rounded-lg text-sm disabled:opacity-50 disabled:cursor-not-allowed',
15
  primary: 'bg-purple-500 text-white hover:bg-purple-600',
16
  secondary:
17
+ 'bg-bolt-elements-hover dark:bg-bolt-elements-hoverDark text-bolt-elements-textSecondary dark:text-bolt-elements-textSecondaryDark hover:text-bolt-elements-textPrimary dark:hover:text-bolt-elements-textPrimaryDark',
18
  danger: 'bg-red-50 text-red-500 hover:bg-red-100 dark:bg-red-500/10 dark:hover:bg-red-500/20',
19
  warning: 'bg-yellow-50 text-yellow-600 hover:bg-yellow-100 dark:bg-yellow-500/10 dark:hover:bg-yellow-500/20',
20
  success: 'bg-green-50 text-green-600 hover:bg-green-100 dark:bg-green-500/10 dark:hover:bg-green-500/20',
 
22
 
23
  // Form styles
24
  form: {
25
+ label: 'block text-sm text-bolt-elements-textSecondary dark:text-bolt-elements-textSecondaryDark mb-2',
26
  input:
27
+ 'w-full px-3 py-2 rounded-lg text-sm bg-bolt-elements-hover dark:bg-bolt-elements-hoverDark border border-bolt-elements-border dark:border-bolt-elements-borderDark text-bolt-elements-textPrimary dark:text-bolt-elements-textPrimaryDark placeholder-bolt-elements-textTertiary focus:outline-none focus:ring-1 focus:ring-purple-500',
28
  },
29
 
30
  // Search container
31
  search: {
32
  input:
33
+ 'w-full h-10 pl-10 pr-4 rounded-lg text-sm bg-bolt-elements-hover dark:bg-bolt-elements-hoverDark border border-bolt-elements-border dark:border-bolt-elements-borderDark text-bolt-elements-textPrimary dark:text-bolt-elements-textPrimaryDark placeholder-bolt-elements-textTertiary focus:outline-none focus:ring-1 focus:ring-purple-500 transition-all',
34
+ },
35
+
36
+ // Scroll container styles
37
+ scroll: {
38
+ container: 'overflow-y-auto overscroll-y-contain',
39
+ content: 'min-h-full',
40
  },
41
 
42
  'loading-spinner': 'i-ph:spinner-gap-bold animate-spin w-4 h-4',
app/components/settings/settings.types.ts CHANGED
@@ -1,15 +1,20 @@
1
  import type { ReactNode } from 'react';
2
 
3
  export type SettingCategory = 'profile' | 'file_sharing' | 'connectivity' | 'system' | 'services' | 'preferences';
 
4
  export type TabType =
5
  | 'profile'
 
 
 
6
  | 'data'
7
  | 'providers'
8
- | 'features'
9
  | 'debug'
10
  | 'event-logs'
11
- | 'connection'
12
- | 'preferences';
 
13
 
14
  export interface UserProfile {
15
  name: string;
@@ -34,6 +39,60 @@ export interface SettingItem {
34
  keywords?: string[];
35
  }
36
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
37
  export const categoryLabels: Record<SettingCategory, string> = {
38
  profile: 'Profile & Account',
39
  file_sharing: 'File Sharing',
 
1
  import type { ReactNode } from 'react';
2
 
3
  export type SettingCategory = 'profile' | 'file_sharing' | 'connectivity' | 'system' | 'services' | 'preferences';
4
+
5
  export type TabType =
6
  | 'profile'
7
+ | 'settings'
8
+ | 'notifications'
9
+ | 'features'
10
  | 'data'
11
  | 'providers'
12
+ | 'connection'
13
  | 'debug'
14
  | 'event-logs'
15
+ | 'update';
16
+
17
+ export type WindowType = 'user' | 'developer';
18
 
19
  export interface UserProfile {
20
  name: string;
 
39
  keywords?: string[];
40
  }
41
 
42
+ export interface TabVisibilityConfig {
43
+ id: TabType;
44
+ visible: boolean;
45
+ window: 'user' | 'developer';
46
+ order: number;
47
+ locked?: boolean;
48
+ }
49
+
50
+ export interface TabWindowConfig {
51
+ userTabs: TabVisibilityConfig[];
52
+ developerTabs: TabVisibilityConfig[];
53
+ }
54
+
55
+ export const TAB_LABELS: Record<TabType, string> = {
56
+ profile: 'Profile',
57
+ settings: 'Settings',
58
+ notifications: 'Notifications',
59
+ features: 'Features',
60
+ data: 'Data',
61
+ providers: 'Providers',
62
+ connection: 'Connection',
63
+ debug: 'Debug',
64
+ 'event-logs': 'Event Logs',
65
+ update: 'Update',
66
+ };
67
+
68
+ export const DEFAULT_TAB_CONFIG: TabVisibilityConfig[] = [
69
+ // User Window Tabs (Visible by default)
70
+ { id: 'features', visible: true, window: 'user', order: 0 },
71
+ { id: 'data', visible: true, window: 'user', order: 1 },
72
+ { id: 'providers', visible: true, window: 'user', order: 2 },
73
+ { id: 'connection', visible: true, window: 'user', order: 3 },
74
+ { id: 'debug', visible: true, window: 'user', order: 4 },
75
+
76
+ // User Window Tabs (Hidden by default)
77
+ { id: 'profile', visible: false, window: 'user', order: 5 },
78
+ { id: 'settings', visible: false, window: 'user', order: 6 },
79
+ { id: 'notifications', visible: false, window: 'user', order: 7 },
80
+ { id: 'event-logs', visible: false, window: 'user', order: 8 },
81
+ { id: 'update', visible: false, window: 'user', order: 9 },
82
+
83
+ // Developer Window Tabs (All visible by default)
84
+ { id: 'profile', visible: true, window: 'developer', order: 0 },
85
+ { id: 'settings', visible: true, window: 'developer', order: 1 },
86
+ { id: 'notifications', visible: true, window: 'developer', order: 2 },
87
+ { id: 'features', visible: true, window: 'developer', order: 3 },
88
+ { id: 'data', visible: true, window: 'developer', order: 4 },
89
+ { id: 'providers', visible: true, window: 'developer', order: 5 },
90
+ { id: 'connection', visible: true, window: 'developer', order: 6 },
91
+ { id: 'debug', visible: true, window: 'developer', order: 7 },
92
+ { id: 'event-logs', visible: true, window: 'developer', order: 8 },
93
+ { id: 'update', visible: true, window: 'developer', order: 9 },
94
+ ];
95
+
96
  export const categoryLabels: Record<SettingCategory, string> = {
97
  profile: 'Profile & Account',
98
  file_sharing: 'File Sharing',
app/components/settings/settings/SettingsTab.tsx ADDED
@@ -0,0 +1,215 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState, useEffect } from 'react';
2
+ import { motion } from 'framer-motion';
3
+ 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
+
9
+ export default function SettingsTab() {
10
+ const [currentTimezone, setCurrentTimezone] = useState('');
11
+ const [settings, setSettings] = useState<UserProfile>(() => {
12
+ const saved = localStorage.getItem('bolt_user_profile');
13
+ return saved
14
+ ? JSON.parse(saved)
15
+ : {
16
+ theme: 'system',
17
+ notifications: true,
18
+ language: 'en',
19
+ timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
20
+ };
21
+ });
22
+
23
+ useEffect(() => {
24
+ setCurrentTimezone(Intl.DateTimeFormat().resolvedOptions().timeZone);
25
+ }, []);
26
+
27
+ // Apply theme when settings changes
28
+ useEffect(() => {
29
+ if (settings.theme === 'system') {
30
+ // Remove theme override
31
+ localStorage.removeItem(kTheme);
32
+
33
+ const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
34
+ document.querySelector('html')?.setAttribute('data-theme', prefersDark ? 'dark' : 'light');
35
+ } else {
36
+ // Set specific theme
37
+ localStorage.setItem(kTheme, settings.theme);
38
+ document.querySelector('html')?.setAttribute('data-theme', settings.theme);
39
+ themeStore.set(settings.theme);
40
+ }
41
+ }, [settings.theme]);
42
+
43
+ // Save settings automatically when they change
44
+ useEffect(() => {
45
+ try {
46
+ // Get existing profile data
47
+ const existingProfile = JSON.parse(localStorage.getItem('bolt_user_profile') || '{}');
48
+
49
+ // Merge with new settings
50
+ const updatedProfile = {
51
+ ...existingProfile,
52
+ theme: settings.theme,
53
+ notifications: settings.notifications,
54
+ language: settings.language,
55
+ timezone: settings.timezone,
56
+ };
57
+
58
+ localStorage.setItem('bolt_user_profile', JSON.stringify(updatedProfile));
59
+ toast.success('Settings updated');
60
+ } catch (error) {
61
+ console.error('Error saving settings:', error);
62
+ toast.error('Failed to update settings');
63
+ }
64
+ }, [settings]);
65
+
66
+ return (
67
+ <div className="space-y-4">
68
+ {/* Theme & Language */}
69
+ <motion.div
70
+ className="bg-white dark:bg-[#0A0A0A] rounded-lg shadow-sm dark:shadow-none p-4 space-y-4"
71
+ initial={{ opacity: 0, y: 20 }}
72
+ animate={{ opacity: 1, y: 0 }}
73
+ transition={{ delay: 0.1 }}
74
+ >
75
+ <div className="flex items-center gap-2 mb-4">
76
+ <div className="i-ph:palette-fill w-4 h-4 text-purple-500" />
77
+ <span className="text-sm font-medium text-bolt-elements-textPrimary">Appearance</span>
78
+ </div>
79
+
80
+ <div>
81
+ <div className="flex items-center gap-2 mb-2">
82
+ <div className="i-ph:paint-brush-fill w-4 h-4 text-bolt-elements-textSecondary" />
83
+ <label className="block text-sm text-bolt-elements-textSecondary">Theme</label>
84
+ </div>
85
+ <div className="flex gap-2">
86
+ {(['light', 'dark', 'system'] as const).map((theme) => (
87
+ <button
88
+ key={theme}
89
+ onClick={() => setSettings((prev) => ({ ...prev, theme }))}
90
+ className={classNames(
91
+ 'px-3 py-1.5 rounded-lg text-sm flex items-center gap-2 transition-colors',
92
+ settings.theme === theme
93
+ ? 'bg-purple-500 text-white hover:bg-purple-600'
94
+ : 'bg-[#F5F5F5] dark:bg-[#1A1A1A] text-bolt-elements-textSecondary hover:bg-[#E5E5E5] dark:hover:bg-[#252525] hover:text-bolt-elements-textPrimary',
95
+ )}
96
+ >
97
+ <div
98
+ className={`w-4 h-4 ${
99
+ theme === 'light'
100
+ ? 'i-ph:sun-fill'
101
+ : theme === 'dark'
102
+ ? 'i-ph:moon-stars-fill'
103
+ : 'i-ph:monitor-fill'
104
+ }`}
105
+ />
106
+ <span className="capitalize">{theme}</span>
107
+ </button>
108
+ ))}
109
+ </div>
110
+ </div>
111
+
112
+ <div>
113
+ <div className="flex items-center gap-2 mb-2">
114
+ <div className="i-ph:translate-fill w-4 h-4 text-bolt-elements-textSecondary" />
115
+ <label className="block text-sm text-bolt-elements-textSecondary">Language</label>
116
+ </div>
117
+ <select
118
+ value={settings.language}
119
+ onChange={(e) => setSettings((prev) => ({ ...prev, language: e.target.value }))}
120
+ className={classNames(
121
+ 'w-full px-3 py-2 rounded-lg text-sm',
122
+ 'bg-[#F5F5F5] dark:bg-[#1A1A1A]',
123
+ 'border border-[#E5E5E5] dark:border-[#1A1A1A]',
124
+ 'text-bolt-elements-textPrimary',
125
+ 'focus:outline-none focus:ring-2 focus:ring-purple-500/30',
126
+ 'transition-all duration-200',
127
+ )}
128
+ >
129
+ <option value="en">English</option>
130
+ <option value="es">Español</option>
131
+ <option value="fr">Français</option>
132
+ <option value="de">Deutsch</option>
133
+ <option value="it">Italiano</option>
134
+ <option value="pt">Português</option>
135
+ <option value="ru">Русский</option>
136
+ <option value="zh">中文</option>
137
+ <option value="ja">日本語</option>
138
+ <option value="ko">한국어</option>
139
+ </select>
140
+ </div>
141
+
142
+ <div>
143
+ <div className="flex items-center gap-2 mb-2">
144
+ <div className="i-ph:bell-fill w-4 h-4 text-bolt-elements-textSecondary" />
145
+ <label className="block text-sm text-bolt-elements-textSecondary">Notifications</label>
146
+ </div>
147
+ <div className="flex items-center justify-between">
148
+ <span className="text-sm text-bolt-elements-textSecondary">
149
+ {settings.notifications ? 'Notifications are enabled' : 'Notifications are disabled'}
150
+ </span>
151
+ <Switch
152
+ checked={settings.notifications}
153
+ onCheckedChange={(checked) => {
154
+ // Update local state
155
+ setSettings((prev) => ({ ...prev, notifications: checked }));
156
+
157
+ // Update localStorage immediately
158
+ const existingProfile = JSON.parse(localStorage.getItem('bolt_user_profile') || '{}');
159
+ const updatedProfile = {
160
+ ...existingProfile,
161
+ notifications: checked,
162
+ };
163
+ localStorage.setItem('bolt_user_profile', JSON.stringify(updatedProfile));
164
+
165
+ // Dispatch storage event for other components
166
+ window.dispatchEvent(
167
+ new StorageEvent('storage', {
168
+ key: 'bolt_user_profile',
169
+ newValue: JSON.stringify(updatedProfile),
170
+ }),
171
+ );
172
+
173
+ toast.success(`Notifications ${checked ? 'enabled' : 'disabled'}`);
174
+ }}
175
+ />
176
+ </div>
177
+ </div>
178
+ </motion.div>
179
+
180
+ {/* Timezone */}
181
+ <motion.div
182
+ className="bg-white dark:bg-[#0A0A0A] rounded-lg shadow-sm dark:shadow-none p-4"
183
+ initial={{ opacity: 0, y: 20 }}
184
+ animate={{ opacity: 1, y: 0 }}
185
+ transition={{ delay: 0.2 }}
186
+ >
187
+ <div className="flex items-center gap-2 mb-4">
188
+ <div className="i-ph:clock-fill w-4 h-4 text-purple-500" />
189
+ <span className="text-sm font-medium text-bolt-elements-textPrimary">Time Settings</span>
190
+ </div>
191
+
192
+ <div>
193
+ <div className="flex items-center gap-2 mb-2">
194
+ <div className="i-ph:globe-fill w-4 h-4 text-bolt-elements-textSecondary" />
195
+ <label className="block text-sm text-bolt-elements-textSecondary">Timezone</label>
196
+ </div>
197
+ <select
198
+ value={settings.timezone}
199
+ onChange={(e) => setSettings((prev) => ({ ...prev, timezone: e.target.value }))}
200
+ className={classNames(
201
+ 'w-full px-3 py-2 rounded-lg text-sm',
202
+ 'bg-[#F5F5F5] dark:bg-[#1A1A1A]',
203
+ 'border border-[#E5E5E5] dark:border-[#1A1A1A]',
204
+ 'text-bolt-elements-textPrimary',
205
+ 'focus:outline-none focus:ring-2 focus:ring-purple-500/30',
206
+ 'transition-all duration-200',
207
+ )}
208
+ >
209
+ <option value={currentTimezone}>{currentTimezone}</option>
210
+ </select>
211
+ </div>
212
+ </motion.div>
213
+ </div>
214
+ );
215
+ }
app/components/settings/shared/DraggableTabList.tsx ADDED
@@ -0,0 +1,158 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 {
9
+ tabs: TabVisibilityConfig[];
10
+ onReorder: (tabs: TabVisibilityConfig[]) => void;
11
+ onWindowChange?: (tab: TabVisibilityConfig, window: 'user' | 'developer') => void;
12
+ onVisibilityChange?: (tab: TabVisibilityConfig, visible: boolean) => void;
13
+ showControls?: boolean;
14
+ }
15
+
16
+ interface DraggableTabItemProps {
17
+ tab: TabVisibilityConfig;
18
+ index: number;
19
+ moveTab: (dragIndex: number, hoverIndex: number) => void;
20
+ showControls?: boolean;
21
+ onWindowChange?: (tab: TabVisibilityConfig, window: 'user' | 'developer') => void;
22
+ onVisibilityChange?: (tab: TabVisibilityConfig, visible: boolean) => void;
23
+ }
24
+
25
+ interface DragItem {
26
+ type: string;
27
+ index: number;
28
+ id: string;
29
+ }
30
+
31
+ const DraggableTabItem = ({
32
+ tab,
33
+ index,
34
+ moveTab,
35
+ showControls,
36
+ onWindowChange,
37
+ onVisibilityChange,
38
+ }: DraggableTabItemProps) => {
39
+ const [{ isDragging }, drag] = useDrag({
40
+ type: 'tab',
41
+ item: { type: 'tab', index, id: tab.id },
42
+ collect: (monitor) => ({
43
+ isDragging: monitor.isDragging(),
44
+ }),
45
+ });
46
+
47
+ const [, drop] = useDrop({
48
+ accept: 'tab',
49
+ hover: (item: DragItem, monitor) => {
50
+ if (!monitor.isOver({ shallow: true })) {
51
+ return;
52
+ }
53
+
54
+ if (item.index === index) {
55
+ return;
56
+ }
57
+
58
+ if (item.id === tab.id) {
59
+ return;
60
+ }
61
+
62
+ moveTab(item.index, index);
63
+ item.index = index;
64
+ },
65
+ });
66
+
67
+ return (
68
+ <motion.div
69
+ ref={(node) => drag(drop(node))}
70
+ initial={false}
71
+ animate={{
72
+ scale: isDragging ? 1.02 : 1,
73
+ boxShadow: isDragging ? '0 8px 16px rgba(0,0,0,0.1)' : 'none',
74
+ }}
75
+ className={classNames(
76
+ 'flex items-center justify-between p-4 rounded-lg',
77
+ 'bg-[#F5F5F5] dark:bg-[#1A1A1A]',
78
+ 'border border-[#E5E5E5] dark:border-[#333333]',
79
+ isDragging ? 'z-50' : '',
80
+ )}
81
+ >
82
+ <div className="flex items-center gap-4">
83
+ <div className="cursor-grab">
84
+ <div className="i-ph:dots-six-vertical w-4 h-4 text-bolt-elements-textSecondary" />
85
+ </div>
86
+ <div>
87
+ <div className="font-medium text-bolt-elements-textPrimary">{TAB_LABELS[tab.id]}</div>
88
+ {showControls && (
89
+ <div className="text-xs text-bolt-elements-textSecondary">
90
+ Order: {tab.order}, Window: {tab.window}
91
+ </div>
92
+ )}
93
+ </div>
94
+ </div>
95
+ {showControls && !tab.locked && (
96
+ <div className="flex items-center gap-4">
97
+ <div className="flex items-center gap-2">
98
+ <Switch
99
+ checked={tab.visible}
100
+ onCheckedChange={(checked: boolean) => onVisibilityChange?.(tab, checked)}
101
+ className="data-[state=checked]:bg-purple-500"
102
+ aria-label={`Toggle ${TAB_LABELS[tab.id]} visibility`}
103
+ />
104
+ <label className="text-sm text-bolt-elements-textSecondary">Visible</label>
105
+ </div>
106
+ <div className="flex items-center gap-2">
107
+ <label className="text-sm text-bolt-elements-textSecondary">User</label>
108
+ <Switch
109
+ checked={tab.window === 'developer'}
110
+ onCheckedChange={(checked: boolean) => onWindowChange?.(tab, checked ? 'developer' : 'user')}
111
+ className="data-[state=checked]:bg-purple-500"
112
+ aria-label={`Toggle ${TAB_LABELS[tab.id]} window assignment`}
113
+ />
114
+ <label className="text-sm text-bolt-elements-textSecondary">Dev</label>
115
+ </div>
116
+ </div>
117
+ )}
118
+ </motion.div>
119
+ );
120
+ };
121
+
122
+ export const DraggableTabList = ({
123
+ tabs,
124
+ onReorder,
125
+ onWindowChange,
126
+ onVisibilityChange,
127
+ showControls = false,
128
+ }: DraggableTabListProps) => {
129
+ const moveTab = (dragIndex: number, hoverIndex: number) => {
130
+ const items = Array.from(tabs);
131
+ const [reorderedItem] = items.splice(dragIndex, 1);
132
+ items.splice(hoverIndex, 0, reorderedItem);
133
+
134
+ // Update order numbers based on position
135
+ const reorderedTabs = items.map((tab, index) => ({
136
+ ...tab,
137
+ order: index + 1,
138
+ }));
139
+
140
+ onReorder(reorderedTabs);
141
+ };
142
+
143
+ return (
144
+ <div className="space-y-2">
145
+ {tabs.map((tab, index) => (
146
+ <DraggableTabItem
147
+ key={tab.id}
148
+ tab={tab}
149
+ index={index}
150
+ moveTab={moveTab}
151
+ showControls={showControls}
152
+ onWindowChange={onWindowChange}
153
+ onVisibilityChange={onVisibilityChange}
154
+ />
155
+ ))}
156
+ </div>
157
+ );
158
+ };
app/components/settings/shared/TabTile.tsx ADDED
@@ -0,0 +1,230 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ };
19
+
20
+ interface TabTileProps {
21
+ tab: TabVisibilityConfig;
22
+ onClick: () => void;
23
+ isActive?: boolean;
24
+ hasUpdate?: boolean;
25
+ statusMessage?: string;
26
+ description?: string;
27
+ isLoading?: boolean;
28
+ }
29
+
30
+ export const TabTile = ({
31
+ tab,
32
+ onClick,
33
+ isActive = false,
34
+ hasUpdate = false,
35
+ statusMessage,
36
+ description,
37
+ isLoading = false,
38
+ }: TabTileProps) => {
39
+ return (
40
+ <Tooltip.Provider delayDuration={200}>
41
+ <Tooltip.Root>
42
+ <Tooltip.Trigger asChild>
43
+ <motion.button
44
+ onClick={onClick}
45
+ disabled={isLoading}
46
+ className={classNames(
47
+ 'relative flex flex-col items-center justify-center gap-3 p-6 rounded-xl',
48
+ 'w-full h-full min-h-[160px]',
49
+
50
+ // Background and border styles
51
+ 'bg-white dark:bg-[#141414]',
52
+ 'border border-[#E5E5E5]/50 dark:border-[#333333]/50',
53
+
54
+ // Shadow and glass effect
55
+ 'shadow-sm backdrop-blur-sm',
56
+ 'dark:shadow-[0_0_15px_rgba(0,0,0,0.1)]',
57
+ 'dark:bg-opacity-50',
58
+
59
+ // Hover effects
60
+ 'hover:border-purple-500/30 dark:hover:border-purple-500/30',
61
+ 'hover:bg-gradient-to-br hover:from-purple-50/50 hover:to-white dark:hover:from-purple-500/5 dark:hover:to-[#141414]',
62
+ 'hover:shadow-md hover:shadow-purple-500/5',
63
+ 'dark:hover:shadow-purple-500/10',
64
+
65
+ // Focus states for keyboard navigation
66
+ 'focus:outline-none',
67
+ 'focus:ring-2 focus:ring-purple-500/50 focus:ring-offset-2',
68
+ 'dark:focus:ring-offset-[#141414]',
69
+ 'focus:border-purple-500/30',
70
+
71
+ // Active state
72
+ isActive
73
+ ? [
74
+ 'border-purple-500/50 dark:border-purple-500/50',
75
+ 'bg-gradient-to-br from-purple-50 to-white dark:from-purple-500/10 dark:to-[#141414]',
76
+ 'shadow-md shadow-purple-500/10',
77
+ ]
78
+ : '',
79
+
80
+ // Loading state
81
+ isLoading ? 'cursor-wait opacity-70' : '',
82
+
83
+ // Transitions
84
+ 'transition-all duration-300 ease-out',
85
+ 'group',
86
+ )}
87
+ whileHover={
88
+ !isLoading
89
+ ? {
90
+ scale: 1.02,
91
+ transition: { duration: 0.2, ease: 'easeOut' },
92
+ }
93
+ : {}
94
+ }
95
+ whileTap={
96
+ !isLoading
97
+ ? {
98
+ scale: 0.98,
99
+ transition: { duration: 0.1, ease: 'easeIn' },
100
+ }
101
+ : {}
102
+ }
103
+ >
104
+ {/* Loading Overlay */}
105
+ {isLoading && (
106
+ <motion.div
107
+ className={classNames(
108
+ 'absolute inset-0 rounded-xl z-10',
109
+ 'bg-white/50 dark:bg-black/50',
110
+ 'backdrop-blur-sm',
111
+ 'flex items-center justify-center',
112
+ )}
113
+ initial={{ opacity: 0 }}
114
+ animate={{ opacity: 1 }}
115
+ transition={{ duration: 0.2 }}
116
+ >
117
+ <motion.div
118
+ className={classNames('w-8 h-8 rounded-full', 'border-2 border-purple-500/30', 'border-t-purple-500')}
119
+ animate={{ rotate: 360 }}
120
+ transition={{
121
+ duration: 1,
122
+ repeat: Infinity,
123
+ ease: 'linear',
124
+ }}
125
+ />
126
+ </motion.div>
127
+ )}
128
+
129
+ {/* Status Indicator */}
130
+ {hasUpdate && (
131
+ <motion.div
132
+ className={classNames(
133
+ 'absolute top-3 right-3',
134
+ 'w-2.5 h-2.5 rounded-full',
135
+ 'bg-green-500',
136
+ 'shadow-lg shadow-green-500/20',
137
+ 'ring-4 ring-green-500/20',
138
+ )}
139
+ initial={{ scale: 0 }}
140
+ animate={{ scale: 1 }}
141
+ transition={{ type: 'spring', bounce: 0.5 }}
142
+ />
143
+ )}
144
+
145
+ {/* Background glow effect */}
146
+ <div
147
+ className={classNames(
148
+ 'absolute inset-0 rounded-xl opacity-0 group-hover:opacity-100',
149
+ 'bg-gradient-to-br from-purple-500/5 to-transparent dark:from-purple-500/10',
150
+ 'transition-opacity duration-300',
151
+ isActive ? 'opacity-100' : '',
152
+ )}
153
+ />
154
+
155
+ {/* Icon */}
156
+ <div
157
+ className={classNames(
158
+ TAB_ICONS[tab.id],
159
+ 'w-12 h-12',
160
+ 'relative',
161
+ 'text-gray-600 dark:text-gray-300',
162
+ 'group-hover:text-purple-500 dark:group-hover:text-purple-400',
163
+ 'transition-all duration-300',
164
+ isActive ? 'text-purple-500 dark:text-purple-400 scale-110' : '',
165
+ )}
166
+ />
167
+
168
+ {/* Label and Description */}
169
+ <div className="relative flex flex-col items-center text-center">
170
+ <div
171
+ className={classNames(
172
+ 'text-base font-medium',
173
+ 'text-gray-700 dark:text-gray-200',
174
+ 'group-hover:text-purple-500 dark:group-hover:text-purple-400',
175
+ 'transition-colors duration-300',
176
+ isActive ? 'text-purple-500 dark:text-purple-400' : '',
177
+ )}
178
+ >
179
+ {TAB_LABELS[tab.id]}
180
+ </div>
181
+ {description && (
182
+ <div
183
+ className={classNames(
184
+ 'text-xs mt-1',
185
+ 'text-gray-500 dark:text-gray-400',
186
+ 'group-hover:text-purple-400/70 dark:group-hover:text-purple-300/70',
187
+ 'transition-colors duration-300',
188
+ 'max-w-[180px]',
189
+ isActive ? 'text-purple-400/70 dark:text-purple-300/70' : '',
190
+ )}
191
+ >
192
+ {description}
193
+ </div>
194
+ )}
195
+ </div>
196
+
197
+ {/* Bottom indicator line */}
198
+ <div
199
+ className={classNames(
200
+ 'absolute bottom-0 left-1/2 -translate-x-1/2',
201
+ 'w-12 h-0.5 rounded-full',
202
+ 'bg-purple-500/0 group-hover:bg-purple-500/50',
203
+ 'transition-all duration-300 ease-out',
204
+ 'transform scale-x-0 group-hover:scale-x-100',
205
+ isActive ? 'bg-purple-500 scale-x-100' : '',
206
+ )}
207
+ />
208
+ </motion.button>
209
+ </Tooltip.Trigger>
210
+ <Tooltip.Portal>
211
+ <Tooltip.Content
212
+ className={classNames(
213
+ 'px-3 py-1.5 rounded-lg',
214
+ 'bg-[#18181B] text-white',
215
+ 'text-sm font-medium',
216
+ 'shadow-xl',
217
+ 'select-none',
218
+ 'z-[100]',
219
+ )}
220
+ side="top"
221
+ sideOffset={5}
222
+ >
223
+ {statusMessage || TAB_LABELS[tab.id]}
224
+ <Tooltip.Arrow className="fill-[#18181B]" />
225
+ </Tooltip.Content>
226
+ </Tooltip.Portal>
227
+ </Tooltip.Root>
228
+ </Tooltip.Provider>
229
+ );
230
+ };
app/components/settings/update/UpdateTab.tsx ADDED
@@ -0,0 +1,217 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState, useEffect } from 'react';
2
+ import { motion } from 'framer-motion';
3
+ import { useSettings } from '~/lib/hooks/useSettings';
4
+ import { logStore } from '~/lib/stores/logs';
5
+ import { classNames } from '~/utils/classNames';
6
+
7
+ interface GitHubCommitResponse {
8
+ sha: string;
9
+ }
10
+
11
+ interface UpdateInfo {
12
+ currentVersion: string;
13
+ latestVersion: string;
14
+ branch: string;
15
+ hasUpdate: boolean;
16
+ }
17
+
18
+ const GITHUB_URLS = {
19
+ commitJson: async (branch: string): Promise<UpdateInfo> => {
20
+ try {
21
+ const response = await fetch(`https://api.github.com/repos/stackblitz-labs/bolt.diy/commits/${branch}`);
22
+ const data = (await response.json()) as GitHubCommitResponse;
23
+
24
+ const currentCommitHash = __COMMIT_HASH;
25
+ const remoteCommitHash = data.sha.slice(0, 7);
26
+
27
+ return {
28
+ currentVersion: currentCommitHash,
29
+ latestVersion: remoteCommitHash,
30
+ branch,
31
+ hasUpdate: remoteCommitHash !== currentCommitHash,
32
+ };
33
+ } catch (error) {
34
+ console.error('Failed to fetch commit info:', error);
35
+ throw new Error('Failed to fetch commit info');
36
+ }
37
+ },
38
+ };
39
+
40
+ const UpdateTab = () => {
41
+ const { isLatestBranch } = useSettings();
42
+ const [updateInfo, setUpdateInfo] = useState<UpdateInfo | null>(null);
43
+ const [isChecking, setIsChecking] = useState(false);
44
+ const [error, setError] = useState<string | null>(null);
45
+
46
+ const checkForUpdates = async () => {
47
+ setIsChecking(true);
48
+ setError(null);
49
+
50
+ try {
51
+ const branchToCheck = isLatestBranch ? 'main' : 'stable';
52
+ const info = await GITHUB_URLS.commitJson(branchToCheck);
53
+ setUpdateInfo(info);
54
+
55
+ if (info.hasUpdate) {
56
+ // Add update notification only if it doesn't already exist
57
+ const existingLogs = Object.values(logStore.logs.get());
58
+ const hasUpdateNotification = existingLogs.some(
59
+ (log) =>
60
+ log.level === 'warning' &&
61
+ log.details?.type === 'update' &&
62
+ log.details.latestVersion === info.latestVersion,
63
+ );
64
+
65
+ if (!hasUpdateNotification) {
66
+ logStore.logWarning('Update Available', {
67
+ currentVersion: info.currentVersion,
68
+ latestVersion: info.latestVersion,
69
+ branch: branchToCheck,
70
+ type: 'update',
71
+ message: `A new version is available on the ${branchToCheck} branch`,
72
+ updateUrl: `https://github.com/stackblitz-labs/bolt.diy/compare/${info.currentVersion}...${info.latestVersion}`,
73
+ });
74
+ }
75
+ }
76
+ } catch (err) {
77
+ setError('Failed to check for updates. Please try again later.');
78
+ console.error('Update check failed:', err);
79
+ } finally {
80
+ setIsChecking(false);
81
+ }
82
+ };
83
+
84
+ useEffect(() => {
85
+ checkForUpdates();
86
+ }, [isLatestBranch]);
87
+
88
+ const handleViewChanges = () => {
89
+ if (updateInfo) {
90
+ window.open(
91
+ `https://github.com/stackblitz-labs/bolt.diy/compare/${updateInfo.currentVersion}...${updateInfo.latestVersion}`,
92
+ '_blank',
93
+ );
94
+ }
95
+ };
96
+
97
+ return (
98
+ <div className="flex flex-col gap-6">
99
+ <motion.div
100
+ className="flex items-center gap-3"
101
+ initial={{ opacity: 0, y: -20 }}
102
+ animate={{ opacity: 1, y: 0 }}
103
+ transition={{ duration: 0.3 }}
104
+ >
105
+ <div className="i-ph:arrow-circle-up text-xl text-purple-500" />
106
+ <div>
107
+ <h3 className="text-lg font-medium text-bolt-elements-textPrimary">Updates</h3>
108
+ <p className="text-sm text-bolt-elements-textSecondary">Check for and manage application updates</p>
109
+ </div>
110
+ </motion.div>
111
+
112
+ <motion.div
113
+ className="flex flex-col gap-4"
114
+ initial={{ opacity: 0, y: 20 }}
115
+ animate={{ opacity: 1, y: 0 }}
116
+ transition={{ duration: 0.3, delay: 0.1 }}
117
+ >
118
+ <div className="flex items-center justify-between">
119
+ <div className="flex items-center gap-4">
120
+ <span className="text-sm text-bolt-elements-textSecondary">
121
+ Currently on {isLatestBranch ? 'main' : 'stable'} branch
122
+ </span>
123
+ {updateInfo && (
124
+ <span className="text-xs text-bolt-elements-textTertiary">Version: {updateInfo.currentVersion}</span>
125
+ )}
126
+ </div>
127
+ <button
128
+ onClick={checkForUpdates}
129
+ disabled={isChecking}
130
+ className={classNames(
131
+ 'px-3 py-2 rounded-lg text-sm',
132
+ 'bg-bolt-elements-background-depth-2 border border-bolt-elements-borderColor',
133
+ 'text-bolt-elements-textPrimary',
134
+ 'hover:bg-bolt-elements-background-depth-3',
135
+ 'focus:outline-none focus:ring-2 focus:ring-purple-500/30',
136
+ 'transition-all duration-200',
137
+ 'disabled:opacity-50 disabled:cursor-not-allowed',
138
+ )}
139
+ >
140
+ <div className="flex items-center gap-2">
141
+ <div className={classNames('i-ph:arrows-clockwise', isChecking ? 'animate-spin' : '')} />
142
+ {isChecking ? 'Checking...' : 'Check for Updates'}
143
+ </div>
144
+ </button>
145
+ </div>
146
+
147
+ {error && (
148
+ <div className="p-4 rounded-lg bg-red-50 border border-red-200 text-red-700 dark:bg-red-900/20 dark:border-red-900/50 dark:text-red-400">
149
+ <div className="flex items-center gap-2">
150
+ <div className="i-ph:warning-circle" />
151
+ {error}
152
+ </div>
153
+ </div>
154
+ )}
155
+
156
+ {updateInfo && (
157
+ <div
158
+ className={classNames(
159
+ 'p-4 rounded-lg border',
160
+ updateInfo.hasUpdate
161
+ ? 'bg-yellow-50 border-yellow-200 dark:bg-yellow-900/20 dark:border-yellow-900/50'
162
+ : 'bg-green-50 border-green-200 dark:bg-green-900/20 dark:border-green-900/50',
163
+ )}
164
+ >
165
+ <div className="flex items-start justify-between gap-4">
166
+ <div className="flex items-center gap-3">
167
+ <span
168
+ className={classNames(
169
+ 'text-lg',
170
+ updateInfo.hasUpdate
171
+ ? 'i-ph:warning text-yellow-600 dark:text-yellow-400'
172
+ : 'i-ph:check-circle text-green-600 dark:text-green-400',
173
+ )}
174
+ />
175
+ <div>
176
+ <h3
177
+ className={classNames(
178
+ 'text-sm font-medium',
179
+ updateInfo.hasUpdate
180
+ ? 'text-yellow-900 dark:text-yellow-300'
181
+ : 'text-green-900 dark:text-green-300',
182
+ )}
183
+ >
184
+ {updateInfo.hasUpdate ? 'Update Available' : 'Up to Date'}
185
+ </h3>
186
+ <p className="text-sm text-bolt-elements-textSecondary mt-1">
187
+ {updateInfo.hasUpdate
188
+ ? `A new version is available on the ${updateInfo.branch} branch`
189
+ : 'You are running the latest version'}
190
+ </p>
191
+ {updateInfo.hasUpdate && (
192
+ <div className="mt-2 flex flex-col gap-1 text-xs text-bolt-elements-textTertiary">
193
+ <p>Current Version: {updateInfo.currentVersion}</p>
194
+ <p>Latest Version: {updateInfo.latestVersion}</p>
195
+ <p>Branch: {updateInfo.branch}</p>
196
+ </div>
197
+ )}
198
+ </div>
199
+ </div>
200
+ {updateInfo.hasUpdate && (
201
+ <button
202
+ onClick={handleViewChanges}
203
+ className="shrink-0 inline-flex items-center gap-2 rounded-md bg-blue-50 px-3 py-1.5 text-sm font-medium text-blue-600 hover:bg-blue-100 dark:bg-blue-900/20 dark:text-blue-400 dark:hover:bg-blue-900/30"
204
+ >
205
+ <span className="i-ph:git-branch text-lg" />
206
+ View Changes
207
+ </button>
208
+ )}
209
+ </div>
210
+ </div>
211
+ )}
212
+ </motion.div>
213
+ </div>
214
+ );
215
+ };
216
+
217
+ export default UpdateTab;
app/components/settings/user/ProfileHeader.tsx ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { Dispatch, SetStateAction } from 'react';
2
+ import type { TabType, TabVisibilityConfig } from '~/components/settings/settings.types';
3
+
4
+ export interface ProfileHeaderProps {
5
+ onNavigate: Dispatch<SetStateAction<TabType | null>>;
6
+ visibleTabs: TabVisibilityConfig[];
7
+ }
8
+
9
+ export { type TabType };
10
+
11
+ export const ProfileHeader = ({ onNavigate, visibleTabs }: ProfileHeaderProps) => {
12
+ return (
13
+ <div className="flex items-center gap-2">
14
+ {visibleTabs.map((tab) => (
15
+ <button
16
+ key={tab.id}
17
+ onClick={() => onNavigate(tab.id)}
18
+ className="text-bolt-elements-textSecondary hover:text-bolt-elements-textPrimary transition-colors"
19
+ >
20
+ {tab.id}
21
+ </button>
22
+ ))}
23
+ </div>
24
+ );
25
+ };
app/components/settings/user/UsersWindow.tsx ADDED
@@ -0,0 +1,385 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as RadixDialog from '@radix-ui/react-dialog';
2
+ import { motion } from 'framer-motion';
3
+ import { useState } from 'react';
4
+ import { classNames } from '~/utils/classNames';
5
+ import { DialogTitle } from '~/components/ui/Dialog';
6
+ import { Switch } from '~/components/ui/Switch';
7
+ import type { TabType, TabVisibilityConfig } from '~/components/settings/settings.types';
8
+ import { TAB_LABELS } from '~/components/settings/settings.types';
9
+ import { DeveloperWindow } from '~/components/settings/developer/DeveloperWindow';
10
+ import { TabTile } from '~/components/settings/shared/TabTile';
11
+ import { tabConfigurationStore, updateTabConfiguration } from '~/lib/stores/settings';
12
+ import { useStore } from '@nanostores/react';
13
+ import { DndProvider, useDrag, useDrop } from 'react-dnd';
14
+ import { HTML5Backend } from 'react-dnd-html5-backend';
15
+ import ProfileTab from '~/components/settings/profile/ProfileTab';
16
+ import SettingsTab from '~/components/settings/settings/SettingsTab';
17
+ import NotificationsTab from '~/components/settings/notifications/NotificationsTab';
18
+ import FeaturesTab from '~/components/settings/features/FeaturesTab';
19
+ import DataTab from '~/components/settings/data/DataTab';
20
+ import { ProvidersTab } from '~/components/settings/providers/ProvidersTab';
21
+ import DebugTab from '~/components/settings/debug/DebugTab';
22
+ import { EventLogsTab } from '~/components/settings/event-logs/EventLogsTab';
23
+ import UpdateTab from '~/components/settings/update/UpdateTab';
24
+ import ConnectionsTab from '~/components/settings/connections/ConnectionsTab';
25
+ import { useUpdateCheck } from '~/lib/hooks/useUpdateCheck';
26
+ import { useFeatures } from '~/lib/hooks/useFeatures';
27
+ import { useNotifications } from '~/lib/hooks/useNotifications';
28
+ import { useConnectionStatus } from '~/lib/hooks/useConnectionStatus';
29
+ import { useDebugStatus } from '~/lib/hooks/useDebugStatus';
30
+
31
+ interface DraggableTabTileProps {
32
+ tab: TabVisibilityConfig;
33
+ index: number;
34
+ moveTab: (dragIndex: number, hoverIndex: number) => void;
35
+ onClick: () => void;
36
+ isActive: boolean;
37
+ hasUpdate: boolean;
38
+ statusMessage: string;
39
+ description: string;
40
+ isLoading?: boolean;
41
+ }
42
+
43
+ const TAB_DESCRIPTIONS: Record<TabType, string> = {
44
+ profile: 'Manage your profile and account settings',
45
+ settings: 'Configure application preferences',
46
+ notifications: 'View and manage your notifications',
47
+ features: 'Explore new and upcoming features',
48
+ data: 'Manage your data and storage',
49
+ providers: 'Configure AI providers and models',
50
+ connection: 'Check connection status and settings',
51
+ debug: 'Debug tools and system information',
52
+ 'event-logs': 'View system events and logs',
53
+ update: 'Check for updates and release notes',
54
+ };
55
+
56
+ const DraggableTabTile = ({
57
+ tab,
58
+ index,
59
+ moveTab,
60
+ onClick,
61
+ isActive,
62
+ hasUpdate,
63
+ statusMessage,
64
+ description,
65
+ isLoading,
66
+ }: DraggableTabTileProps) => {
67
+ const [{ isDragging }, drag] = useDrag({
68
+ type: 'tab',
69
+ item: { index },
70
+ collect: (monitor) => ({
71
+ isDragging: monitor.isDragging(),
72
+ }),
73
+ });
74
+
75
+ const [, drop] = useDrop({
76
+ accept: 'tab',
77
+ hover: (item: { index: number }) => {
78
+ if (item.index === index) {
79
+ return;
80
+ }
81
+
82
+ moveTab(item.index, index);
83
+ item.index = index;
84
+ },
85
+ });
86
+
87
+ return (
88
+ <div ref={(node) => drag(drop(node))} style={{ opacity: isDragging ? 0.5 : 1 }}>
89
+ <TabTile
90
+ tab={tab}
91
+ onClick={onClick}
92
+ isActive={isActive}
93
+ hasUpdate={hasUpdate}
94
+ statusMessage={statusMessage}
95
+ description={description}
96
+ isLoading={isLoading}
97
+ />
98
+ </div>
99
+ );
100
+ };
101
+
102
+ interface UsersWindowProps {
103
+ open: boolean;
104
+ onClose: () => void;
105
+ }
106
+
107
+ export const UsersWindow = ({ open, onClose }: UsersWindowProps) => {
108
+ const [developerMode, setDeveloperMode] = useState(false);
109
+ const [activeTab, setActiveTab] = useState<TabType | null>(null);
110
+ const [loadingTab, setLoadingTab] = useState<TabType | null>(null);
111
+ const tabConfiguration = useStore(tabConfigurationStore);
112
+
113
+ // Status hooks
114
+ const { hasUpdate, currentVersion, acknowledgeUpdate } = useUpdateCheck();
115
+ const { hasNewFeatures, unviewedFeatures, acknowledgeAllFeatures } = useFeatures();
116
+ const { hasUnreadNotifications, unreadNotifications, markAllAsRead } = useNotifications();
117
+ const { hasConnectionIssues, currentIssue, acknowledgeIssue } = useConnectionStatus();
118
+ const { hasActiveWarnings, activeIssues, acknowledgeAllIssues } = useDebugStatus();
119
+
120
+ const handleDeveloperModeChange = (checked: boolean) => {
121
+ setDeveloperMode(checked);
122
+ };
123
+
124
+ const handleBack = () => {
125
+ setActiveTab(null);
126
+ };
127
+
128
+ // Only show tabs that are assigned to the user window AND are visible
129
+ const visibleUserTabs = tabConfiguration.userTabs
130
+ .filter((tab: TabVisibilityConfig) => tab.window === 'user' && tab.visible)
131
+ .sort((a: TabVisibilityConfig, b: TabVisibilityConfig) => (a.order || 0) - (b.order || 0));
132
+
133
+ const moveTab = (dragIndex: number, hoverIndex: number) => {
134
+ const draggedTab = visibleUserTabs[dragIndex];
135
+ const targetTab = visibleUserTabs[hoverIndex];
136
+
137
+ // Update the order of the dragged and target tabs
138
+ const updatedDraggedTab = { ...draggedTab, order: targetTab.order };
139
+ const updatedTargetTab = { ...targetTab, order: draggedTab.order };
140
+
141
+ // Update both tabs in the store
142
+ updateTabConfiguration(updatedDraggedTab);
143
+ updateTabConfiguration(updatedTargetTab);
144
+ };
145
+
146
+ const handleTabClick = async (tabId: TabType) => {
147
+ setLoadingTab(tabId);
148
+ setActiveTab(tabId);
149
+
150
+ // Acknowledge the status based on tab type
151
+ switch (tabId) {
152
+ case 'update':
153
+ await acknowledgeUpdate();
154
+ break;
155
+ case 'features':
156
+ await acknowledgeAllFeatures();
157
+ break;
158
+ case 'notifications':
159
+ await markAllAsRead();
160
+ break;
161
+ case 'connection':
162
+ acknowledgeIssue();
163
+ break;
164
+ case 'debug':
165
+ await acknowledgeAllIssues();
166
+ break;
167
+ }
168
+
169
+ // Simulate loading time (remove this in production)
170
+ await new Promise((resolve) => setTimeout(resolve, 1000));
171
+ setLoadingTab(null);
172
+ };
173
+
174
+ const getTabComponent = () => {
175
+ switch (activeTab) {
176
+ case 'profile':
177
+ return <ProfileTab />;
178
+ case 'settings':
179
+ return <SettingsTab />;
180
+ case 'notifications':
181
+ return <NotificationsTab />;
182
+ case 'features':
183
+ return <FeaturesTab />;
184
+ case 'data':
185
+ return <DataTab />;
186
+ case 'providers':
187
+ return <ProvidersTab />;
188
+ case 'connection':
189
+ return <ConnectionsTab />;
190
+ case 'debug':
191
+ return <DebugTab />;
192
+ case 'event-logs':
193
+ return <EventLogsTab />;
194
+ case 'update':
195
+ return <UpdateTab />;
196
+ default:
197
+ return null;
198
+ }
199
+ };
200
+
201
+ const getTabUpdateStatus = (tabId: TabType): boolean => {
202
+ switch (tabId) {
203
+ case 'update':
204
+ return hasUpdate;
205
+ case 'features':
206
+ return hasNewFeatures;
207
+ case 'notifications':
208
+ return hasUnreadNotifications;
209
+ case 'connection':
210
+ return hasConnectionIssues;
211
+ case 'debug':
212
+ return hasActiveWarnings;
213
+ default:
214
+ return false;
215
+ }
216
+ };
217
+
218
+ const getStatusMessage = (tabId: TabType): string => {
219
+ switch (tabId) {
220
+ case 'update':
221
+ return `New update available (v${currentVersion})`;
222
+ case 'features':
223
+ return `${unviewedFeatures.length} new feature${unviewedFeatures.length === 1 ? '' : 's'} to explore`;
224
+ case 'notifications':
225
+ return `${unreadNotifications.length} unread notification${unreadNotifications.length === 1 ? '' : 's'}`;
226
+ case 'connection':
227
+ return currentIssue === 'disconnected'
228
+ ? 'Connection lost'
229
+ : currentIssue === 'high-latency'
230
+ ? 'High latency detected'
231
+ : 'Connection issues detected';
232
+ case 'debug': {
233
+ const warnings = activeIssues.filter((i) => i.type === 'warning').length;
234
+ const errors = activeIssues.filter((i) => i.type === 'error').length;
235
+
236
+ return `${warnings} warning${warnings === 1 ? '' : 's'}, ${errors} error${errors === 1 ? '' : 's'}`;
237
+ }
238
+ default:
239
+ return '';
240
+ }
241
+ };
242
+
243
+ return (
244
+ <>
245
+ <DeveloperWindow open={developerMode} onClose={() => setDeveloperMode(false)} />
246
+ <DndProvider backend={HTML5Backend}>
247
+ <RadixDialog.Root open={open}>
248
+ <RadixDialog.Portal>
249
+ <div className="fixed inset-0 flex items-center justify-center z-[50]">
250
+ <RadixDialog.Overlay asChild>
251
+ <motion.div
252
+ className="absolute inset-0 bg-black/50 backdrop-blur-sm"
253
+ initial={{ opacity: 0 }}
254
+ animate={{ opacity: 1 }}
255
+ exit={{ opacity: 0 }}
256
+ transition={{ duration: 0.2 }}
257
+ />
258
+ </RadixDialog.Overlay>
259
+ <RadixDialog.Content aria-describedby={undefined} asChild>
260
+ <motion.div
261
+ className={classNames(
262
+ 'relative',
263
+ 'w-[1200px] h-[90vh]',
264
+ 'bg-[#FAFAFA] dark:bg-[#0A0A0A]',
265
+ 'rounded-2xl shadow-2xl',
266
+ 'border border-[#E5E5E5] dark:border-[#1A1A1A]',
267
+ 'flex flex-col overflow-hidden',
268
+ 'z-[51]',
269
+ )}
270
+ initial={{ opacity: 0, scale: 0.95, y: 20 }}
271
+ animate={{ opacity: 1, scale: 1, y: 0 }}
272
+ exit={{ opacity: 0, scale: 0.95, y: 20 }}
273
+ transition={{ duration: 0.2 }}
274
+ >
275
+ {/* Header */}
276
+ <div className="flex-none flex items-center justify-between px-6 py-4 border-b border-[#E5E5E5] dark:border-[#1A1A1A]">
277
+ <div className="flex items-center gap-3">
278
+ {activeTab ? (
279
+ <motion.button
280
+ onClick={handleBack}
281
+ className={classNames(
282
+ 'flex items-center justify-center w-8 h-8 rounded-lg',
283
+ 'bg-[#F5F5F5] dark:bg-[#1A1A1A]',
284
+ 'hover:bg-purple-500/10 dark:hover:bg-purple-500/20',
285
+ 'group transition-all duration-200',
286
+ )}
287
+ whileHover={{ scale: 1.05 }}
288
+ whileTap={{ scale: 0.95 }}
289
+ >
290
+ <div className="i-ph:arrow-left w-4 h-4 text-bolt-elements-textSecondary group-hover:text-purple-500 transition-colors" />
291
+ </motion.button>
292
+ ) : (
293
+ <motion.div
294
+ className="i-ph:lightning-fill w-5 h-5 text-purple-500"
295
+ initial={{ rotate: -10 }}
296
+ animate={{ rotate: 10 }}
297
+ transition={{
298
+ repeat: Infinity,
299
+ repeatType: 'reverse',
300
+ duration: 2,
301
+ ease: 'easeInOut',
302
+ }}
303
+ />
304
+ )}
305
+ <DialogTitle className="text-lg font-medium text-bolt-elements-textPrimary">
306
+ {activeTab ? TAB_LABELS[activeTab] : 'Bolt Control Panel'}
307
+ </DialogTitle>
308
+ </div>
309
+ <div className="flex items-center gap-3">
310
+ <div className="flex items-center gap-2">
311
+ <Switch
312
+ checked={developerMode}
313
+ onCheckedChange={handleDeveloperModeChange}
314
+ className="data-[state=checked]:bg-purple-500"
315
+ aria-label="Toggle developer mode"
316
+ />
317
+ <label className="text-sm text-bolt-elements-textSecondary">Developer Mode</label>
318
+ </div>
319
+ <motion.button
320
+ onClick={onClose}
321
+ className={classNames(
322
+ 'flex items-center justify-center w-8 h-8 rounded-lg',
323
+ 'bg-[#F5F5F5] dark:bg-[#1A1A1A]',
324
+ 'hover:bg-purple-500/10 dark:hover:bg-purple-500/20',
325
+ 'group transition-all duration-200',
326
+ )}
327
+ whileHover={{ scale: 1.05 }}
328
+ whileTap={{ scale: 0.95 }}
329
+ >
330
+ <div className="i-ph:x w-4 h-4 text-bolt-elements-textSecondary group-hover:text-purple-500 transition-colors" />
331
+ </motion.button>
332
+ </div>
333
+ </div>
334
+
335
+ {/* Content */}
336
+ <div
337
+ className={classNames(
338
+ 'flex-1',
339
+ 'overflow-y-auto',
340
+ 'hover:overflow-y-auto',
341
+ 'scrollbar scrollbar-w-2',
342
+ 'scrollbar-track-transparent',
343
+ 'scrollbar-thumb-[#E5E5E5] hover:scrollbar-thumb-[#CCCCCC]',
344
+ 'dark:scrollbar-thumb-[#333333] dark:hover:scrollbar-thumb-[#444444]',
345
+ 'will-change-scroll',
346
+ 'touch-auto',
347
+ )}
348
+ >
349
+ <motion.div
350
+ key={activeTab || 'home'}
351
+ initial={{ opacity: 0, y: 20 }}
352
+ animate={{ opacity: 1, y: 0 }}
353
+ className="p-6"
354
+ >
355
+ {activeTab ? (
356
+ getTabComponent()
357
+ ) : (
358
+ <div className="grid grid-cols-4 gap-4">
359
+ {visibleUserTabs.map((tab: TabVisibilityConfig, index: number) => (
360
+ <DraggableTabTile
361
+ key={tab.id}
362
+ tab={tab}
363
+ index={index}
364
+ moveTab={moveTab}
365
+ onClick={() => handleTabClick(tab.id)}
366
+ isActive={activeTab === tab.id}
367
+ hasUpdate={getTabUpdateStatus(tab.id)}
368
+ statusMessage={getStatusMessage(tab.id)}
369
+ description={TAB_DESCRIPTIONS[tab.id]}
370
+ isLoading={loadingTab === tab.id}
371
+ />
372
+ ))}
373
+ </div>
374
+ )}
375
+ </motion.div>
376
+ </div>
377
+ </motion.div>
378
+ </RadixDialog.Content>
379
+ </div>
380
+ </RadixDialog.Portal>
381
+ </RadixDialog.Root>
382
+ </DndProvider>
383
+ </>
384
+ );
385
+ };
app/components/sidebar/Menu.client.tsx CHANGED
@@ -3,7 +3,7 @@ import { useCallback, useEffect, useRef, useState } from 'react';
3
  import { toast } from 'react-toastify';
4
  import { Dialog, DialogButton, DialogDescription, DialogRoot, DialogTitle } from '~/components/ui/Dialog';
5
  import { ThemeSwitch } from '~/components/ui/ThemeSwitch';
6
- import { SettingsWindow } from '~/components/settings/SettingsWindow';
7
  import { SettingsButton } from '~/components/ui/SettingsButton';
8
  import { db, deleteById, getAll, chatId, type ChatHistoryItem, useChatHistory } from '~/lib/persistence';
9
  import { cubicEasingFn } from '~/utils/easings';
@@ -226,7 +226,7 @@ export const Menu = () => {
226
  <ThemeSwitch />
227
  </div>
228
  </div>
229
- <SettingsWindow open={isSettingsOpen} onClose={() => setIsSettingsOpen(false)} />
230
  </motion.div>
231
  );
232
  };
 
3
  import { toast } from 'react-toastify';
4
  import { Dialog, DialogButton, DialogDescription, DialogRoot, DialogTitle } from '~/components/ui/Dialog';
5
  import { ThemeSwitch } from '~/components/ui/ThemeSwitch';
6
+ import { UsersWindow } from '~/components/settings/user/UsersWindow';
7
  import { SettingsButton } from '~/components/ui/SettingsButton';
8
  import { db, deleteById, getAll, chatId, type ChatHistoryItem, useChatHistory } from '~/lib/persistence';
9
  import { cubicEasingFn } from '~/utils/easings';
 
226
  <ThemeSwitch />
227
  </div>
228
  </div>
229
+ <UsersWindow open={isSettingsOpen} onClose={() => setIsSettingsOpen(false)} />
230
  </motion.div>
231
  );
232
  };
app/components/ui/Dropdown.tsx ADDED
@@ -0,0 +1,63 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as DropdownMenu from '@radix-ui/react-dropdown-menu';
2
+ import { type ReactNode } from 'react';
3
+ import { classNames } from '~/utils/classNames';
4
+
5
+ interface DropdownProps {
6
+ trigger: ReactNode;
7
+ children: ReactNode;
8
+ align?: 'start' | 'center' | 'end';
9
+ sideOffset?: number;
10
+ }
11
+
12
+ interface DropdownItemProps {
13
+ children: ReactNode;
14
+ onSelect?: () => void;
15
+ className?: string;
16
+ }
17
+
18
+ export const DropdownItem = ({ children, onSelect, className }: DropdownItemProps) => (
19
+ <DropdownMenu.Item
20
+ className={classNames(
21
+ 'relative flex items-center gap-2 px-3 py-2 rounded-lg text-sm',
22
+ 'text-bolt-elements-textPrimary hover:text-bolt-elements-textPrimary',
23
+ 'hover:bg-bolt-elements-background-depth-3',
24
+ 'transition-colors cursor-pointer',
25
+ 'outline-none',
26
+ className,
27
+ )}
28
+ onSelect={onSelect}
29
+ >
30
+ {children}
31
+ </DropdownMenu.Item>
32
+ );
33
+
34
+ export const DropdownSeparator = () => <DropdownMenu.Separator className="h-px bg-bolt-elements-borderColor my-1" />;
35
+
36
+ export const Dropdown = ({ trigger, children, align = 'end', sideOffset = 5 }: DropdownProps) => {
37
+ return (
38
+ <DropdownMenu.Root>
39
+ <DropdownMenu.Trigger asChild>{trigger}</DropdownMenu.Trigger>
40
+
41
+ <DropdownMenu.Portal>
42
+ <DropdownMenu.Content
43
+ className={classNames(
44
+ 'min-w-[220px] rounded-lg p-2',
45
+ 'bg-bolt-elements-background-depth-2',
46
+ 'border border-bolt-elements-borderColor',
47
+ 'shadow-lg',
48
+ 'animate-in fade-in-80 zoom-in-95',
49
+ 'data-[side=bottom]:slide-in-from-top-2',
50
+ 'data-[side=left]:slide-in-from-right-2',
51
+ 'data-[side=right]:slide-in-from-left-2',
52
+ 'data-[side=top]:slide-in-from-bottom-2',
53
+ 'z-[1000]',
54
+ )}
55
+ sideOffset={sideOffset}
56
+ align={align}
57
+ >
58
+ {children}
59
+ </DropdownMenu.Content>
60
+ </DropdownMenu.Portal>
61
+ </DropdownMenu.Root>
62
+ );
63
+ };
app/components/workbench/Preview.tsx CHANGED
@@ -11,13 +11,14 @@ interface WindowSize {
11
  name: string;
12
  width: number;
13
  height: number;
 
14
  }
15
 
16
  const WINDOW_SIZES: WindowSize[] = [
17
- { name: 'Mobile (375x667)', width: 375, height: 667 },
18
- { name: 'Tablet (768x1024)', width: 768, height: 1024 },
19
- { name: 'Laptop (1366x768)', width: 1366, height: 768 },
20
- { name: 'Desktop (1920x1080)', width: 1920, height: 1080 },
21
  ];
22
 
23
  export const Preview = memo(() => {
@@ -249,14 +250,17 @@ export const Preview = memo(() => {
249
  {isPortDropdownOpen && (
250
  <div className="z-iframe-overlay w-full h-full absolute" onClick={() => setIsPortDropdownOpen(false)} />
251
  )}
252
- <div className="bg-bolt-elements-background-depth-2 p-2 flex items-center gap-1.5">
253
- <IconButton icon="i-ph:arrow-clockwise" onClick={reloadPreview} />
254
- <IconButton
255
- icon="i-ph:selection"
256
- onClick={() => setIsSelectionMode(!isSelectionMode)}
257
- className={isSelectionMode ? 'bg-bolt-elements-background-depth-3' : ''}
258
- />
259
- <div className="flex items-center gap-1 flex-grow bg-bolt-elements-preview-addressBar-background border border-bolt-elements-borderColor text-bolt-elements-preview-addressBar-text rounded-full px-3 py-1 text-sm hover:bg-bolt-elements-preview-addressBar-backgroundHover hover:focus-within:bg-bolt-elements-preview-addressBar-backgroundActive focus-within:bg-bolt-elements-preview-addressBar-backgroundActive focus-within-border-bolt-elements-borderColorActive focus-within:text-bolt-elements-preview-addressBar-textActive">
 
 
 
260
  <input
261
  title="URL"
262
  ref={inputRef}
@@ -278,68 +282,80 @@ export const Preview = memo(() => {
278
  />
279
  </div>
280
 
281
- {previews.length > 1 && (
282
- <PortDropdown
283
- activePreviewIndex={activePreviewIndex}
284
- setActivePreviewIndex={setActivePreviewIndex}
285
- isDropdownOpen={isPortDropdownOpen}
286
- setHasSelectedPreview={(value) => (hasSelectedPreview.current = value)}
287
- setIsDropdownOpen={setIsPortDropdownOpen}
288
- previews={previews}
 
 
 
 
 
 
 
 
289
  />
290
- )}
291
-
292
- <IconButton
293
- icon="i-ph:devices"
294
- onClick={toggleDeviceMode}
295
- title={isDeviceModeOn ? 'Switch to Responsive Mode' : 'Switch to Device Mode'}
296
- />
297
-
298
- <IconButton
299
- icon="i-ph:layout-light"
300
- onClick={() => setIsPreviewOnly(!isPreviewOnly)}
301
- title={isPreviewOnly ? 'Show Full Interface' : 'Show Preview Only'}
302
- />
303
-
304
- <IconButton
305
- icon={isFullscreen ? 'i-ph:arrows-in' : 'i-ph:arrows-out'}
306
- onClick={toggleFullscreen}
307
- title={isFullscreen ? 'Exit Full Screen' : 'Full Screen'}
308
- />
309
-
310
- <div className="relative">
311
  <IconButton
312
- icon="i-ph:arrow-square-out"
313
- onClick={() => openInNewWindow(selectedWindowSize)}
314
- title={`Open Preview in ${selectedWindowSize.name} Window`}
315
  />
 
316
  <IconButton
317
- icon="i-ph:caret-down"
318
- onClick={() => setIsWindowSizeDropdownOpen(!isWindowSizeDropdownOpen)}
319
- className="ml-1"
320
- title="Select Window Size"
321
  />
322
 
323
- {isWindowSizeDropdownOpen && (
324
- <>
325
- <div className="fixed inset-0 z-50" onClick={() => setIsWindowSizeDropdownOpen(false)} />
326
- <div className="absolute right-0 top-full mt-1 z-50 bg-bolt-elements-background-depth-2 rounded-lg shadow-lg border border-bolt-elements-borderColor overflow-hidden">
327
- {WINDOW_SIZES.map((size) => (
328
- <button
329
- key={size.name}
330
- className="w-full px-4 py-2 text-left hover:bg-bolt-elements-background-depth-3 text-sm whitespace-nowrap"
331
- onClick={() => {
332
- setSelectedWindowSize(size);
333
- setIsWindowSizeDropdownOpen(false);
334
- openInNewWindow(size);
335
- }}
336
- >
337
- {size.name}
338
- </button>
339
- ))}
340
- </div>
341
- </>
342
- )}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
343
  </div>
344
  </div>
345
 
@@ -349,7 +365,7 @@ export const Preview = memo(() => {
349
  width: isDeviceModeOn ? `${widthPercent}%` : '100%',
350
  height: '100%',
351
  overflow: 'visible',
352
- background: '#fff',
353
  position: 'relative',
354
  display: 'flex',
355
  }}
@@ -359,7 +375,7 @@ export const Preview = memo(() => {
359
  <iframe
360
  ref={iframeRef}
361
  title="preview"
362
- className="border-none w-full h-full bg-white"
363
  src={iframeUrl}
364
  sandbox="allow-scripts allow-forms allow-popups allow-modals allow-storage-access-by-user-activation allow-same-origin"
365
  allow="cross-origin-isolated"
@@ -371,7 +387,9 @@ export const Preview = memo(() => {
371
  />
372
  </>
373
  ) : (
374
- <div className="flex w-full h-full justify-center items-center bg-white">No preview available</div>
 
 
375
  )}
376
 
377
  {isDeviceModeOn && (
 
11
  name: string;
12
  width: number;
13
  height: number;
14
+ icon: string;
15
  }
16
 
17
  const WINDOW_SIZES: WindowSize[] = [
18
+ { name: 'Mobile', width: 375, height: 667, icon: 'i-ph:device-mobile' },
19
+ { name: 'Tablet', width: 768, height: 1024, icon: 'i-ph:device-tablet' },
20
+ { name: 'Laptop', width: 1366, height: 768, icon: 'i-ph:laptop' },
21
+ { name: 'Desktop', width: 1920, height: 1080, icon: 'i-ph:monitor' },
22
  ];
23
 
24
  export const Preview = memo(() => {
 
250
  {isPortDropdownOpen && (
251
  <div className="z-iframe-overlay w-full h-full absolute" onClick={() => setIsPortDropdownOpen(false)} />
252
  )}
253
+ <div className="bg-bolt-elements-background-depth-2 p-2 flex items-center gap-2">
254
+ <div className="flex items-center gap-2">
255
+ <IconButton icon="i-ph:arrow-clockwise" onClick={reloadPreview} />
256
+ <IconButton
257
+ icon="i-ph:selection"
258
+ onClick={() => setIsSelectionMode(!isSelectionMode)}
259
+ className={isSelectionMode ? 'bg-bolt-elements-background-depth-3' : ''}
260
+ />
261
+ </div>
262
+
263
+ <div className="flex-grow flex items-center gap-1 bg-bolt-elements-preview-addressBar-background border border-bolt-elements-borderColor text-bolt-elements-preview-addressBar-text rounded-full px-3 py-1 text-sm hover:bg-bolt-elements-preview-addressBar-backgroundHover hover:focus-within:bg-bolt-elements-preview-addressBar-backgroundActive focus-within:bg-bolt-elements-preview-addressBar-backgroundActive focus-within-border-bolt-elements-borderColorActive focus-within:text-bolt-elements-preview-addressBar-textActive">
264
  <input
265
  title="URL"
266
  ref={inputRef}
 
282
  />
283
  </div>
284
 
285
+ <div className="flex items-center gap-2">
286
+ {previews.length > 1 && (
287
+ <PortDropdown
288
+ activePreviewIndex={activePreviewIndex}
289
+ setActivePreviewIndex={setActivePreviewIndex}
290
+ isDropdownOpen={isPortDropdownOpen}
291
+ setHasSelectedPreview={(value) => (hasSelectedPreview.current = value)}
292
+ setIsDropdownOpen={setIsPortDropdownOpen}
293
+ previews={previews}
294
+ />
295
+ )}
296
+
297
+ <IconButton
298
+ icon="i-ph:devices"
299
+ onClick={toggleDeviceMode}
300
+ title={isDeviceModeOn ? 'Switch to Responsive Mode' : 'Switch to Device Mode'}
301
  />
302
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
303
  <IconButton
304
+ icon="i-ph:layout-light"
305
+ onClick={() => setIsPreviewOnly(!isPreviewOnly)}
306
+ title={isPreviewOnly ? 'Show Full Interface' : 'Show Preview Only'}
307
  />
308
+
309
  <IconButton
310
+ icon={isFullscreen ? 'i-ph:arrows-in' : 'i-ph:arrows-out'}
311
+ onClick={toggleFullscreen}
312
+ title={isFullscreen ? 'Exit Full Screen' : 'Full Screen'}
 
313
  />
314
 
315
+ <div className="flex items-center relative">
316
+ <IconButton
317
+ icon="i-ph:arrow-square-out"
318
+ onClick={() => openInNewWindow(selectedWindowSize)}
319
+ title={`Open Preview in ${selectedWindowSize.name} Window`}
320
+ />
321
+ <IconButton
322
+ icon="i-ph:caret-down"
323
+ onClick={() => setIsWindowSizeDropdownOpen(!isWindowSizeDropdownOpen)}
324
+ className="ml-1"
325
+ title="Select Window Size"
326
+ />
327
+
328
+ {isWindowSizeDropdownOpen && (
329
+ <>
330
+ <div className="fixed inset-0 z-50" onClick={() => setIsWindowSizeDropdownOpen(false)} />
331
+ <div className="absolute right-0 top-full mt-2 z-50 min-w-[240px] bg-white dark:bg-black rounded-xl shadow-2xl border border-[#E5E7EB] dark:border-[rgba(255,255,255,0.1)] overflow-hidden">
332
+ {WINDOW_SIZES.map((size) => (
333
+ <button
334
+ key={size.name}
335
+ className="w-full px-4 py-3.5 text-left text-[#111827] dark:text-gray-300 text-sm whitespace-nowrap flex items-center gap-3 group hover:bg-[#F5EEFF] dark:hover:bg-gray-900 bg-white dark:bg-black"
336
+ onClick={() => {
337
+ setSelectedWindowSize(size);
338
+ setIsWindowSizeDropdownOpen(false);
339
+ openInNewWindow(size);
340
+ }}
341
+ >
342
+ <div
343
+ className={`${size.icon} w-5 h-5 text-[#6B7280] dark:text-gray-400 group-hover:text-[#6D28D9] dark:group-hover:text-[#6D28D9] transition-colors duration-200`}
344
+ />
345
+ <div className="flex flex-col">
346
+ <span className="font-medium group-hover:text-[#6D28D9] dark:group-hover:text-[#6D28D9] transition-colors duration-200">
347
+ {size.name}
348
+ </span>
349
+ <span className="text-xs text-[#6B7280] dark:text-gray-400 group-hover:text-[#6D28D9] dark:group-hover:text-[#6D28D9] transition-colors duration-200">
350
+ {size.width} × {size.height}
351
+ </span>
352
+ </div>
353
+ </button>
354
+ ))}
355
+ </div>
356
+ </>
357
+ )}
358
+ </div>
359
  </div>
360
  </div>
361
 
 
365
  width: isDeviceModeOn ? `${widthPercent}%` : '100%',
366
  height: '100%',
367
  overflow: 'visible',
368
+ background: 'var(--bolt-elements-background-depth-1)',
369
  position: 'relative',
370
  display: 'flex',
371
  }}
 
375
  <iframe
376
  ref={iframeRef}
377
  title="preview"
378
+ className="border-none w-full h-full bg-bolt-elements-background-depth-1"
379
  src={iframeUrl}
380
  sandbox="allow-scripts allow-forms allow-popups allow-modals allow-storage-access-by-user-activation allow-same-origin"
381
  allow="cross-origin-isolated"
 
387
  />
388
  </>
389
  ) : (
390
+ <div className="flex w-full h-full justify-center items-center bg-bolt-elements-background-depth-1 text-bolt-elements-textPrimary">
391
+ No preview available
392
+ </div>
393
  )}
394
 
395
  {isDeviceModeOn && (
app/lib/api/connection.ts ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export interface ConnectionStatus {
2
+ connected: boolean;
3
+ latency: number;
4
+ lastChecked: string;
5
+ }
6
+
7
+ export const checkConnection = async (): Promise<ConnectionStatus> => {
8
+ /*
9
+ * TODO: Implement actual connection check logic
10
+ * This is a mock implementation
11
+ */
12
+ const connected = Math.random() > 0.1; // 90% chance of being connected
13
+ return {
14
+ connected,
15
+ latency: connected ? Math.floor(Math.random() * 1500) : 0, // Random latency between 0-1500ms
16
+ lastChecked: new Date().toISOString(),
17
+ };
18
+ };
app/lib/api/debug.ts ADDED
@@ -0,0 +1,65 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export interface DebugWarning {
2
+ id: string;
3
+ message: string;
4
+ timestamp: string;
5
+ code: string;
6
+ }
7
+
8
+ export interface DebugError {
9
+ id: string;
10
+ message: string;
11
+ timestamp: string;
12
+ stack?: string;
13
+ }
14
+
15
+ export interface DebugStatus {
16
+ warnings: DebugWarning[];
17
+ errors: DebugError[];
18
+ lastChecked: string;
19
+ }
20
+
21
+ export interface DebugIssue {
22
+ id: string;
23
+ type: 'warning' | 'error';
24
+ message: string;
25
+ }
26
+
27
+ export const getDebugStatus = async (): Promise<DebugStatus> => {
28
+ /*
29
+ * TODO: Implement actual debug status logic
30
+ * This is a mock implementation
31
+ */
32
+ return {
33
+ warnings: [
34
+ {
35
+ id: 'warn-1',
36
+ message: 'High memory usage detected',
37
+ timestamp: new Date().toISOString(),
38
+ code: 'MEM_HIGH',
39
+ },
40
+ ],
41
+ errors: [
42
+ {
43
+ id: 'err-1',
44
+ message: 'Failed to connect to database',
45
+ timestamp: new Date().toISOString(),
46
+ stack: 'Error: Connection timeout',
47
+ },
48
+ ],
49
+ lastChecked: new Date().toISOString(),
50
+ };
51
+ };
52
+
53
+ export const acknowledgeWarning = async (warningId: string): Promise<void> => {
54
+ /*
55
+ * TODO: Implement actual warning acknowledgment logic
56
+ */
57
+ console.log(`Acknowledging warning ${warningId}`);
58
+ };
59
+
60
+ export const acknowledgeError = async (errorId: string): Promise<void> => {
61
+ /*
62
+ * TODO: Implement actual error acknowledgment logic
63
+ */
64
+ console.log(`Acknowledging error ${errorId}`);
65
+ };
app/lib/api/features.ts ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export interface Feature {
2
+ id: string;
3
+ name: string;
4
+ description: string;
5
+ viewed: boolean;
6
+ releaseDate: string;
7
+ }
8
+
9
+ export const getFeatureFlags = async (): Promise<Feature[]> => {
10
+ /*
11
+ * TODO: Implement actual feature flags logic
12
+ * This is a mock implementation
13
+ */
14
+ return [
15
+ {
16
+ id: 'feature-1',
17
+ name: 'Dark Mode',
18
+ description: 'Enable dark mode for better night viewing',
19
+ viewed: true,
20
+ releaseDate: '2024-03-15',
21
+ },
22
+ {
23
+ id: 'feature-2',
24
+ name: 'Tab Management',
25
+ description: 'Customize your tab layout',
26
+ viewed: false,
27
+ releaseDate: '2024-03-20',
28
+ },
29
+ ];
30
+ };
31
+
32
+ export const markFeatureViewed = async (featureId: string): Promise<void> => {
33
+ /* TODO: Implement actual feature viewed logic */
34
+ console.log(`Marking feature ${featureId} as viewed`);
35
+ };
app/lib/api/notifications.ts ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export interface Notification {
2
+ id: string;
3
+ title: string;
4
+ message: string;
5
+ type: 'info' | 'warning' | 'error' | 'success';
6
+ read: boolean;
7
+ timestamp: string;
8
+ }
9
+
10
+ export const getNotifications = async (): Promise<Notification[]> => {
11
+ /*
12
+ * TODO: Implement actual notifications logic
13
+ * This is a mock implementation
14
+ */
15
+ return [
16
+ {
17
+ id: 'notif-1',
18
+ title: 'Welcome to Bolt',
19
+ message: 'Get started by exploring the features',
20
+ type: 'info',
21
+ read: true,
22
+ timestamp: new Date().toISOString(),
23
+ },
24
+ {
25
+ id: 'notif-2',
26
+ title: 'New Update Available',
27
+ message: 'Version 1.0.1 is now available',
28
+ type: 'info',
29
+ read: false,
30
+ timestamp: new Date().toISOString(),
31
+ },
32
+ ];
33
+ };
34
+
35
+ export const markNotificationRead = async (notificationId: string): Promise<void> => {
36
+ /*
37
+ * TODO: Implement actual notification read logic
38
+ */
39
+ console.log(`Marking notification ${notificationId} as read`);
40
+ };
app/lib/api/updates.ts ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export interface UpdateCheckResult {
2
+ available: boolean;
3
+ version: string;
4
+ releaseNotes?: string;
5
+ }
6
+
7
+ export const checkForUpdates = async (): Promise<UpdateCheckResult> => {
8
+ /*
9
+ * TODO: Implement actual update check logic
10
+ * This is a mock implementation
11
+ */
12
+ return {
13
+ available: Math.random() > 0.7, // 30% chance of update
14
+ version: '1.0.1',
15
+ releaseNotes: 'Bug fixes and performance improvements',
16
+ };
17
+ };
18
+
19
+ export const acknowledgeUpdate = async (version: string): Promise<void> => {
20
+ /*
21
+ * TODO: Implement actual update acknowledgment logic
22
+ * This is a mock implementation that would typically:
23
+ * 1. Store the acknowledged version in a persistent store
24
+ * 2. Update the UI state
25
+ * 3. Potentially send analytics
26
+ */
27
+ console.log(`Acknowledging update version ${version}`);
28
+ };
app/lib/hooks/index.ts CHANGED
@@ -4,3 +4,8 @@ export * from './useShortcuts';
4
  export * from './useSnapScroll';
5
  export * from './useEditChatDescription';
6
  export { default } from './useViewport';
 
 
 
 
 
 
4
  export * from './useSnapScroll';
5
  export * from './useEditChatDescription';
6
  export { default } from './useViewport';
7
+ export { useUpdateCheck } from './useUpdateCheck';
8
+ export { useFeatures } from './useFeatures';
9
+ export { useNotifications } from './useNotifications';
10
+ export { useConnectionStatus } from './useConnectionStatus';
11
+ export { useDebugStatus } from './useDebugStatus';
app/lib/hooks/useConnectionStatus.ts ADDED
@@ -0,0 +1,61 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState, useEffect } from 'react';
2
+ import { checkConnection } from '~/lib/api/connection';
3
+
4
+ const ACKNOWLEDGED_CONNECTION_ISSUE_KEY = 'bolt_acknowledged_connection_issue';
5
+
6
+ type ConnectionIssueType = 'disconnected' | 'high-latency' | null;
7
+
8
+ const getAcknowledgedIssue = (): string | null => {
9
+ try {
10
+ return localStorage.getItem(ACKNOWLEDGED_CONNECTION_ISSUE_KEY);
11
+ } catch {
12
+ return null;
13
+ }
14
+ };
15
+
16
+ export const useConnectionStatus = () => {
17
+ const [hasConnectionIssues, setHasConnectionIssues] = useState(false);
18
+ const [currentIssue, setCurrentIssue] = useState<ConnectionIssueType>(null);
19
+ const [acknowledgedIssue, setAcknowledgedIssue] = useState<string | null>(() => getAcknowledgedIssue());
20
+
21
+ const checkStatus = async () => {
22
+ try {
23
+ const status = await checkConnection();
24
+ const issue = !status.connected ? 'disconnected' : status.latency > 1000 ? 'high-latency' : null;
25
+
26
+ setCurrentIssue(issue);
27
+
28
+ // Only show issues if they're new or different from the acknowledged one
29
+ setHasConnectionIssues(issue !== null && issue !== acknowledgedIssue);
30
+ } catch (error) {
31
+ console.error('Failed to check connection:', error);
32
+
33
+ // Show connection issues if we can't even check the status
34
+ setCurrentIssue('disconnected');
35
+ setHasConnectionIssues(true);
36
+ }
37
+ };
38
+
39
+ useEffect(() => {
40
+ // Check immediately and then every 10 seconds
41
+ checkStatus();
42
+
43
+ const interval = setInterval(checkStatus, 10 * 1000);
44
+
45
+ return () => clearInterval(interval);
46
+ }, [acknowledgedIssue]);
47
+
48
+ const acknowledgeIssue = () => {
49
+ setAcknowledgedIssue(currentIssue);
50
+ setAcknowledgedIssue(currentIssue);
51
+ setHasConnectionIssues(false);
52
+ };
53
+
54
+ const resetAcknowledgment = () => {
55
+ setAcknowledgedIssue(null);
56
+ setAcknowledgedIssue(null);
57
+ checkStatus();
58
+ };
59
+
60
+ return { hasConnectionIssues, currentIssue, acknowledgeIssue, resetAcknowledgment };
61
+ };
app/lib/hooks/useDebugStatus.ts ADDED
@@ -0,0 +1,89 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState, useEffect } from 'react';
2
+ import { getDebugStatus, acknowledgeWarning, acknowledgeError, type DebugIssue } from '~/lib/api/debug';
3
+
4
+ const ACKNOWLEDGED_DEBUG_ISSUES_KEY = 'bolt_acknowledged_debug_issues';
5
+
6
+ const getAcknowledgedIssues = (): string[] => {
7
+ try {
8
+ const stored = localStorage.getItem(ACKNOWLEDGED_DEBUG_ISSUES_KEY);
9
+ return stored ? JSON.parse(stored) : [];
10
+ } catch {
11
+ return [];
12
+ }
13
+ };
14
+
15
+ const setAcknowledgedIssues = (issueIds: string[]) => {
16
+ try {
17
+ localStorage.setItem(ACKNOWLEDGED_DEBUG_ISSUES_KEY, JSON.stringify(issueIds));
18
+ } catch (error) {
19
+ console.error('Failed to persist acknowledged debug issues:', error);
20
+ }
21
+ };
22
+
23
+ export const useDebugStatus = () => {
24
+ const [hasActiveWarnings, setHasActiveWarnings] = useState(false);
25
+ const [activeIssues, setActiveIssues] = useState<DebugIssue[]>([]);
26
+ const [acknowledgedIssueIds, setAcknowledgedIssueIds] = useState<string[]>(() => getAcknowledgedIssues());
27
+
28
+ const checkDebugStatus = async () => {
29
+ try {
30
+ const status = await getDebugStatus();
31
+ const issues: DebugIssue[] = [
32
+ ...status.warnings.map((w) => ({ ...w, type: 'warning' as const })),
33
+ ...status.errors.map((e) => ({ ...e, type: 'error' as const })),
34
+ ].filter((issue) => !acknowledgedIssueIds.includes(issue.id));
35
+
36
+ setActiveIssues(issues);
37
+ setHasActiveWarnings(issues.length > 0);
38
+ } catch (error) {
39
+ console.error('Failed to check debug status:', error);
40
+ }
41
+ };
42
+
43
+ useEffect(() => {
44
+ // Check immediately and then every 5 seconds
45
+ checkDebugStatus();
46
+
47
+ const interval = setInterval(checkDebugStatus, 5 * 1000);
48
+
49
+ return () => clearInterval(interval);
50
+ }, [acknowledgedIssueIds]);
51
+
52
+ const acknowledgeIssue = async (issue: DebugIssue) => {
53
+ try {
54
+ if (issue.type === 'warning') {
55
+ await acknowledgeWarning(issue.id);
56
+ } else {
57
+ await acknowledgeError(issue.id);
58
+ }
59
+
60
+ const newAcknowledgedIds = [...acknowledgedIssueIds, issue.id];
61
+ setAcknowledgedIssueIds(newAcknowledgedIds);
62
+ setAcknowledgedIssues(newAcknowledgedIds);
63
+ setActiveIssues((prev) => prev.filter((i) => i.id !== issue.id));
64
+ setHasActiveWarnings(activeIssues.length > 1);
65
+ } catch (error) {
66
+ console.error('Failed to acknowledge issue:', error);
67
+ }
68
+ };
69
+
70
+ const acknowledgeAllIssues = async () => {
71
+ try {
72
+ await Promise.all(
73
+ activeIssues.map((issue) =>
74
+ issue.type === 'warning' ? acknowledgeWarning(issue.id) : acknowledgeError(issue.id),
75
+ ),
76
+ );
77
+
78
+ const newAcknowledgedIds = [...acknowledgedIssueIds, ...activeIssues.map((i) => i.id)];
79
+ setAcknowledgedIssueIds(newAcknowledgedIds);
80
+ setAcknowledgedIssues(newAcknowledgedIds);
81
+ setActiveIssues([]);
82
+ setHasActiveWarnings(false);
83
+ } catch (error) {
84
+ console.error('Failed to acknowledge all issues:', error);
85
+ }
86
+ };
87
+
88
+ return { hasActiveWarnings, activeIssues, acknowledgeIssue, acknowledgeAllIssues };
89
+ };
app/lib/hooks/useFeatures.ts ADDED
@@ -0,0 +1,72 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState, useEffect } from 'react';
2
+ import { getFeatureFlags, markFeatureViewed, type Feature } from '~/lib/api/features';
3
+
4
+ const VIEWED_FEATURES_KEY = 'bolt_viewed_features';
5
+
6
+ const getViewedFeatures = (): string[] => {
7
+ try {
8
+ const stored = localStorage.getItem(VIEWED_FEATURES_KEY);
9
+ return stored ? JSON.parse(stored) : [];
10
+ } catch {
11
+ return [];
12
+ }
13
+ };
14
+
15
+ const setViewedFeatures = (featureIds: string[]) => {
16
+ try {
17
+ localStorage.setItem(VIEWED_FEATURES_KEY, JSON.stringify(featureIds));
18
+ } catch (error) {
19
+ console.error('Failed to persist viewed features:', error);
20
+ }
21
+ };
22
+
23
+ export const useFeatures = () => {
24
+ const [hasNewFeatures, setHasNewFeatures] = useState(false);
25
+ const [unviewedFeatures, setUnviewedFeatures] = useState<Feature[]>([]);
26
+ const [viewedFeatureIds, setViewedFeatureIds] = useState<string[]>(() => getViewedFeatures());
27
+
28
+ useEffect(() => {
29
+ const checkNewFeatures = async () => {
30
+ try {
31
+ const features = await getFeatureFlags();
32
+ const unviewed = features.filter((feature) => !viewedFeatureIds.includes(feature.id));
33
+ setUnviewedFeatures(unviewed);
34
+ setHasNewFeatures(unviewed.length > 0);
35
+ } catch (error) {
36
+ console.error('Failed to check for new features:', error);
37
+ }
38
+ };
39
+
40
+ checkNewFeatures();
41
+ }, [viewedFeatureIds]);
42
+
43
+ const acknowledgeFeature = async (featureId: string) => {
44
+ try {
45
+ await markFeatureViewed(featureId);
46
+
47
+ const newViewedIds = [...viewedFeatureIds, featureId];
48
+ setViewedFeatureIds(newViewedIds);
49
+ setViewedFeatures(newViewedIds);
50
+ setUnviewedFeatures((prev) => prev.filter((feature) => feature.id !== featureId));
51
+ setHasNewFeatures(unviewedFeatures.length > 1);
52
+ } catch (error) {
53
+ console.error('Failed to acknowledge feature:', error);
54
+ }
55
+ };
56
+
57
+ const acknowledgeAllFeatures = async () => {
58
+ try {
59
+ await Promise.all(unviewedFeatures.map((feature) => markFeatureViewed(feature.id)));
60
+
61
+ const newViewedIds = [...viewedFeatureIds, ...unviewedFeatures.map((f) => f.id)];
62
+ setViewedFeatureIds(newViewedIds);
63
+ setViewedFeatures(newViewedIds);
64
+ setUnviewedFeatures([]);
65
+ setHasNewFeatures(false);
66
+ } catch (error) {
67
+ console.error('Failed to acknowledge all features:', error);
68
+ }
69
+ };
70
+
71
+ return { hasNewFeatures, unviewedFeatures, acknowledgeFeature, acknowledgeAllFeatures };
72
+ };
app/lib/hooks/useNotifications.ts ADDED
@@ -0,0 +1,77 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState, useEffect } from 'react';
2
+ import { getNotifications, markNotificationRead, type Notification } from '~/lib/api/notifications';
3
+
4
+ const READ_NOTIFICATIONS_KEY = 'bolt_read_notifications';
5
+
6
+ const getReadNotifications = (): string[] => {
7
+ try {
8
+ const stored = localStorage.getItem(READ_NOTIFICATIONS_KEY);
9
+ return stored ? JSON.parse(stored) : [];
10
+ } catch {
11
+ return [];
12
+ }
13
+ };
14
+
15
+ const setReadNotifications = (notificationIds: string[]) => {
16
+ try {
17
+ localStorage.setItem(READ_NOTIFICATIONS_KEY, JSON.stringify(notificationIds));
18
+ } catch (error) {
19
+ console.error('Failed to persist read notifications:', error);
20
+ }
21
+ };
22
+
23
+ export const useNotifications = () => {
24
+ const [hasUnreadNotifications, setHasUnreadNotifications] = useState(false);
25
+ const [unreadNotifications, setUnreadNotifications] = useState<Notification[]>([]);
26
+ const [readNotificationIds, setReadNotificationIds] = useState<string[]>(() => getReadNotifications());
27
+
28
+ const checkNotifications = async () => {
29
+ try {
30
+ const notifications = await getNotifications();
31
+ const unread = notifications.filter((n) => !readNotificationIds.includes(n.id));
32
+ setUnreadNotifications(unread);
33
+ setHasUnreadNotifications(unread.length > 0);
34
+ } catch (error) {
35
+ console.error('Failed to check notifications:', error);
36
+ }
37
+ };
38
+
39
+ useEffect(() => {
40
+ // Check immediately and then every minute
41
+ checkNotifications();
42
+
43
+ const interval = setInterval(checkNotifications, 60 * 1000);
44
+
45
+ return () => clearInterval(interval);
46
+ }, [readNotificationIds]);
47
+
48
+ const markAsRead = async (notificationId: string) => {
49
+ try {
50
+ await markNotificationRead(notificationId);
51
+
52
+ const newReadIds = [...readNotificationIds, notificationId];
53
+ setReadNotificationIds(newReadIds);
54
+ setReadNotifications(newReadIds);
55
+ setUnreadNotifications((prev) => prev.filter((n) => n.id !== notificationId));
56
+ setHasUnreadNotifications(unreadNotifications.length > 1);
57
+ } catch (error) {
58
+ console.error('Failed to mark notification as read:', error);
59
+ }
60
+ };
61
+
62
+ const markAllAsRead = async () => {
63
+ try {
64
+ await Promise.all(unreadNotifications.map((n) => markNotificationRead(n.id)));
65
+
66
+ const newReadIds = [...readNotificationIds, ...unreadNotifications.map((n) => n.id)];
67
+ setReadNotificationIds(newReadIds);
68
+ setReadNotifications(newReadIds);
69
+ setUnreadNotifications([]);
70
+ setHasUnreadNotifications(false);
71
+ } catch (error) {
72
+ console.error('Failed to mark all notifications as read:', error);
73
+ }
74
+ };
75
+
76
+ return { hasUnreadNotifications, unreadNotifications, markAsRead, markAllAsRead };
77
+ };
app/lib/hooks/{useSettings.tsx → useSettings.ts} RENAMED
@@ -9,24 +9,63 @@ import {
9
  latestBranchStore,
10
  autoSelectStarterTemplate,
11
  enableContextOptimizationStore,
 
 
 
12
  } from '~/lib/stores/settings';
13
  import { useCallback, useEffect, useState } from 'react';
14
  import Cookies from 'js-cookie';
15
- import type { IProviderSetting, ProviderInfo } from '~/types/model';
16
- import { logStore } from '~/lib/stores/logs'; // assuming logStore is imported from this location
17
-
18
- interface CommitData {
19
- commit: string;
20
- version?: string;
 
 
 
 
 
 
21
  }
22
 
23
- const versionData: CommitData = {
24
- commit: __COMMIT_HASH,
25
- version: __APP_VERSION,
26
- };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
27
 
28
- export function useSettings() {
29
- const providers = useStore(providersStore);
30
  const debug = useStore(isDebugMode);
31
  const eventLogs = useStore(isEventLogsEnabled);
32
  const promptId = useStore(promptStore);
@@ -35,106 +74,18 @@ export function useSettings() {
35
  const autoSelectTemplate = useStore(autoSelectStarterTemplate);
36
  const [activeProviders, setActiveProviders] = useState<ProviderInfo[]>([]);
37
  const contextOptimizationEnabled = useStore(enableContextOptimizationStore);
38
-
39
- // Function to check if we're on stable version
40
- const checkIsStableVersion = async () => {
41
- try {
42
- const response = await fetch(
43
- `https://api.github.com/repos/stackblitz-labs/bolt.diy/git/refs/tags/v${versionData.version}`,
44
- );
45
- const data: { object: { sha: string } } = await response.json();
46
-
47
- return versionData.commit.slice(0, 7) === data.object.sha.slice(0, 7);
48
- } catch (error) {
49
- console.warn('Error checking stable version:', error);
50
- return false;
51
- }
52
- };
53
-
54
- // reading values from cookies on mount
55
- useEffect(() => {
56
- const savedProviders = Cookies.get('providers');
57
-
58
- if (savedProviders) {
59
- try {
60
- const parsedProviders: Record<string, IProviderSetting> = JSON.parse(savedProviders);
61
- Object.keys(providers).forEach((provider) => {
62
- const currentProviderSettings = parsedProviders[provider];
63
-
64
- if (currentProviderSettings) {
65
- providersStore.setKey(provider, {
66
- ...providers[provider],
67
- settings: {
68
- ...currentProviderSettings,
69
- enabled: currentProviderSettings.enabled ?? true,
70
- },
71
- });
72
- }
73
- });
74
- } catch (error) {
75
- console.error('Failed to parse providers from cookies:', error);
76
- }
77
- }
78
-
79
- // load debug mode from cookies
80
- const savedDebugMode = Cookies.get('isDebugEnabled');
81
-
82
- if (savedDebugMode) {
83
- isDebugMode.set(savedDebugMode === 'true');
84
- }
85
-
86
- // load event logs from cookies
87
- const savedEventLogs = Cookies.get('isEventLogsEnabled');
88
-
89
- if (savedEventLogs) {
90
- isEventLogsEnabled.set(savedEventLogs === 'true');
91
- }
92
-
93
- // load local models from cookies
94
- const savedLocalModels = Cookies.get('isLocalModelsEnabled');
95
-
96
- if (savedLocalModels) {
97
- isLocalModelsEnabled.set(savedLocalModels === 'true');
98
- }
99
-
100
- const promptId = Cookies.get('promptId');
101
-
102
- if (promptId) {
103
- promptStore.set(promptId);
104
- }
105
-
106
- // load latest branch setting from cookies or determine based on version
107
- const savedLatestBranch = Cookies.get('isLatestBranch');
108
- let checkCommit = Cookies.get('commitHash');
109
-
110
- if (checkCommit === undefined) {
111
- checkCommit = versionData.commit;
112
- }
113
-
114
- if (savedLatestBranch === undefined || checkCommit !== versionData.commit) {
115
- // If setting hasn't been set by user, check version
116
- checkIsStableVersion().then((isStable) => {
117
- const shouldUseLatest = !isStable;
118
- latestBranchStore.set(shouldUseLatest);
119
- Cookies.set('isLatestBranch', String(shouldUseLatest));
120
- Cookies.set('commitHash', String(versionData.commit));
121
- });
122
- } else {
123
- latestBranchStore.set(savedLatestBranch === 'true');
124
- }
125
-
126
- const autoSelectTemplate = Cookies.get('autoSelectTemplate');
127
-
128
- if (autoSelectTemplate) {
129
- autoSelectStarterTemplate.set(autoSelectTemplate === 'true');
130
- }
131
-
132
- const savedContextOptimizationEnabled = Cookies.get('contextOptimizationEnabled');
133
-
134
- if (savedContextOptimizationEnabled) {
135
- enableContextOptimizationStore.set(savedContextOptimizationEnabled === 'true');
136
- }
137
- }, []);
138
 
139
  // writing values to cookies on change
140
  useEffect(() => {
@@ -158,14 +109,18 @@ export function useSettings() {
158
  setActiveProviders(active);
159
  }, [providers, isLocalModel]);
160
 
161
- // helper function to update settings
162
- const updateProviderSettings = useCallback(
163
- (provider: string, config: IProviderSetting) => {
164
- const settings = providers[provider].settings;
165
- providersStore.setKey(provider, { ...providers[provider], settings: { ...settings, ...config } });
166
- },
167
- [providers],
168
- );
 
 
 
 
169
 
170
  const enableDebugMode = useCallback((enabled: boolean) => {
171
  isDebugMode.set(enabled);
@@ -173,7 +128,7 @@ export function useSettings() {
173
  Cookies.set('isDebugEnabled', String(enabled));
174
  }, []);
175
 
176
- const enableEventLogs = useCallback((enabled: boolean) => {
177
  isEventLogsEnabled.set(enabled);
178
  logStore.logSystem(`Event logs ${enabled ? 'enabled' : 'disabled'}`);
179
  Cookies.set('isEventLogsEnabled', String(enabled));
@@ -189,6 +144,7 @@ export function useSettings() {
189
  promptStore.set(promptId);
190
  Cookies.set('promptId', promptId);
191
  }, []);
 
192
  const enableLatestBranch = useCallback((enabled: boolean) => {
193
  latestBranchStore.set(enabled);
194
  logStore.logSystem(`Main branch updates ${enabled ? 'enabled' : 'disabled'}`);
@@ -207,16 +163,45 @@ export function useSettings() {
207
  Cookies.set('contextOptimizationEnabled', String(enabled));
208
  }, []);
209
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
210
  return {
 
211
  providers,
212
  activeProviders,
213
  updateProviderSettings,
 
 
214
  debug,
215
  enableDebugMode,
216
  eventLogs,
217
- enableEventLogs,
218
- isLocalModel,
219
- enableLocalModels,
220
  promptId,
221
  setPromptId,
222
  isLatestBranch,
@@ -225,5 +210,13 @@ export function useSettings() {
225
  setAutoSelectTemplate,
226
  contextOptimizationEnabled,
227
  enableContextOptimization,
 
 
 
 
 
 
 
 
228
  };
229
  }
 
9
  latestBranchStore,
10
  autoSelectStarterTemplate,
11
  enableContextOptimizationStore,
12
+ tabConfigurationStore,
13
+ updateTabConfiguration as updateTabConfig,
14
+ resetTabConfiguration as resetTabConfig,
15
  } from '~/lib/stores/settings';
16
  import { useCallback, useEffect, useState } from 'react';
17
  import Cookies from 'js-cookie';
18
+ import type { IProviderSetting, ProviderInfo, IProviderConfig } from '~/types/model';
19
+ import type { TabWindowConfig, TabVisibilityConfig } from '~/components/settings/settings.types';
20
+ import { logStore } from '~/lib/stores/logs';
21
+ import { getLocalStorage, setLocalStorage } from '~/utils/localStorage';
22
+
23
+ export interface Settings {
24
+ theme: 'light' | 'dark' | 'system';
25
+ language: string;
26
+ notifications: boolean;
27
+ eventLogs: boolean;
28
+ timezone: string;
29
+ tabConfiguration: TabWindowConfig;
30
  }
31
 
32
+ export interface UseSettingsReturn {
33
+ // Theme and UI settings
34
+ setTheme: (theme: Settings['theme']) => void;
35
+ setLanguage: (language: string) => void;
36
+ setNotifications: (enabled: boolean) => void;
37
+ setEventLogs: (enabled: boolean) => void;
38
+ setTimezone: (timezone: string) => void;
39
+ settings: Settings;
40
+
41
+ // Provider settings
42
+ providers: Record<string, IProviderConfig>;
43
+ activeProviders: ProviderInfo[];
44
+ updateProviderSettings: (provider: string, config: IProviderSetting) => void;
45
+ isLocalModel: boolean;
46
+ enableLocalModels: (enabled: boolean) => void;
47
+
48
+ // Debug and development settings
49
+ debug: boolean;
50
+ enableDebugMode: (enabled: boolean) => void;
51
+ eventLogs: boolean;
52
+ promptId: string;
53
+ setPromptId: (promptId: string) => void;
54
+ isLatestBranch: boolean;
55
+ enableLatestBranch: (enabled: boolean) => void;
56
+ autoSelectTemplate: boolean;
57
+ setAutoSelectTemplate: (enabled: boolean) => void;
58
+ contextOptimizationEnabled: boolean;
59
+ enableContextOptimization: (enabled: boolean) => void;
60
+
61
+ // Tab configuration
62
+ tabConfiguration: TabWindowConfig;
63
+ updateTabConfiguration: (config: TabVisibilityConfig) => void;
64
+ resetTabConfiguration: () => void;
65
+ }
66
 
67
+ export function useSettings(): UseSettingsReturn {
68
+ const providers = useStore(providersStore) as Record<string, IProviderConfig>;
69
  const debug = useStore(isDebugMode);
70
  const eventLogs = useStore(isEventLogsEnabled);
71
  const promptId = useStore(promptStore);
 
74
  const autoSelectTemplate = useStore(autoSelectStarterTemplate);
75
  const [activeProviders, setActiveProviders] = useState<ProviderInfo[]>([]);
76
  const contextOptimizationEnabled = useStore(enableContextOptimizationStore);
77
+ const tabConfiguration = useStore(tabConfigurationStore);
78
+ const [settings, setSettings] = useState<Settings>(() => {
79
+ const storedSettings = getLocalStorage('settings');
80
+ return {
81
+ theme: storedSettings?.theme || 'system',
82
+ language: storedSettings?.language || 'en',
83
+ notifications: storedSettings?.notifications ?? true,
84
+ eventLogs: storedSettings?.eventLogs ?? true,
85
+ timezone: storedSettings?.timezone || Intl.DateTimeFormat().resolvedOptions().timeZone,
86
+ tabConfiguration,
87
+ };
88
+ });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
89
 
90
  // writing values to cookies on change
91
  useEffect(() => {
 
109
  setActiveProviders(active);
110
  }, [providers, isLocalModel]);
111
 
112
+ const saveSettings = useCallback((newSettings: Partial<Settings>) => {
113
+ setSettings((prev) => {
114
+ const updated = { ...prev, ...newSettings };
115
+ setLocalStorage('settings', updated);
116
+
117
+ return updated;
118
+ });
119
+ }, []);
120
+
121
+ const updateProviderSettings = useCallback((provider: string, config: IProviderSetting) => {
122
+ providersStore.setKey(provider, { settings: config } as IProviderConfig);
123
+ }, []);
124
 
125
  const enableDebugMode = useCallback((enabled: boolean) => {
126
  isDebugMode.set(enabled);
 
128
  Cookies.set('isDebugEnabled', String(enabled));
129
  }, []);
130
 
131
+ const setEventLogs = useCallback((enabled: boolean) => {
132
  isEventLogsEnabled.set(enabled);
133
  logStore.logSystem(`Event logs ${enabled ? 'enabled' : 'disabled'}`);
134
  Cookies.set('isEventLogsEnabled', String(enabled));
 
144
  promptStore.set(promptId);
145
  Cookies.set('promptId', promptId);
146
  }, []);
147
+
148
  const enableLatestBranch = useCallback((enabled: boolean) => {
149
  latestBranchStore.set(enabled);
150
  logStore.logSystem(`Main branch updates ${enabled ? 'enabled' : 'disabled'}`);
 
163
  Cookies.set('contextOptimizationEnabled', String(enabled));
164
  }, []);
165
 
166
+ const setTheme = useCallback(
167
+ (theme: Settings['theme']) => {
168
+ saveSettings({ theme });
169
+ },
170
+ [saveSettings],
171
+ );
172
+
173
+ const setLanguage = useCallback(
174
+ (language: string) => {
175
+ saveSettings({ language });
176
+ },
177
+ [saveSettings],
178
+ );
179
+
180
+ const setNotifications = useCallback(
181
+ (enabled: boolean) => {
182
+ saveSettings({ notifications: enabled });
183
+ },
184
+ [saveSettings],
185
+ );
186
+
187
+ const setTimezone = useCallback(
188
+ (timezone: string) => {
189
+ saveSettings({ timezone });
190
+ },
191
+ [saveSettings],
192
+ );
193
+
194
  return {
195
+ ...settings,
196
  providers,
197
  activeProviders,
198
  updateProviderSettings,
199
+ isLocalModel,
200
+ enableLocalModels,
201
  debug,
202
  enableDebugMode,
203
  eventLogs,
204
+ setEventLogs,
 
 
205
  promptId,
206
  setPromptId,
207
  isLatestBranch,
 
210
  setAutoSelectTemplate,
211
  contextOptimizationEnabled,
212
  enableContextOptimization,
213
+ setTheme,
214
+ setLanguage,
215
+ setNotifications,
216
+ setTimezone,
217
+ settings,
218
+ tabConfiguration,
219
+ updateTabConfiguration: updateTabConfig,
220
+ resetTabConfiguration: resetTabConfig,
221
  };
222
  }
app/lib/hooks/useUpdateCheck.ts ADDED
@@ -0,0 +1,58 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState, useEffect } from 'react';
2
+ import { checkForUpdates, acknowledgeUpdate } from '~/lib/api/updates';
3
+
4
+ const LAST_ACKNOWLEDGED_VERSION_KEY = 'bolt_last_acknowledged_version';
5
+
6
+ export const useUpdateCheck = () => {
7
+ const [hasUpdate, setHasUpdate] = useState(false);
8
+ const [currentVersion, setCurrentVersion] = useState<string>('');
9
+ const [lastAcknowledgedVersion, setLastAcknowledgedVersion] = useState<string | null>(() => {
10
+ try {
11
+ return localStorage.getItem(LAST_ACKNOWLEDGED_VERSION_KEY);
12
+ } catch {
13
+ return null;
14
+ }
15
+ });
16
+
17
+ useEffect(() => {
18
+ const checkUpdate = async () => {
19
+ try {
20
+ const { available, version } = await checkForUpdates();
21
+ setCurrentVersion(version);
22
+
23
+ // Only show update if it's a new version and hasn't been acknowledged
24
+ setHasUpdate(available && version !== lastAcknowledgedVersion);
25
+ } catch (error) {
26
+ console.error('Failed to check for updates:', error);
27
+ }
28
+ };
29
+
30
+ // Check immediately and then every 30 minutes
31
+ checkUpdate();
32
+
33
+ const interval = setInterval(checkUpdate, 30 * 60 * 1000);
34
+
35
+ return () => clearInterval(interval);
36
+ }, [lastAcknowledgedVersion]);
37
+
38
+ const handleAcknowledgeUpdate = async () => {
39
+ try {
40
+ const { version } = await checkForUpdates();
41
+ await acknowledgeUpdate(version);
42
+
43
+ // Store in localStorage
44
+ try {
45
+ localStorage.setItem(LAST_ACKNOWLEDGED_VERSION_KEY, version);
46
+ } catch (error) {
47
+ console.error('Failed to persist acknowledged version:', error);
48
+ }
49
+
50
+ setLastAcknowledgedVersion(version);
51
+ setHasUpdate(false);
52
+ } catch (error) {
53
+ console.error('Failed to acknowledge update:', error);
54
+ }
55
+ };
56
+
57
+ return { hasUpdate, currentVersion, acknowledgeUpdate: handleAcknowledgeUpdate };
58
+ };
app/lib/modules/llm/providers/github.ts CHANGED
@@ -11,7 +11,8 @@ export default class GithubProvider extends BaseProvider {
11
  config = {
12
  apiTokenKey: 'GITHUB_API_KEY',
13
  };
14
- // find more in https://github.com/marketplace?type=models
 
15
  staticModels: ModelInfo[] = [
16
  { name: 'gpt-4o', label: 'GPT-4o', provider: 'Github', maxTokenAllowed: 8000 },
17
  { name: 'o1', label: 'o1-preview', provider: 'Github', maxTokenAllowed: 100000 },
 
11
  config = {
12
  apiTokenKey: 'GITHUB_API_KEY',
13
  };
14
+
15
+ // find more in https://github.com/marketplace?type=models
16
  staticModels: ModelInfo[] = [
17
  { name: 'gpt-4o', label: 'GPT-4o', provider: 'Github', maxTokenAllowed: 8000 },
18
  { name: 'o1', label: 'o1-preview', provider: 'Github', maxTokenAllowed: 100000 },
app/lib/stores/logs.ts CHANGED
@@ -10,7 +10,10 @@ export interface LogEntry {
10
  level: 'info' | 'warning' | 'error' | 'debug';
11
  message: string;
12
  details?: Record<string, any>;
13
- category: 'system' | 'provider' | 'user' | 'error';
 
 
 
14
  }
15
 
16
  const MAX_LOGS = 1000; // Maximum number of logs to keep in memory
@@ -101,18 +104,76 @@ class LogStore {
101
  return this.addLog(message, 'info', 'user', details);
102
  }
103
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
104
  // Error events
105
  logError(message: string, error?: Error | unknown, details?: Record<string, any>) {
106
- const errorDetails = {
107
- ...(details || {}),
108
- error:
109
- error instanceof Error
110
- ? {
111
- message: error.message,
112
- stack: error.stack,
113
- }
114
- : error,
115
- };
116
  return this.addLog(message, 'error', 'error', errorDetails);
117
  }
118
 
 
10
  level: 'info' | 'warning' | 'error' | 'debug';
11
  message: string;
12
  details?: Record<string, any>;
13
+ category: 'system' | 'provider' | 'user' | 'error' | 'api' | 'auth' | 'database' | 'network';
14
+ subCategory?: string;
15
+ duration?: number;
16
+ statusCode?: number;
17
  }
18
 
19
  const MAX_LOGS = 1000; // Maximum number of logs to keep in memory
 
104
  return this.addLog(message, 'info', 'user', details);
105
  }
106
 
107
+ // API Connection Logging
108
+ logAPIRequest(endpoint: string, method: string, duration: number, statusCode: number, details?: Record<string, any>) {
109
+ const message = `${method} ${endpoint} - ${statusCode} (${duration}ms)`;
110
+ const level = statusCode >= 400 ? 'error' : statusCode >= 300 ? 'warning' : 'info';
111
+
112
+ return this.addLog(message, level, 'api', {
113
+ ...details,
114
+ endpoint,
115
+ method,
116
+ duration,
117
+ statusCode,
118
+ timestamp: new Date().toISOString(),
119
+ });
120
+ }
121
+
122
+ // Authentication Logging
123
+ logAuth(
124
+ action: 'login' | 'logout' | 'token_refresh' | 'key_validation',
125
+ success: boolean,
126
+ details?: Record<string, any>,
127
+ ) {
128
+ const message = `Auth ${action} - ${success ? 'Success' : 'Failed'}`;
129
+ const level = success ? 'info' : 'error';
130
+
131
+ return this.addLog(message, level, 'auth', {
132
+ ...details,
133
+ action,
134
+ success,
135
+ timestamp: new Date().toISOString(),
136
+ });
137
+ }
138
+
139
+ // Network Status Logging
140
+ logNetworkStatus(status: 'online' | 'offline' | 'reconnecting' | 'connected', details?: Record<string, any>) {
141
+ const message = `Network ${status}`;
142
+ const level = status === 'offline' ? 'error' : status === 'reconnecting' ? 'warning' : 'info';
143
+
144
+ return this.addLog(message, level, 'network', {
145
+ ...details,
146
+ status,
147
+ timestamp: new Date().toISOString(),
148
+ });
149
+ }
150
+
151
+ // Database Operations Logging
152
+ logDatabase(operation: string, success: boolean, duration: number, details?: Record<string, any>) {
153
+ const message = `DB ${operation} - ${success ? 'Success' : 'Failed'} (${duration}ms)`;
154
+ const level = success ? 'info' : 'error';
155
+
156
+ return this.addLog(message, level, 'database', {
157
+ ...details,
158
+ operation,
159
+ success,
160
+ duration,
161
+ timestamp: new Date().toISOString(),
162
+ });
163
+ }
164
+
165
  // Error events
166
  logError(message: string, error?: Error | unknown, details?: Record<string, any>) {
167
+ const errorDetails =
168
+ error instanceof Error
169
+ ? {
170
+ name: error.name,
171
+ message: error.message,
172
+ stack: error.stack,
173
+ ...details,
174
+ }
175
+ : { error, ...details };
176
+
177
  return this.addLog(message, 'error', 'error', errorDetails);
178
  }
179
 
app/lib/stores/settings.ts CHANGED
@@ -2,6 +2,9 @@ import { atom, map } from 'nanostores';
2
  import { workbenchStore } from './workbench';
3
  import { PROVIDER_LIST } from '~/utils/constants';
4
  import type { IProviderConfig } from '~/types/model';
 
 
 
5
 
6
  export interface Shortcut {
7
  key: string;
@@ -46,7 +49,9 @@ export const providersStore = map<ProviderSetting>(initialProviderSettings);
46
 
47
  export const isDebugMode = atom(false);
48
 
49
- export const isEventLogsEnabled = atom(false);
 
 
50
 
51
  export const isLocalModelsEnabled = atom(true);
52
 
@@ -56,3 +61,48 @@ export const latestBranchStore = atom(false);
56
 
57
  export const autoSelectStarterTemplate = atom(false);
58
  export const enableContextOptimizationStore = atom(false);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2
  import { workbenchStore } from './workbench';
3
  import { PROVIDER_LIST } from '~/utils/constants';
4
  import type { IProviderConfig } from '~/types/model';
5
+ import type { TabVisibilityConfig, TabWindowConfig } from '~/components/settings/settings.types';
6
+ import { DEFAULT_TAB_CONFIG } from '~/components/settings/settings.types';
7
+ import Cookies from 'js-cookie';
8
 
9
  export interface Shortcut {
10
  key: string;
 
49
 
50
  export const isDebugMode = atom(false);
51
 
52
+ // Initialize event logs from cookie or default to false
53
+ const savedEventLogs = Cookies.get('isEventLogsEnabled');
54
+ export const isEventLogsEnabled = atom(savedEventLogs === 'true');
55
 
56
  export const isLocalModelsEnabled = atom(true);
57
 
 
61
 
62
  export const autoSelectStarterTemplate = atom(false);
63
  export const enableContextOptimizationStore = atom(false);
64
+
65
+ // Initialize tab configuration from cookie or default
66
+ const savedTabConfig = Cookies.get('tabConfiguration');
67
+ const initialTabConfig: TabWindowConfig = savedTabConfig
68
+ ? JSON.parse(savedTabConfig)
69
+ : {
70
+ userTabs: DEFAULT_TAB_CONFIG.filter((tab) => tab.window === 'user'),
71
+ developerTabs: DEFAULT_TAB_CONFIG.filter((tab) => tab.window === 'developer'),
72
+ };
73
+
74
+ export const tabConfigurationStore = map<TabWindowConfig>(initialTabConfig);
75
+
76
+ // Helper function to update tab configuration
77
+ export const updateTabConfiguration = (config: TabVisibilityConfig) => {
78
+ const currentConfig = tabConfigurationStore.get();
79
+ const isUserTab = config.window === 'user';
80
+ const targetArray = isUserTab ? 'userTabs' : 'developerTabs';
81
+
82
+ // Only update the tab in its respective window
83
+ const updatedTabs = currentConfig[targetArray].map((tab) => (tab.id === config.id ? { ...config } : tab));
84
+
85
+ // If tab doesn't exist in this window yet, add it
86
+ if (!updatedTabs.find((tab) => tab.id === config.id)) {
87
+ updatedTabs.push(config);
88
+ }
89
+
90
+ // Create new config, only updating the target window's tabs
91
+ const newConfig: TabWindowConfig = {
92
+ ...currentConfig,
93
+ [targetArray]: updatedTabs,
94
+ };
95
+
96
+ tabConfigurationStore.set(newConfig);
97
+ Cookies.set('tabConfiguration', JSON.stringify(newConfig));
98
+ };
99
+
100
+ // Helper function to reset tab configuration
101
+ export const resetTabConfiguration = () => {
102
+ const defaultConfig: TabWindowConfig = {
103
+ userTabs: DEFAULT_TAB_CONFIG.filter((tab) => tab.window === 'user'),
104
+ developerTabs: DEFAULT_TAB_CONFIG.filter((tab) => tab.window === 'developer'),
105
+ };
106
+ tabConfigurationStore.set(defaultConfig);
107
+ Cookies.set('tabConfiguration', JSON.stringify(defaultConfig));
108
+ };
app/root.tsx CHANGED
@@ -6,6 +6,8 @@ import { themeStore } from './lib/stores/theme';
6
  import { stripIndents } from './utils/stripIndent';
7
  import { createHead } from 'remix-island';
8
  import { useEffect } from 'react';
 
 
9
 
10
  import reactToastifyStyles from 'react-toastify/dist/ReactToastify.css?url';
11
  import globalStyles from './styles/index.scss?url';
@@ -70,11 +72,11 @@ export function Layout({ children }: { children: React.ReactNode }) {
70
  }, [theme]);
71
 
72
  return (
73
- <>
74
  {children}
75
  <ScrollRestoration />
76
  <Scripts />
77
- </>
78
  );
79
  }
80
 
 
6
  import { stripIndents } from './utils/stripIndent';
7
  import { createHead } from 'remix-island';
8
  import { useEffect } from 'react';
9
+ import { DndProvider } from 'react-dnd';
10
+ import { HTML5Backend } from 'react-dnd-html5-backend';
11
 
12
  import reactToastifyStyles from 'react-toastify/dist/ReactToastify.css?url';
13
  import globalStyles from './styles/index.scss?url';
 
72
  }, [theme]);
73
 
74
  return (
75
+ <DndProvider backend={HTML5Backend}>
76
  {children}
77
  <ScrollRestoration />
78
  <Scripts />
79
+ </DndProvider>
80
  );
81
  }
82
 
app/utils/localStorage.ts ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export function getLocalStorage(key: string) {
2
+ try {
3
+ const item = localStorage.getItem(key);
4
+ return item ? JSON.parse(item) : null;
5
+ } catch (error) {
6
+ console.error(`Error reading from localStorage key "${key}":`, error);
7
+ return null;
8
+ }
9
+ }
10
+
11
+ export function setLocalStorage(key: string, value: any) {
12
+ try {
13
+ localStorage.setItem(key, JSON.stringify(value));
14
+ } catch (error) {
15
+ console.error(`Error writing to localStorage key "${key}":`, error);
16
+ }
17
+ }
package.json CHANGED
@@ -92,6 +92,8 @@
92
  "nanostores": "^0.10.3",
93
  "ollama-ai-provider": "^0.15.2",
94
  "react": "^18.3.1",
 
 
95
  "react-dom": "^18.3.1",
96
  "react-hotkeys-hook": "^4.6.1",
97
  "react-icons": "^5.4.0",
 
92
  "nanostores": "^0.10.3",
93
  "ollama-ai-provider": "^0.15.2",
94
  "react": "^18.3.1",
95
+ "react-dnd": "^16.0.1",
96
+ "react-dnd-html5-backend": "^16.0.1",
97
  "react-dom": "^18.3.1",
98
  "react-hotkeys-hook": "^4.6.1",
99
  "react-icons": "^5.4.0",
pnpm-lock.yaml CHANGED
@@ -197,6 +197,12 @@ importers:
197
  react:
198
  specifier: ^18.3.1
199
  version: 18.3.1
 
 
 
 
 
 
200
  react-dom:
201
  specifier: ^18.3.1
202
  version: 18.3.1([email protected])
@@ -2049,6 +2055,15 @@ packages:
2049
  react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
2050
  react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
2051
 
 
 
 
 
 
 
 
 
 
2052
  '@react-stately/[email protected]':
2053
  resolution: {integrity: sha512-iMQSGcpaecghDIh3mZEpZfoFH3ExBwTtuBEcvZ2XnGzCgQjeYXcMdIUwAfVQLXFTdHUHGF6Gu6/dFrYsCzySBQ==}
2054
  peerDependencies:
@@ -3309,6 +3324,9 @@ packages:
3309
3310
  resolution: {integrity: sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg==}
3311
 
 
 
 
3312
3313
  resolution: {integrity: sha512-IGBwjF7tNk3cwypFNH/7bfzBcgSCbaMOD3GsaY1AU/JRrnHnYgEM0+9kQt52iZxjNsjBtJYtao146V+f8jFZNw==}
3314
  engines: {node: '>=10'}
@@ -3846,6 +3864,9 @@ packages:
3846
3847
  resolution: {integrity: sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg==}
3848
 
 
 
 
3849
3850
  resolution: {integrity: sha512-HVJyzUrLIL1c0QmviVh5E8VGyUS7xCFPS6yydaVd1UegW+ibV/CohqTH9MkOLDp5o+rb82DMo77PTuc9F/8GKw==}
3851
  engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
@@ -5028,6 +5049,24 @@ packages:
5028
  resolution: {integrity: sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==}
5029
  engines: {node: '>= 0.8'}
5030
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5031
5032
  resolution: {integrity: sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==}
5033
  peerDependencies:
@@ -5044,6 +5083,9 @@ packages:
5044
  peerDependencies:
5045
  react: '*'
5046
 
 
 
 
5047
5048
  resolution: {integrity: sha512-186Gw/vF1uRkydbsOIkcGXw7aHq0sZOCRFFjGrr7b9+nVZg4UfA4enXCaxm4fUzecU38sWfrNDitGhshuU7rdg==}
5049
  peerDependencies:
@@ -5128,6 +5170,9 @@ packages:
5128
  resolution: {integrity: sha512-yDMz9g+VaZkqBYS/ozoBJwaBhTbZo3UNYQHNRw1D3UFQB8oHB4uS/tAODO+ZLjGWmUbKnIlOWO+aaIiAxrUWHA==}
5129
  engines: {node: '>= 14.16.0'}
5130
 
 
 
 
5131
5132
  resolution: {integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==}
5133
 
@@ -8116,6 +8161,12 @@ snapshots:
8116
  react: 18.3.1
8117
  react-dom: 18.3.1([email protected])
8118
 
 
 
 
 
 
 
8119
  '@react-stately/[email protected]([email protected])':
8120
  dependencies:
8121
  '@swc/helpers': 0.5.15
@@ -9678,6 +9729,12 @@ snapshots:
9678
  miller-rabin: 4.0.1
9679
  randombytes: 2.1.0
9680
 
 
 
 
 
 
 
9681
9682
 
9683
@@ -10423,6 +10480,10 @@ snapshots:
10423
  minimalistic-assert: 1.0.1
10424
  minimalistic-crypto-utils: 1.0.1
10425
 
 
 
 
 
10426
10427
  dependencies:
10428
  lru-cache: 7.18.3
@@ -11969,6 +12030,22 @@ snapshots:
11969
  iconv-lite: 0.4.24
11970
  unpipe: 1.0.0
11971
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
11972
11973
  dependencies:
11974
  loose-envify: 1.4.0
@@ -11984,6 +12061,8 @@ snapshots:
11984
  dependencies:
11985
  react: 18.3.1
11986
 
 
 
11987
11988
  dependencies:
11989
  '@types/hast': 3.0.4
@@ -12080,6 +12159,10 @@ snapshots:
12080
 
12081
12082
 
 
 
 
 
12083
12084
 
12085
 
197
  react:
198
  specifier: ^18.3.1
199
  version: 18.3.1
200
+ react-dnd:
201
+ specifier: ^16.0.1
202
+ version: 16.0.1(@types/[email protected])(@types/[email protected])([email protected])
203
+ react-dnd-html5-backend:
204
+ specifier: ^16.0.1
205
+ version: 16.0.1
206
  react-dom:
207
  specifier: ^18.3.1
208
  version: 18.3.1([email protected])
 
2055
  react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
2056
  react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
2057
 
2058
+ '@react-dnd/[email protected]':
2059
+ resolution: {integrity: sha512-WLyfoHvxhs0V9U+GTsGilGgf2QsPl6ZZ44fnv0/b8T3nQyvzxidxsg/ZltbWssbsRDlYW8UKSQMTGotuTotZ6A==}
2060
+
2061
+ '@react-dnd/[email protected]':
2062
+ resolution: {integrity: sha512-xKCTqAK/FFauOM9Ta2pswIyT3D8AQlfrYdOi/toTPEhqCuAs1v5tcJ3Y08Izh1cJ5Jchwy9SeAXmMg6zrKs2iw==}
2063
+
2064
+ '@react-dnd/[email protected]':
2065
+ resolution: {integrity: sha512-/RVXdLvJxLg4QKvMoM5WlwNR9ViO9z8B/qPcc+C0Sa/teJY7QG7kJ441DwzOjMYEY7GmU4dj5EcGHIkKZiQZCA==}
2066
+
2067
  '@react-stately/[email protected]':
2068
  resolution: {integrity: sha512-iMQSGcpaecghDIh3mZEpZfoFH3ExBwTtuBEcvZ2XnGzCgQjeYXcMdIUwAfVQLXFTdHUHGF6Gu6/dFrYsCzySBQ==}
2069
  peerDependencies:
 
3324
3325
  resolution: {integrity: sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg==}
3326
 
3327
3328
+ resolution: {integrity: sha512-HK294sl7tbw6F6IeuK16YSBUoorvHpY8RHO+9yFfaJyCDVb6n7PRcezrOEOa2SBCqiYpemh5Jx20ZcjKdFAVng==}
3329
+
3330
3331
  resolution: {integrity: sha512-IGBwjF7tNk3cwypFNH/7bfzBcgSCbaMOD3GsaY1AU/JRrnHnYgEM0+9kQt52iZxjNsjBtJYtao146V+f8jFZNw==}
3332
  engines: {node: '>=10'}
 
3864
3865
  resolution: {integrity: sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg==}
3866
 
3867
3868
+ resolution: {integrity: sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==}
3869
+
3870
3871
  resolution: {integrity: sha512-HVJyzUrLIL1c0QmviVh5E8VGyUS7xCFPS6yydaVd1UegW+ibV/CohqTH9MkOLDp5o+rb82DMo77PTuc9F/8GKw==}
3872
  engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
 
5049
  resolution: {integrity: sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==}
5050
  engines: {node: '>= 0.8'}
5051
 
5052
5053
+ resolution: {integrity: sha512-Wu3dw5aDJmOGw8WjH1I1/yTH+vlXEL4vmjk5p+MHxP8HuHJS1lAGeIdG/hze1AvNeXWo/JgULV87LyQOr+r5jw==}
5054
+
5055
5056
+ resolution: {integrity: sha512-QeoM/i73HHu2XF9aKksIUuamHPDvRglEwdHL4jsp784BgUuWcg6mzfxT0QDdQz8Wj0qyRKx2eMg8iZtWvU4E2Q==}
5057
+ peerDependencies:
5058
+ '@types/hoist-non-react-statics': '>= 3.3.1'
5059
+ '@types/node': '>= 12'
5060
+ '@types/react': '>= 16'
5061
+ react: '>= 16.14'
5062
+ peerDependenciesMeta:
5063
+ '@types/hoist-non-react-statics':
5064
+ optional: true
5065
+ '@types/node':
5066
+ optional: true
5067
+ '@types/react':
5068
+ optional: true
5069
+
5070
5071
  resolution: {integrity: sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==}
5072
  peerDependencies:
 
5083
  peerDependencies:
5084
  react: '*'
5085
 
5086
5087
+ resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
5088
+
5089
5090
  resolution: {integrity: sha512-186Gw/vF1uRkydbsOIkcGXw7aHq0sZOCRFFjGrr7b9+nVZg4UfA4enXCaxm4fUzecU38sWfrNDitGhshuU7rdg==}
5091
  peerDependencies:
 
5170
  resolution: {integrity: sha512-yDMz9g+VaZkqBYS/ozoBJwaBhTbZo3UNYQHNRw1D3UFQB8oHB4uS/tAODO+ZLjGWmUbKnIlOWO+aaIiAxrUWHA==}
5171
  engines: {node: '>= 14.16.0'}
5172
 
5173
5174
+ resolution: {integrity: sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==}
5175
+
5176
5177
  resolution: {integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==}
5178
 
 
8161
  react: 18.3.1
8162
  react-dom: 18.3.1([email protected])
8163
 
8164
+ '@react-dnd/[email protected]': {}
8165
+
8166
+ '@react-dnd/[email protected]': {}
8167
+
8168
+ '@react-dnd/[email protected]': {}
8169
+
8170
  '@react-stately/[email protected]([email protected])':
8171
  dependencies:
8172
  '@swc/helpers': 0.5.15
 
9729
  miller-rabin: 4.0.1
9730
  randombytes: 2.1.0
9731
 
9732
9733
+ dependencies:
9734
+ '@react-dnd/asap': 5.0.2
9735
+ '@react-dnd/invariant': 4.0.2
9736
+ redux: 4.2.1
9737
+
9738
9739
 
9740
 
10480
  minimalistic-assert: 1.0.1
10481
  minimalistic-crypto-utils: 1.0.1
10482
 
10483
10484
+ dependencies:
10485
+ react-is: 16.13.1
10486
+
10487
10488
  dependencies:
10489
  lru-cache: 7.18.3
 
12030
  iconv-lite: 0.4.24
12031
  unpipe: 1.0.0
12032
 
12033
12034
+ dependencies:
12035
+ dnd-core: 16.0.1
12036
+
12037
12038
+ dependencies:
12039
+ '@react-dnd/invariant': 4.0.2
12040
+ '@react-dnd/shallowequal': 4.0.2
12041
+ dnd-core: 16.0.1
12042
+ fast-deep-equal: 3.1.3
12043
+ hoist-non-react-statics: 3.3.2
12044
+ react: 18.3.1
12045
+ optionalDependencies:
12046
+ '@types/node': 22.10.1
12047
+ '@types/react': 18.3.12
12048
+
12049
12050
  dependencies:
12051
  loose-envify: 1.4.0
 
12061
  dependencies:
12062
  react: 18.3.1
12063
 
12064
12065
+
12066
12067
  dependencies:
12068
  '@types/hast': 3.0.4
 
12159
 
12160
12161
 
12162
12163
+ dependencies:
12164
+ '@babel/runtime': 7.26.0
12165
+
12166
12167
 
12168