Stijnus commited on
Commit
a94330e
·
1 Parent(s): 6d98aff
app/components/settings/CHANGELOG.md ADDED
@@ -0,0 +1,189 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Settings Components Changelog
2
+
3
+ All notable changes to the settings components will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [Unreleased]
9
+
10
+ ### Added
11
+
12
+ - New Settings Dashboard with improved UI/UX
13
+ - Tab management system with drag-and-drop reordering
14
+ - Enhanced developer tools window
15
+ - Bulk update functionality for Ollama models
16
+ - System performance monitoring in Debug tab
17
+ - Data import/export functionality
18
+ - Enhanced event logging system
19
+ - Profile customization options
20
+ - Auto energy saver mode in TaskManager
21
+ - Energy savings tracking and statistics
22
+ - Persistent settings storage using localStorage
23
+
24
+ ### Changed
25
+
26
+ - Removed green status indicators from TaskManagerTab for cleaner UI
27
+ - Changed connection status indicators to use neutral colors
28
+ - Updated energy saver mode indicator to use neutral colors
29
+ - Simplified process status display in TaskManager
30
+ - Improved tab organization with window-specific grouping
31
+ - Enhanced settings persistence with better localStorage handling
32
+
33
+ ### Fixed
34
+
35
+ - Status indicator consistency across dark/light themes
36
+ - Process status updates during energy saver mode
37
+ - UI rendering issues in dark mode
38
+ - Tab visibility state management
39
+ - Settings import/export reliability
40
+
41
+ ## [1.0.0] - Initial Release
42
+
43
+ ### Added
44
+
45
+ #### User Window Components
46
+
47
+ - **Profile Tab**
48
+
49
+ - User profile and account settings management
50
+ - Avatar customization
51
+ - Account preferences
52
+
53
+ - **Settings Tab**
54
+
55
+ - Application preferences configuration
56
+ - UI behavior customization
57
+ - General settings management
58
+
59
+ - **Notifications Tab**
60
+
61
+ - Real-time notification center
62
+ - Unread notification tracking
63
+ - Notification preferences
64
+ - Support for different notification types
65
+ - Integration with logStore
66
+
67
+ - **Cloud Providers Tab**
68
+
69
+ - Cloud-based AI provider configuration
70
+ - API key management
71
+ - Cloud model selection
72
+ - Provider-specific settings
73
+ - Status monitoring
74
+
75
+ - **Local Providers Tab**
76
+
77
+ - Local AI model management
78
+ - Ollama integration and model updates
79
+ - LM Studio configuration
80
+ - Local inference settings
81
+ - Model download and updates
82
+
83
+ - **Task Manager Tab**
84
+
85
+ - System resource monitoring
86
+ - Process management
87
+ - Performance metrics and graphs
88
+ - Battery status monitoring
89
+ - Energy saving features
90
+ - Alert configurations
91
+
92
+ - **Connections Tab**
93
+
94
+ - Network status monitoring
95
+ - GitHub integration
96
+ - Connection health metrics
97
+ - Secure token storage
98
+ - Auto-reconnect settings
99
+
100
+ - **Debug Tab**
101
+
102
+ - System diagnostics
103
+ - Performance monitoring
104
+ - Error tracking
105
+ - Provider status checks
106
+
107
+ - **Event Logs Tab**
108
+
109
+ - Comprehensive system logs
110
+ - Filtered log views
111
+ - Log management tools
112
+ - Error tracking
113
+ - Performance metrics
114
+
115
+ - **Update Tab**
116
+ - Version management
117
+ - Update notifications
118
+ - Release notes
119
+ - Auto-update configuration
120
+
121
+ ### Technical Enhancements
122
+
123
+ #### State Management
124
+
125
+ - Implemented Nano Stores for efficient state handling
126
+ - Added persistent settings storage
127
+ - Real-time state synchronization
128
+ - Provider state management
129
+
130
+ #### Performance
131
+
132
+ - Lazy loading of tab contents
133
+ - Efficient DOM updates
134
+ - Optimized animations
135
+ - Resource monitoring
136
+
137
+ #### Accessibility
138
+
139
+ - Keyboard navigation support
140
+ - Screen reader compatibility
141
+ - Focus management
142
+ - ARIA attributes implementation
143
+
144
+ #### UI/UX Features
145
+
146
+ - Drag & Drop tab management
147
+ - Dynamic status updates
148
+ - Responsive design with Framer Motion
149
+ - Dark/Light mode support
150
+ - Enhanced provider management
151
+ - Resource monitoring dashboard
152
+
153
+ ### Dependencies
154
+
155
+ - Radix UI for accessible components
156
+ - Framer Motion for animations
157
+ - React DnD for drag and drop
158
+ - Nano Stores for state management
159
+
160
+ ## Future Plans
161
+
162
+ - Additional customization options
163
+ - Enhanced theme support
164
+ - Extended API integrations
165
+ - Advanced monitoring capabilities
166
+ - Custom provider plugins
167
+ - Enhanced resource management
168
+ - Advanced debugging features
169
+
170
+ ## Historical Changes
171
+
172
+ ### Task Manager
173
+
174
+ - Added real-time system metrics monitoring
175
+ - Implemented process tracking functionality
176
+ - Added battery status monitoring
177
+ - Integrated energy saving features
178
+
179
+ ### Connections
180
+
181
+ - Added GitHub integration
182
+ - Implemented secure token storage
183
+ - Added connection status indicators
184
+
185
+ ### Notifications
186
+
187
+ - Implemented centralized notification system
188
+ - Added support for different notification types (error, warning, update)
189
+ - Integrated with logStore for persistent storage
app/components/settings/connections/ConnectionsTab.tsx CHANGED
@@ -194,7 +194,7 @@ export default function ConnectionsTab() {
194
  )}
195
 
196
  {connection.user && (
197
- <span className="text-sm text-green-500 flex items-center gap-1">
198
  <div className="i-ph:check-circle w-4 h-4" />
199
  Connected to GitHub
200
  </span>
 
194
  )}
195
 
196
  {connection.user && (
197
+ <span className="text-sm text-bolt-elements-textSecondary flex items-center gap-1">
198
  <div className="i-ph:check-circle w-4 h-4" />
199
  Connected to GitHub
200
  </span>
app/components/settings/debug/DebugTab.tsx CHANGED
@@ -4,13 +4,6 @@ import { classNames } from '~/utils/classNames';
4
  import { logStore } from '~/lib/stores/logs';
5
  import type { LogEntry } from '~/lib/stores/logs';
6
 
7
- interface ProviderStatus {
8
- id: string;
9
- name: string;
10
- status: 'online' | 'offline' | 'error';
11
- error?: string;
12
- }
13
-
14
  interface SystemInfo {
15
  os: string;
16
  arch: string;
@@ -90,14 +83,24 @@ interface SystemInfo {
90
  };
91
  }
92
 
 
 
 
 
 
 
 
 
 
 
93
  export default function DebugTab() {
94
- const [providerStatuses, setProviderStatuses] = useState<ProviderStatus[]>([]);
95
  const [systemInfo, setSystemInfo] = useState<SystemInfo | null>(null);
 
96
  const [loading, setLoading] = useState({
97
  systemInfo: false,
98
- providers: false,
99
  performance: false,
100
  errors: false,
 
101
  });
102
  const [errorLog, setErrorLog] = useState<{
103
  errors: any[];
@@ -109,8 +112,8 @@ export default function DebugTab() {
109
 
110
  // Fetch initial data
111
  useEffect(() => {
112
- checkProviderStatus();
113
  getSystemInfo();
 
114
  }, []);
115
 
116
  // Set up error listeners when component mounts
@@ -146,75 +149,6 @@ export default function DebugTab() {
146
  };
147
  }, []);
148
 
149
- const checkProviderStatus = async () => {
150
- try {
151
- setLoading((prev) => ({ ...prev, providers: true }));
152
-
153
- // Fetch real provider statuses
154
- const providers: ProviderStatus[] = [];
155
-
156
- // Check OpenAI status
157
- try {
158
- const openaiResponse = await fetch('/api/providers/openai/status');
159
- providers.push({
160
- id: 'openai',
161
- name: 'OpenAI',
162
- status: openaiResponse.ok ? 'online' : 'error',
163
- error: !openaiResponse.ok ? 'API Error' : undefined,
164
- });
165
- } catch {
166
- providers.push({ id: 'openai', name: 'OpenAI', status: 'offline' });
167
- }
168
-
169
- // Check Anthropic status
170
- try {
171
- const anthropicResponse = await fetch('/api/providers/anthropic/status');
172
- providers.push({
173
- id: 'anthropic',
174
- name: 'Anthropic',
175
- status: anthropicResponse.ok ? 'online' : 'error',
176
- error: !anthropicResponse.ok ? 'API Error' : undefined,
177
- });
178
- } catch {
179
- providers.push({ id: 'anthropic', name: 'Anthropic', status: 'offline' });
180
- }
181
-
182
- // Check Local Models status
183
- try {
184
- const localResponse = await fetch('/api/providers/local/status');
185
- providers.push({
186
- id: 'local',
187
- name: 'Local Models',
188
- status: localResponse.ok ? 'online' : 'error',
189
- error: !localResponse.ok ? 'API Error' : undefined,
190
- });
191
- } catch {
192
- providers.push({ id: 'local', name: 'Local Models', status: 'offline' });
193
- }
194
-
195
- // Check Ollama status
196
- try {
197
- const ollamaResponse = await fetch('/api/providers/ollama/status');
198
- providers.push({
199
- id: 'ollama',
200
- name: 'Ollama',
201
- status: ollamaResponse.ok ? 'online' : 'error',
202
- error: !ollamaResponse.ok ? 'API Error' : undefined,
203
- });
204
- } catch {
205
- providers.push({ id: 'ollama', name: 'Ollama', status: 'offline' });
206
- }
207
-
208
- setProviderStatuses(providers);
209
- toast.success('Provider status updated');
210
- } catch (error) {
211
- toast.error('Failed to check provider status');
212
- console.error('Failed to check provider status:', error);
213
- } finally {
214
- setLoading((prev) => ({ ...prev, providers: false }));
215
- }
216
- };
217
-
218
  const getSystemInfo = async () => {
219
  try {
220
  setLoading((prev) => ({ ...prev, systemInfo: true }));
@@ -360,6 +294,26 @@ export default function DebugTab() {
360
  }
361
  };
362
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
363
  // Helper function to format bytes to human readable format
364
  const formatBytes = (bytes: number) => {
365
  const units = ['B', 'KB', 'MB', 'GB'];
@@ -509,29 +463,40 @@ export default function DebugTab() {
509
  }
510
  };
511
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
512
  return (
513
  <div className="flex flex-col gap-6">
514
  {/* Action Buttons */}
515
  <div className="flex flex-wrap gap-4">
516
- <button
517
- onClick={checkProviderStatus}
518
- disabled={loading.providers}
519
- className={classNames(
520
- 'flex items-center gap-2 px-4 py-2 text-sm font-medium rounded-lg transition-colors',
521
- 'bg-[#F5F5F5] dark:bg-[#1A1A1A] hover:bg-[#E5E5E5] dark:hover:bg-[#2A2A2A]',
522
- 'text-bolt-elements-textPrimary dark:text-bolt-elements-textPrimary',
523
- 'focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2',
524
- { 'opacity-50 cursor-not-allowed': loading.providers },
525
- )}
526
- >
527
- {loading.providers ? (
528
- <div className="i-ph:spinner-gap w-4 h-4 animate-spin" />
529
- ) : (
530
- <div className="i-ph:plug w-4 h-4" />
531
- )}
532
- Check Providers
533
- </button>
534
-
535
  <button
536
  onClick={getSystemInfo}
537
  disabled={loading.systemInfo}
@@ -588,6 +553,19 @@ export default function DebugTab() {
588
  )}
589
  Check Errors
590
  </button>
 
 
 
 
 
 
 
 
 
 
 
 
 
591
  </div>
592
 
593
  {/* Error Log Display */}
@@ -772,53 +750,6 @@ export default function DebugTab() {
772
  )}
773
  </div>
774
 
775
- {/* Provider Status */}
776
- <div className="p-6 rounded-xl bg-white dark:bg-[#0A0A0A] border border-[#E5E5E5] dark:border-[#1A1A1A]">
777
- <div className="flex items-center justify-between mb-4">
778
- <div className="flex items-center gap-3">
779
- <div className="i-ph:robot text-purple-500 w-5 h-5" />
780
- <h3 className="text-base font-medium text-bolt-elements-textPrimary">Provider Status</h3>
781
- </div>
782
- <button
783
- onClick={checkProviderStatus}
784
- className={classNames(
785
- 'flex items-center gap-2 px-3 py-2 rounded-lg text-sm',
786
- 'bg-[#F5F5F5] dark:bg-[#1A1A1A] hover:bg-[#E5E5E5] dark:hover:bg-[#2A2A2A]',
787
- 'text-bolt-elements-textPrimary dark:text-bolt-elements-textPrimary',
788
- 'transition-colors duration-200',
789
- { 'opacity-50 cursor-not-allowed': loading.providers },
790
- )}
791
- disabled={loading.providers}
792
- >
793
- <div className={classNames('i-ph:arrows-clockwise w-4 h-4', loading.providers ? 'animate-spin' : '')} />
794
- Refresh
795
- </button>
796
- </div>
797
- <div className="grid grid-cols-2 gap-4">
798
- {providerStatuses.map((provider) => (
799
- <div
800
- key={provider.id}
801
- className="flex items-center justify-between p-3 rounded-lg bg-[#F8F8F8] dark:bg-[#141414]"
802
- >
803
- <div className="flex items-center gap-3">
804
- <div
805
- className={classNames(
806
- 'w-2 h-2 rounded-full',
807
- provider.status === 'online'
808
- ? 'bg-green-500'
809
- : provider.status === 'offline'
810
- ? 'bg-red-500'
811
- : 'bg-yellow-500',
812
- )}
813
- />
814
- <span className="text-sm text-bolt-elements-textPrimary">{provider.name}</span>
815
- </div>
816
- <span className="text-xs text-bolt-elements-textSecondary capitalize">{provider.status}</span>
817
- </div>
818
- ))}
819
- </div>
820
- </div>
821
-
822
  {/* Performance Metrics */}
823
  <div className="p-6 rounded-xl bg-white dark:bg-[#0A0A0A] border border-[#E5E5E5] dark:border-[#1A1A1A]">
824
  <div className="flex items-center justify-between mb-4">
@@ -906,6 +837,82 @@ export default function DebugTab() {
906
  )}
907
  </div>
908
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
909
  {/* Error Check */}
910
  <div className="p-6 rounded-xl bg-white dark:bg-[#0A0A0A] border border-[#E5E5E5] dark:border-[#1A1A1A]">
911
  <div className="flex items-center justify-between mb-4">
 
4
  import { logStore } from '~/lib/stores/logs';
5
  import type { LogEntry } from '~/lib/stores/logs';
6
 
 
 
 
 
 
 
 
7
  interface SystemInfo {
8
  os: string;
9
  arch: string;
 
83
  };
84
  }
85
 
86
+ interface WebAppInfo {
87
+ name: string;
88
+ version: string;
89
+ description: string;
90
+ license: string;
91
+ nodeVersion: string;
92
+ dependencies: { [key: string]: string };
93
+ devDependencies: { [key: string]: string };
94
+ }
95
+
96
  export default function DebugTab() {
 
97
  const [systemInfo, setSystemInfo] = useState<SystemInfo | null>(null);
98
+ const [webAppInfo, setWebAppInfo] = useState<WebAppInfo | null>(null);
99
  const [loading, setLoading] = useState({
100
  systemInfo: false,
 
101
  performance: false,
102
  errors: false,
103
+ webAppInfo: false,
104
  });
105
  const [errorLog, setErrorLog] = useState<{
106
  errors: any[];
 
112
 
113
  // Fetch initial data
114
  useEffect(() => {
 
115
  getSystemInfo();
116
+ getWebAppInfo();
117
  }, []);
118
 
119
  // Set up error listeners when component mounts
 
149
  };
150
  }, []);
151
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
152
  const getSystemInfo = async () => {
153
  try {
154
  setLoading((prev) => ({ ...prev, systemInfo: true }));
 
294
  }
295
  };
296
 
297
+ const getWebAppInfo = async () => {
298
+ try {
299
+ setLoading((prev) => ({ ...prev, webAppInfo: true }));
300
+
301
+ const response = await fetch('/api/system/app-info');
302
+
303
+ if (!response.ok) {
304
+ throw new Error('Failed to fetch webapp info');
305
+ }
306
+
307
+ const data = await response.json();
308
+ setWebAppInfo(data as WebAppInfo);
309
+ } catch (error) {
310
+ console.error('Failed to fetch webapp info:', error);
311
+ toast.error('Failed to fetch webapp information');
312
+ } finally {
313
+ setLoading((prev) => ({ ...prev, webAppInfo: false }));
314
+ }
315
+ };
316
+
317
  // Helper function to format bytes to human readable format
318
  const formatBytes = (bytes: number) => {
319
  const units = ['B', 'KB', 'MB', 'GB'];
 
463
  }
464
  };
465
 
466
+ const exportDebugInfo = () => {
467
+ try {
468
+ const debugData = {
469
+ timestamp: new Date().toISOString(),
470
+ system: systemInfo,
471
+ webApp: webAppInfo,
472
+ errors: errorLog.errors,
473
+ performance: {
474
+ memory: (performance as any).memory || {},
475
+ timing: performance.timing,
476
+ navigation: performance.navigation,
477
+ },
478
+ };
479
+
480
+ const blob = new Blob([JSON.stringify(debugData, null, 2)], { type: 'application/json' });
481
+ const url = window.URL.createObjectURL(blob);
482
+ const a = document.createElement('a');
483
+ a.href = url;
484
+ a.download = `bolt-debug-info-${new Date().toISOString()}.json`;
485
+ document.body.appendChild(a);
486
+ a.click();
487
+ window.URL.revokeObjectURL(url);
488
+ document.body.removeChild(a);
489
+ toast.success('Debug information exported successfully');
490
+ } catch (error) {
491
+ console.error('Failed to export debug info:', error);
492
+ toast.error('Failed to export debug information');
493
+ }
494
+ };
495
+
496
  return (
497
  <div className="flex flex-col gap-6">
498
  {/* Action Buttons */}
499
  <div className="flex flex-wrap gap-4">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
500
  <button
501
  onClick={getSystemInfo}
502
  disabled={loading.systemInfo}
 
553
  )}
554
  Check Errors
555
  </button>
556
+
557
+ <button
558
+ onClick={exportDebugInfo}
559
+ className={classNames(
560
+ 'flex items-center gap-2 px-4 py-2 text-sm font-medium rounded-lg transition-colors',
561
+ 'bg-[#F5F5F5] dark:bg-[#1A1A1A] hover:bg-[#E5E5E5] dark:hover:bg-[#2A2A2A]',
562
+ 'text-bolt-elements-textPrimary dark:text-bolt-elements-textPrimary',
563
+ 'focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2',
564
+ )}
565
+ >
566
+ <div className="i-ph:download w-4 h-4" />
567
+ Export Debug Info
568
+ </button>
569
  </div>
570
 
571
  {/* Error Log Display */}
 
750
  )}
751
  </div>
752
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
753
  {/* Performance Metrics */}
754
  <div className="p-6 rounded-xl bg-white dark:bg-[#0A0A0A] border border-[#E5E5E5] dark:border-[#1A1A1A]">
755
  <div className="flex items-center justify-between mb-4">
 
837
  )}
838
  </div>
839
 
840
+ {/* WebApp Information */}
841
+ <div className="p-6 rounded-xl bg-white dark:bg-[#0A0A0A] border border-[#E5E5E5] dark:border-[#1A1A1A]">
842
+ <div className="flex items-center justify-between mb-4">
843
+ <div className="flex items-center gap-3">
844
+ <div className="i-ph:info text-blue-500 w-5 h-5" />
845
+ <h3 className="text-base font-medium text-bolt-elements-textPrimary">WebApp Information</h3>
846
+ </div>
847
+ <button
848
+ onClick={getWebAppInfo}
849
+ className={classNames(
850
+ 'flex items-center gap-2 px-3 py-2 rounded-lg text-sm',
851
+ 'bg-[#F5F5F5] dark:bg-[#1A1A1A] hover:bg-[#E5E5E5] dark:hover:bg-[#2A2A2A]',
852
+ 'text-bolt-elements-textPrimary dark:text-bolt-elements-textPrimary',
853
+ 'transition-colors duration-200',
854
+ { 'opacity-50 cursor-not-allowed': loading.webAppInfo },
855
+ )}
856
+ disabled={loading.webAppInfo}
857
+ >
858
+ <div className={classNames('i-ph:arrows-clockwise w-4 h-4', loading.webAppInfo ? 'animate-spin' : '')} />
859
+ Refresh
860
+ </button>
861
+ </div>
862
+ {webAppInfo ? (
863
+ <div className="grid grid-cols-2 gap-4">
864
+ <div className="space-y-2">
865
+ <div className="text-sm flex items-center gap-2">
866
+ <div className="i-ph:app-window text-bolt-elements-textSecondary w-4 h-4" />
867
+ <span className="text-bolt-elements-textSecondary">Name: </span>
868
+ <span className="text-bolt-elements-textPrimary">{webAppInfo.name}</span>
869
+ </div>
870
+ <div className="text-sm flex items-center gap-2">
871
+ <div className="i-ph:tag text-bolt-elements-textSecondary w-4 h-4" />
872
+ <span className="text-bolt-elements-textSecondary">Version: </span>
873
+ <span className="text-bolt-elements-textPrimary">{webAppInfo.version}</span>
874
+ </div>
875
+ <div className="text-sm flex items-center gap-2">
876
+ <div className="i-ph:file-text text-bolt-elements-textSecondary w-4 h-4" />
877
+ <span className="text-bolt-elements-textSecondary">Description: </span>
878
+ <span className="text-bolt-elements-textPrimary">{webAppInfo.description}</span>
879
+ </div>
880
+ <div className="text-sm flex items-center gap-2">
881
+ <div className="i-ph:certificate text-bolt-elements-textSecondary w-4 h-4" />
882
+ <span className="text-bolt-elements-textSecondary">License: </span>
883
+ <span className="text-bolt-elements-textPrimary">{webAppInfo.license}</span>
884
+ </div>
885
+ <div className="text-sm flex items-center gap-2">
886
+ <div className="i-ph:node text-bolt-elements-textSecondary w-4 h-4" />
887
+ <span className="text-bolt-elements-textSecondary">Node Version: </span>
888
+ <span className="text-bolt-elements-textPrimary">{webAppInfo.nodeVersion}</span>
889
+ </div>
890
+ </div>
891
+ <div className="space-y-2">
892
+ <div className="text-sm">
893
+ <div className="flex items-center gap-2 mb-2">
894
+ <div className="i-ph:package text-bolt-elements-textSecondary w-4 h-4" />
895
+ <span className="text-bolt-elements-textSecondary">Key Dependencies:</span>
896
+ </div>
897
+ <div className="pl-6 space-y-1">
898
+ {Object.entries(webAppInfo.dependencies)
899
+ .filter(([key]) => ['react', '@remix-run/react', 'next', 'typescript'].includes(key))
900
+ .map(([key, version]) => (
901
+ <div key={key} className="text-xs text-bolt-elements-textPrimary">
902
+ {key}: {version}
903
+ </div>
904
+ ))}
905
+ </div>
906
+ </div>
907
+ </div>
908
+ </div>
909
+ ) : (
910
+ <div className="text-sm text-bolt-elements-textSecondary">
911
+ {loading.webAppInfo ? 'Loading webapp information...' : 'No webapp information available'}
912
+ </div>
913
+ )}
914
+ </div>
915
+
916
  {/* Error Check */}
917
  <div className="p-6 rounded-xl bg-white dark:bg-[#0A0A0A] border border-[#E5E5E5] dark:border-[#1A1A1A]">
918
  <div className="flex items-center justify-between mb-4">
app/components/settings/event-logs/EventLogsTab.tsx CHANGED
@@ -1,515 +1,421 @@
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
 
305
  const handleExportLogs = useCallback(() => {
306
- try {
307
- const logText = logStore
308
- .getLogs()
309
- .map(
310
- (log) =>
311
- `[${log.level.toUpperCase()}] ${log.timestamp} - ${log.message}${
312
- log.details ? '\nDetails: ' + JSON.stringify(log.details, null, 2) : ''
313
- }`,
314
- )
315
- .join('\n\n');
316
-
317
- const blob = new Blob([logText], { type: 'text/plain' });
318
- const url = URL.createObjectURL(blob);
319
- const a = document.createElement('a');
320
- a.href = url;
321
- a.download = `event-logs-${new Date().toISOString()}.txt`;
322
- document.body.appendChild(a);
323
- a.click();
324
- document.body.removeChild(a);
325
- URL.revokeObjectURL(url);
326
- toast.success('Logs exported successfully');
327
- } catch (error) {
328
- toast.error('Failed to export logs');
329
- console.error('Export error:', error);
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>
 
1
+ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
2
+ import { motion } from 'framer-motion';
3
  import { Switch } from '~/components/ui/Switch';
4
  import { logStore, type LogEntry } from '~/lib/stores/logs';
5
  import { useStore } from '@nanostores/react';
6
  import { classNames } from '~/utils/classNames';
 
7
 
8
  interface SelectOption {
9
  value: string;
10
  label: string;
11
+ icon?: string;
12
  color?: string;
13
  }
14
 
15
  const logLevelOptions: SelectOption[] = [
16
+ {
17
+ value: 'all',
18
+ label: 'All Levels',
19
+ icon: 'i-ph:funnel',
20
+ color: '#9333ea',
21
+ },
22
+ {
23
+ value: 'info',
24
+ label: 'Info',
25
+ icon: 'i-ph:info',
26
+ color: '#3b82f6',
27
+ },
28
+ {
29
+ value: 'warning',
30
+ label: 'Warning',
31
+ icon: 'i-ph:warning',
32
+ color: '#f59e0b',
33
+ },
34
+ {
35
+ value: 'error',
36
+ label: 'Error',
37
+ icon: 'i-ph:x-circle',
38
+ color: '#ef4444',
39
+ },
40
+ {
41
+ value: 'debug',
42
+ label: 'Debug',
43
+ icon: 'i-ph:bug',
44
+ color: '#6b7280',
45
+ },
46
  ];
47
 
48
  const logCategoryOptions: SelectOption[] = [
49
+ {
50
+ value: 'all',
51
+ label: 'All Categories',
52
+ icon: 'i-ph:squares-four',
53
+ color: '#9333ea',
54
+ },
55
+ {
56
+ value: 'api',
57
+ label: 'API',
58
+ icon: 'i-ph:cloud',
59
+ color: '#3b82f6',
60
+ },
61
+ {
62
+ value: 'auth',
63
+ label: 'Auth',
64
+ icon: 'i-ph:key',
65
+ color: '#f59e0b',
66
+ },
67
+ {
68
+ value: 'database',
69
+ label: 'Database',
70
+ icon: 'i-ph:database',
71
+ color: '#10b981',
72
+ },
73
+ {
74
+ value: 'network',
75
+ label: 'Network',
76
+ icon: 'i-ph:wifi-high',
77
+ color: '#6366f1',
78
+ },
79
+ {
80
+ value: 'performance',
81
+ label: 'Performance',
82
+ icon: 'i-ph:chart-line-up',
83
+ color: '#8b5cf6',
84
+ },
85
  ];
86
 
87
+ interface LogEntryItemProps {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
88
  log: LogEntry;
89
  isExpanded: boolean;
90
  use24Hour: boolean;
91
+ showTimestamp: boolean;
92
+ }
 
93
 
94
+ const LogEntryItem = ({ log, isExpanded: forceExpanded, use24Hour, showTimestamp }: LogEntryItemProps) => {
95
+ const [localExpanded, setLocalExpanded] = useState(forceExpanded);
96
+
97
+ // Update local expanded state when forceExpanded changes
98
  useEffect(() => {
99
+ setLocalExpanded(forceExpanded);
100
  }, [forceExpanded]);
101
 
102
+ const timestamp = useMemo(() => {
 
 
 
 
 
 
 
 
 
 
 
 
103
  const date = new Date(log.timestamp);
104
+
105
+ if (use24Hour) {
106
+ return date.toLocaleTimeString('en-US', { hour12: false });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
107
  }
108
+
109
+ return date.toLocaleTimeString('en-US', { hour12: true });
110
  }, [log.timestamp, use24Hour]);
111
 
112
+ const levelColor = useMemo(() => {
113
+ switch (log.level) {
114
+ case 'error':
115
+ return 'text-red-500 bg-red-50 dark:bg-red-500/10';
116
+ case 'warning':
117
+ return 'text-yellow-500 bg-yellow-50 dark:bg-yellow-500/10';
118
+ case 'debug':
119
+ return 'text-gray-500 bg-gray-50 dark:bg-gray-500/10';
120
+ default:
121
+ return 'text-blue-500 bg-blue-50 dark:bg-blue-500/10';
122
+ }
123
+ }, [log.level]);
124
+
125
  return (
126
+ <div className="p-3 hover:bg-gray-50 dark:hover:bg-gray-800/50 transition-colors">
127
+ <div className="flex items-start gap-3">
128
+ <div className={classNames('px-2 py-0.5 rounded text-xs font-medium uppercase', levelColor)}>{log.level}</div>
129
+ {showTimestamp && <div className="text-sm text-bolt-elements-textTertiary">{timestamp}</div>}
130
+ <div className="flex-grow">
131
+ <div className="text-sm text-bolt-elements-textPrimary">{log.message}</div>
132
+ {log.details && (
133
+ <button
134
+ onClick={() => setLocalExpanded(!localExpanded)}
135
+ className="mt-1 text-xs text-bolt-elements-textTertiary hover:text-bolt-elements-textSecondary"
136
+ >
137
+ {localExpanded ? 'Hide' : 'Show'} Details
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
138
  </button>
139
+ )}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
140
  </div>
141
+ {log.category && (
142
+ <div className="px-2 py-0.5 rounded-full text-xs bg-gray-100 dark:bg-gray-800 text-bolt-elements-textSecondary">
 
 
 
 
 
 
143
  {log.category}
144
+ </div>
145
+ )}
146
  </div>
147
+ {localExpanded && log.details && (
148
+ <div className="mt-2 p-2 rounded bg-gray-50 dark:bg-gray-800/50">
149
+ <pre className="text-xs text-bolt-elements-textSecondary whitespace-pre-wrap">
 
 
 
 
 
 
 
150
  {JSON.stringify(log.details, null, 2)}
151
  </pre>
152
+ </div>
153
  )}
154
  </div>
155
  );
156
  };
157
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
158
  export function EventLogsTab() {
159
  const logs = useStore(logStore.logs);
160
+ const [selectedLevel, setSelectedLevel] = useState('all');
161
+ const [selectedCategory, setSelectedCategory] = useState('all');
 
162
  const [searchQuery, setSearchQuery] = useState('');
163
+ const [use24Hour, setUse24Hour] = useState(false);
164
+ const [autoExpand, setAutoExpand] = useState(false);
165
+ const [showTimestamps, setShowTimestamps] = useState(true);
166
+ const [showLevelFilter, setShowLevelFilter] = useState(false);
167
+ const [showCategoryFilter, setShowCategoryFilter] = useState(false);
168
  const [isRefreshing, setIsRefreshing] = useState(false);
169
  const logsContainerRef = useRef<HTMLDivElement>(null);
170
+ const levelFilterRef = useRef<HTMLDivElement>(null);
171
+ const categoryFilterRef = useRef<HTMLDivElement>(null);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
172
 
173
  const filteredLogs = useMemo(() => {
174
+ return logStore.getFilteredLogs(
175
+ selectedLevel === 'all' ? undefined : (selectedLevel as LogEntry['level']),
176
+ selectedCategory === 'all' ? undefined : (selectedCategory as LogEntry['category']),
177
+ searchQuery,
178
+ );
179
+ }, [logs, selectedLevel, selectedCategory, searchQuery]);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
180
 
181
  const handleExportLogs = useCallback(() => {
182
+ const exportData = {
183
+ timestamp: new Date().toISOString(),
184
+ logs: filteredLogs,
185
+ filters: {
186
+ level: selectedLevel,
187
+ category: selectedCategory,
188
+ searchQuery,
189
+ },
190
+ };
191
+
192
+ const blob = new Blob([JSON.stringify(exportData, null, 2)], { type: 'application/json' });
193
+ const url = URL.createObjectURL(blob);
194
+ const a = document.createElement('a');
195
+ a.href = url;
196
+ a.download = `bolt-logs-${new Date().toISOString()}.json`;
197
+ document.body.appendChild(a);
198
+ a.click();
199
+ document.body.removeChild(a);
200
+ URL.revokeObjectURL(url);
201
+ }, [filteredLogs, selectedLevel, selectedCategory, searchQuery]);
202
+
203
+ const handleRefresh = useCallback(async () => {
204
+ setIsRefreshing(true);
205
+ await logStore.refreshLogs();
206
+ setTimeout(() => setIsRefreshing(false), 500); // Keep animation visible for at least 500ms
207
  }, []);
208
 
209
+ // Close filters when clicking outside
210
+ useEffect(() => {
211
+ const handleClickOutside = (event: MouseEvent) => {
212
+ if (levelFilterRef.current && !levelFilterRef.current.contains(event.target as Node)) {
213
+ setShowLevelFilter(false);
214
+ }
215
 
216
+ if (categoryFilterRef.current && !categoryFilterRef.current.contains(event.target as Node)) {
217
+ setShowCategoryFilter(false);
218
+ }
219
+ };
220
 
221
+ document.addEventListener('mousedown', handleClickOutside);
 
 
 
222
 
223
+ return () => {
224
+ document.removeEventListener('mousedown', handleClickOutside);
225
+ };
226
+ }, []);
227
 
228
+ const selectedLevelOption = logLevelOptions.find((opt) => opt.value === selectedLevel);
229
+ const selectedCategoryOption = logCategoryOptions.find((opt) => opt.value === selectedCategory);
 
 
230
 
231
  return (
232
  <div className="flex flex-col h-full gap-4 p-6">
233
+ {/* Header */}
234
+ <div className="flex items-center justify-between">
235
+ <div className="flex items-center gap-3">
236
+ <div className="i-ph:list text-xl text-purple-500" />
237
+ <div>
238
+ <h2 className="text-lg font-semibold">Event Logs</h2>
239
+ <p className="text-sm text-bolt-elements-textSecondary">Track system events and debug information</p>
 
 
 
240
  </div>
241
+ </div>
242
+ <div className="flex items-center gap-4">
243
  <motion.button
244
  onClick={handleRefresh}
245
+ className="p-2 rounded-lg text-purple-600 dark:text-purple-400 hover:bg-purple-50 dark:hover:bg-purple-900/20 transition-all duration-200"
 
 
 
 
 
 
 
 
 
246
  whileHover={{ scale: 1.05 }}
247
  whileTap={{ scale: 0.95 }}
248
+ animate={isRefreshing ? { rotate: 360 } : {}}
249
+ transition={isRefreshing ? { duration: 1, repeat: Infinity, ease: 'linear' } : {}}
250
  >
251
+ <div className="i-ph:arrows-clockwise text-xl" />
 
 
252
  </motion.button>
253
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
254
  </div>
255
 
256
+ {/* Top Controls */}
257
+ <div className="flex items-center gap-4">
258
+ {/* Search */}
259
+ <div className="flex-grow relative">
 
 
260
  <input
261
  type="text"
262
  placeholder="Search logs..."
 
 
 
 
 
 
 
 
263
  value={searchQuery}
264
  onChange={(e) => setSearchQuery(e.target.value)}
265
+ className="w-full px-3 py-1.5 pl-9 rounded-lg text-sm bg-white/50 dark:bg-gray-800/30 border border-gray-200/50 dark:border-gray-700/50"
266
  />
267
+ <div className="absolute left-3 top-1/2 -translate-y-1/2 text-bolt-elements-textTertiary">
268
+ <div className="i-ph:magnifying-glass text-base" />
269
+ </div>
270
  </div>
271
 
272
+ {/* Right Controls */}
273
+ <div className="flex items-center gap-4">
274
+ <div className="flex items-center gap-2">
275
+ <Switch checked={showTimestamps} onCheckedChange={setShowTimestamps} />
276
+ <span className="text-sm text-bolt-elements-textSecondary whitespace-nowrap">Show Timestamps</span>
277
+ </div>
 
 
 
 
 
 
 
 
 
278
 
279
+ <div className="flex items-center gap-2">
280
+ <Switch checked={use24Hour} onCheckedChange={setUse24Hour} />
281
+ <span className="text-sm text-bolt-elements-textSecondary whitespace-nowrap">24h Time</span>
282
+ </div>
283
+
284
+ <div className="flex items-center gap-2">
285
+ <Switch checked={autoExpand} onCheckedChange={setAutoExpand} />
286
+ <span className="text-sm text-bolt-elements-textSecondary whitespace-nowrap">Auto Expand</span>
287
+ </div>
 
 
 
288
 
 
 
 
 
 
 
 
289
  <motion.button
290
  onClick={handleExportLogs}
291
+ className="flex items-center gap-2 px-4 py-1.5 rounded-lg text-sm bg-purple-500 hover:bg-purple-600 text-white transition-colors"
292
  whileHover={{ scale: 1.02 }}
293
  whileTap={{ scale: 0.98 }}
294
  >
295
+ <div className="i-ph:download text-base" />
296
+ Export Logs
297
  </motion.button>
298
+ </div>
299
+ </div>
300
+
301
+ {/* Filters */}
302
+ <div className="flex items-center gap-4 -mt-2">
303
+ {/* Level Filter */}
304
+ <div ref={levelFilterRef} className="relative">
305
+ <button
306
+ onClick={() => {
307
+ setShowLevelFilter(!showLevelFilter);
308
+ setShowCategoryFilter(false);
309
+ }}
310
+ className="flex items-center gap-2 px-2 py-1 rounded hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors"
311
  >
312
+ <div
313
+ className={classNames('text-sm', selectedLevelOption?.icon || 'i-ph:funnel')}
314
+ style={{ color: selectedLevelOption?.color }}
315
+ />
316
+ <div className="text-sm text-bolt-elements-textSecondary">{selectedLevelOption?.label || 'All Levels'}</div>
317
+ <div
318
+ className={classNames(
319
+ 'i-ph:caret-down text-sm text-bolt-elements-textTertiary transition-transform',
320
+ showLevelFilter ? 'rotate-180' : '',
321
+ )}
322
+ />
323
+ </button>
324
+
325
+ {showLevelFilter && (
326
+ <div className="absolute left-0 top-full mt-1 z-20">
327
+ <div className="p-1 rounded-lg shadow-lg bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700">
328
+ {logLevelOptions.map((option) => (
329
+ <button
330
+ key={option.value}
331
+ onClick={() => {
332
+ setSelectedLevel(option.value);
333
+ setShowLevelFilter(false);
334
+ }}
335
+ className={classNames(
336
+ 'flex items-center gap-2 w-full px-3 py-1.5 rounded-md text-sm transition-colors',
337
+ option.value === selectedLevel
338
+ ? 'bg-purple-100 dark:bg-purple-800/40 text-purple-900 dark:text-purple-200'
339
+ : 'hover:bg-gray-100 dark:hover:bg-gray-700/50',
340
+ )}
341
+ >
342
+ <div className={classNames(option.icon, 'text-base', option.color)} />
343
+ <span>{option.label}</span>
344
+ </button>
345
+ ))}
346
+ </div>
347
+ </div>
348
+ )}
349
+ </div>
350
+
351
+ <div className="w-px h-4 bg-gray-200 dark:bg-gray-700" />
352
+
353
+ {/* Category Filter */}
354
+ <div ref={categoryFilterRef} className="relative">
355
+ <button
356
+ onClick={() => {
357
+ setShowCategoryFilter(!showCategoryFilter);
358
+ setShowLevelFilter(false);
359
+ }}
360
+ className="flex items-center gap-2 px-2 py-1 rounded hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors"
361
+ >
362
+ <div
363
+ className={classNames('text-sm', selectedCategoryOption?.icon || 'i-ph:squares-four')}
364
+ style={{ color: selectedCategoryOption?.color }}
365
+ />
366
+ <div className="text-sm text-bolt-elements-textSecondary">
367
+ {selectedCategoryOption?.label || 'All Categories'}
368
+ </div>
369
+ <div
370
+ className={classNames(
371
+ 'i-ph:caret-down text-sm text-bolt-elements-textTertiary transition-transform',
372
+ showCategoryFilter ? 'rotate-180' : '',
373
+ )}
374
+ />
375
+ </button>
376
+
377
+ {showCategoryFilter && (
378
+ <div className="absolute left-0 top-full mt-1 z-20">
379
+ <div className="p-1 rounded-lg shadow-lg bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700">
380
+ {logCategoryOptions.map((option) => (
381
+ <button
382
+ key={option.value}
383
+ onClick={() => {
384
+ setSelectedCategory(option.value);
385
+ setShowCategoryFilter(false);
386
+ }}
387
+ className={classNames(
388
+ 'flex items-center gap-2 w-full px-3 py-1.5 rounded-md text-sm transition-colors',
389
+ option.value === selectedCategory
390
+ ? 'bg-purple-100 dark:bg-purple-800/40 text-purple-900 dark:text-purple-200'
391
+ : 'hover:bg-gray-100 dark:hover:bg-gray-700/50',
392
+ )}
393
+ >
394
+ <div className={classNames(option.icon, 'text-base', option.color)} />
395
+ <span>{option.label}</span>
396
+ </button>
397
+ ))}
398
+ </div>
399
+ </div>
400
+ )}
401
+ </div>
402
+ </div>
403
+
404
+ {/* Logs Container */}
405
+ <div
406
+ ref={logsContainerRef}
407
+ className="flex-grow overflow-y-auto rounded-lg bg-white/50 dark:bg-gray-800/30 border border-gray-200/50 dark:border-gray-700/50"
408
+ >
409
+ <div className="divide-y divide-gray-200/50 dark:divide-gray-700/50">
410
+ {filteredLogs.map((log) => (
411
+ <LogEntryItem
412
+ key={log.id}
413
+ log={log}
414
+ isExpanded={autoExpand}
415
+ use24Hour={use24Hour}
416
+ showTimestamp={showTimestamps}
417
+ />
418
+ ))}
419
  </div>
420
  </div>
421
  </div>
app/components/settings/task-manager/TaskManagerTab.tsx CHANGED
@@ -11,6 +11,7 @@ import {
11
  Tooltip,
12
  Legend,
13
  } from 'chart.js';
 
14
 
15
  // Register ChartJS components
16
  ChartJS.register(CategoryScale, LinearScale, PointElement, LineElement, Title, Tooltip, Legend);
@@ -120,8 +121,18 @@ export default function TaskManagerTab() {
120
  metrics: false,
121
  processes: false,
122
  });
123
- const [energySaverMode, setEnergySaverMode] = useState(false);
124
- const [autoEnergySaver, setAutoEnergySaver] = useState(true);
 
 
 
 
 
 
 
 
 
 
125
  const [energySavings, setEnergySavings] = useState<EnergySavings>({
126
  updatesReduced: 0,
127
  timeInSaverMode: 0,
@@ -130,6 +141,26 @@ export default function TaskManagerTab() {
130
 
131
  const saverModeStartTime = useRef<number | null>(null);
132
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
133
  // Calculate energy savings
134
  const updateEnergySavings = useCallback(() => {
135
  if (!energySaverMode) {
@@ -163,17 +194,25 @@ export default function TaskManagerTab() {
163
  }, [energySaverMode]);
164
 
165
  useEffect((): (() => void) | undefined => {
166
- if (energySaverMode) {
167
- const savingsInterval = setInterval(updateEnergySavings, 1000);
168
- return () => clearInterval(savingsInterval);
 
 
 
 
 
169
  }
170
 
171
- return undefined;
 
 
172
  }, [energySaverMode, updateEnergySavings]);
173
 
174
- // Auto energy saver effect
175
  useEffect((): (() => void) | undefined => {
176
  if (!autoEnergySaver) {
 
 
177
  return undefined;
178
  }
179
 
@@ -194,18 +233,6 @@ export default function TaskManagerTab() {
194
  return () => clearInterval(batteryCheckInterval);
195
  }, [autoEnergySaver]);
196
 
197
- const getStatusColor = (status: 'active' | 'idle' | 'suspended'): string => {
198
- if (status === 'active') {
199
- return 'text-green-500';
200
- }
201
-
202
- if (status === 'suspended') {
203
- return 'text-yellow-500';
204
- }
205
-
206
- return 'text-gray-400';
207
- };
208
-
209
  const getUsageColor = (usage: number): string => {
210
  if (usage > 80) {
211
  return 'text-red-500';
@@ -215,7 +242,7 @@ export default function TaskManagerTab() {
215
  return 'text-yellow-500';
216
  }
217
 
218
- return 'text-green-500';
219
  };
220
 
221
  const getImpactColor = (impact: 'high' | 'medium' | 'low'): string => {
@@ -227,7 +254,7 @@ export default function TaskManagerTab() {
227
  return 'text-yellow-500';
228
  }
229
 
230
- return 'text-green-500';
231
  };
232
 
233
  const renderUsageGraph = (data: number[], label: string, color: string) => {
@@ -359,7 +386,7 @@ export default function TaskManagerTab() {
359
  type: 'Network',
360
  cpuUsage: Math.random() * 5,
361
  memoryUsage: Math.random() * 50,
362
- status: 'active',
363
  lastUpdate: new Date().toISOString(),
364
  impact: 'high',
365
  },
@@ -368,7 +395,7 @@ export default function TaskManagerTab() {
368
  type: 'Animation',
369
  cpuUsage: Math.random() * 3,
370
  memoryUsage: Math.random() * 30,
371
- status: 'active',
372
  lastUpdate: new Date().toISOString(),
373
  impact: 'medium',
374
  },
@@ -386,7 +413,7 @@ export default function TaskManagerTab() {
386
  type: 'Storage',
387
  cpuUsage: Math.random() * 1,
388
  memoryUsage: Math.random() * 15,
389
- status: 'active',
390
  lastUpdate: new Date().toISOString(),
391
  impact: 'low',
392
  },
@@ -395,7 +422,7 @@ export default function TaskManagerTab() {
395
  type: 'Network',
396
  cpuUsage: Math.random() * 2,
397
  memoryUsage: Math.random() * 10,
398
- status: 'active',
399
  lastUpdate: new Date().toISOString(),
400
  impact: 'medium',
401
  },
@@ -444,7 +471,7 @@ export default function TaskManagerTab() {
444
  type="checkbox"
445
  id="autoEnergySaver"
446
  checked={autoEnergySaver}
447
- onChange={(e) => setAutoEnergySaver(e.target.checked)}
448
  className="form-checkbox h-4 w-4 text-purple-600 rounded border-gray-300 dark:border-gray-700"
449
  />
450
  <label htmlFor="autoEnergySaver" className="text-sm text-bolt-elements-textSecondary">
@@ -456,7 +483,7 @@ export default function TaskManagerTab() {
456
  type="checkbox"
457
  id="energySaver"
458
  checked={energySaverMode}
459
- onChange={(e) => !autoEnergySaver && setEnergySaverMode(e.target.checked)}
460
  disabled={autoEnergySaver}
461
  className="form-checkbox h-4 w-4 text-purple-600 rounded border-gray-300 dark:border-gray-700 disabled:opacity-50"
462
  />
@@ -465,7 +492,7 @@ export default function TaskManagerTab() {
465
  className={classNames('text-sm text-bolt-elements-textSecondary', { 'opacity-50': autoEnergySaver })}
466
  >
467
  Energy Saver
468
- {energySaverMode && <span className="ml-2 text-xs text-green-500">Active</span>}
469
  </label>
470
  </div>
471
  </div>
@@ -502,7 +529,7 @@ export default function TaskManagerTab() {
502
  <p className="text-lg font-medium text-bolt-elements-textPrimary">
503
  {Math.round(metrics.battery.level)}%
504
  {metrics.battery.charging && (
505
- <span className="ml-2 text-green-500">
506
  <div className="i-ph:lightning-fill w-4 h-4 inline-block" />
507
  </span>
508
  )}
@@ -597,10 +624,9 @@ export default function TaskManagerTab() {
597
  </span>
598
  </td>
599
  <td className="py-3 px-4">
600
- <div className="flex items-center gap-2">
601
- <div className={classNames('w-2 h-2 rounded-full', getStatusColor(process.status))} />
602
- <span className="text-sm text-bolt-elements-textSecondary capitalize">{process.status}</span>
603
- </div>
604
  </td>
605
  <td className="py-3 px-4">
606
  <span className={classNames('text-sm', getImpactColor(process.impact))}>{process.impact}</span>
 
11
  Tooltip,
12
  Legend,
13
  } from 'chart.js';
14
+ import { toast } from 'react-toastify'; // Import toast
15
 
16
  // Register ChartJS components
17
  ChartJS.register(CategoryScale, LinearScale, PointElement, LineElement, Title, Tooltip, Legend);
 
121
  metrics: false,
122
  processes: false,
123
  });
124
+ const [energySaverMode, setEnergySaverMode] = useState<boolean>(() => {
125
+ // Initialize from localStorage, default to false
126
+ const saved = localStorage.getItem('energySaverMode');
127
+ return saved ? JSON.parse(saved) : false;
128
+ });
129
+
130
+ const [autoEnergySaver, setAutoEnergySaver] = useState<boolean>(() => {
131
+ // Initialize from localStorage, default to false
132
+ const saved = localStorage.getItem('autoEnergySaver');
133
+ return saved ? JSON.parse(saved) : false;
134
+ });
135
+
136
  const [energySavings, setEnergySavings] = useState<EnergySavings>({
137
  updatesReduced: 0,
138
  timeInSaverMode: 0,
 
141
 
142
  const saverModeStartTime = useRef<number | null>(null);
143
 
144
+ // Handle energy saver mode changes
145
+ const handleEnergySaverChange = (checked: boolean) => {
146
+ setEnergySaverMode(checked);
147
+ localStorage.setItem('energySaverMode', JSON.stringify(checked));
148
+ toast.success(checked ? 'Energy Saver mode enabled' : 'Energy Saver mode disabled');
149
+ };
150
+
151
+ // Handle auto energy saver changes
152
+ const handleAutoEnergySaverChange = (checked: boolean) => {
153
+ setAutoEnergySaver(checked);
154
+ localStorage.setItem('autoEnergySaver', JSON.stringify(checked));
155
+ toast.success(checked ? 'Auto Energy Saver enabled' : 'Auto Energy Saver disabled');
156
+
157
+ if (!checked) {
158
+ // When disabling auto mode, also disable energy saver mode
159
+ setEnergySaverMode(false);
160
+ localStorage.setItem('energySaverMode', 'false');
161
+ }
162
+ };
163
+
164
  // Calculate energy savings
165
  const updateEnergySavings = useCallback(() => {
166
  if (!energySaverMode) {
 
194
  }, [energySaverMode]);
195
 
196
  useEffect((): (() => void) | undefined => {
197
+ if (!energySaverMode) {
198
+ // Clear any existing intervals and reset savings when disabled
199
+ setEnergySavings({
200
+ updatesReduced: 0,
201
+ timeInSaverMode: 0,
202
+ estimatedEnergySaved: 0,
203
+ });
204
+ return undefined;
205
  }
206
 
207
+ const savingsInterval = setInterval(updateEnergySavings, 1000);
208
+
209
+ return () => clearInterval(savingsInterval);
210
  }, [energySaverMode, updateEnergySavings]);
211
 
 
212
  useEffect((): (() => void) | undefined => {
213
  if (!autoEnergySaver) {
214
+ // If auto mode is disabled, clear any forced energy saver state
215
+ setEnergySaverMode(false);
216
  return undefined;
217
  }
218
 
 
233
  return () => clearInterval(batteryCheckInterval);
234
  }, [autoEnergySaver]);
235
 
 
 
 
 
 
 
 
 
 
 
 
 
236
  const getUsageColor = (usage: number): string => {
237
  if (usage > 80) {
238
  return 'text-red-500';
 
242
  return 'text-yellow-500';
243
  }
244
 
245
+ return 'text-gray-500';
246
  };
247
 
248
  const getImpactColor = (impact: 'high' | 'medium' | 'low'): string => {
 
254
  return 'text-yellow-500';
255
  }
256
 
257
+ return 'text-gray-500';
258
  };
259
 
260
  const renderUsageGraph = (data: number[], label: string, color: string) => {
 
386
  type: 'Network',
387
  cpuUsage: Math.random() * 5,
388
  memoryUsage: Math.random() * 50,
389
+ status: 'idle',
390
  lastUpdate: new Date().toISOString(),
391
  impact: 'high',
392
  },
 
395
  type: 'Animation',
396
  cpuUsage: Math.random() * 3,
397
  memoryUsage: Math.random() * 30,
398
+ status: 'idle',
399
  lastUpdate: new Date().toISOString(),
400
  impact: 'medium',
401
  },
 
413
  type: 'Storage',
414
  cpuUsage: Math.random() * 1,
415
  memoryUsage: Math.random() * 15,
416
+ status: 'idle',
417
  lastUpdate: new Date().toISOString(),
418
  impact: 'low',
419
  },
 
422
  type: 'Network',
423
  cpuUsage: Math.random() * 2,
424
  memoryUsage: Math.random() * 10,
425
+ status: 'idle',
426
  lastUpdate: new Date().toISOString(),
427
  impact: 'medium',
428
  },
 
471
  type="checkbox"
472
  id="autoEnergySaver"
473
  checked={autoEnergySaver}
474
+ onChange={(e) => handleAutoEnergySaverChange(e.target.checked)}
475
  className="form-checkbox h-4 w-4 text-purple-600 rounded border-gray-300 dark:border-gray-700"
476
  />
477
  <label htmlFor="autoEnergySaver" className="text-sm text-bolt-elements-textSecondary">
 
483
  type="checkbox"
484
  id="energySaver"
485
  checked={energySaverMode}
486
+ onChange={(e) => !autoEnergySaver && handleEnergySaverChange(e.target.checked)}
487
  disabled={autoEnergySaver}
488
  className="form-checkbox h-4 w-4 text-purple-600 rounded border-gray-300 dark:border-gray-700 disabled:opacity-50"
489
  />
 
492
  className={classNames('text-sm text-bolt-elements-textSecondary', { 'opacity-50': autoEnergySaver })}
493
  >
494
  Energy Saver
495
+ {energySaverMode && <span className="ml-2 text-xs text-bolt-elements-textSecondary">Active</span>}
496
  </label>
497
  </div>
498
  </div>
 
529
  <p className="text-lg font-medium text-bolt-elements-textPrimary">
530
  {Math.round(metrics.battery.level)}%
531
  {metrics.battery.charging && (
532
+ <span className="ml-2 text-bolt-elements-textSecondary">
533
  <div className="i-ph:lightning-fill w-4 h-4 inline-block" />
534
  </span>
535
  )}
 
624
  </span>
625
  </td>
626
  <td className="py-3 px-4">
627
+ <span className={classNames('text-sm text-bolt-elements-textSecondary capitalize')}>
628
+ {process.status}
629
+ </span>
 
630
  </td>
631
  <td className="py-3 px-4">
632
  <span className={classNames('text-sm', getImpactColor(process.impact))}>{process.impact}</span>
app/components/settings/update/UpdateTab.tsx CHANGED
@@ -707,7 +707,7 @@ const UpdateTab = () => {
707
  <div className="max-h-[400px] overflow-y-auto">
708
  {categorizeChangelog(updateInfo.changelog).map(([category, messages]) => (
709
  <div key={category} className="border-b last:border-b-0 border-bolt-elements-borderColor">
710
- <div className="p-3 bg-bolt-elements-bg-depth-4">
711
  <h5 className="text-sm font-medium text-bolt-elements-textPrimary">
712
  {category}
713
  <span className="ml-2 text-xs text-bolt-elements-textSecondary">
 
707
  <div className="max-h-[400px] overflow-y-auto">
708
  {categorizeChangelog(updateInfo.changelog).map(([category, messages]) => (
709
  <div key={category} className="border-b last:border-b-0 border-bolt-elements-borderColor">
710
+ <div className="p-3 bg-[#EAEAEA] dark:bg-[#2A2A2A]">
711
  <h5 className="text-sm font-medium text-bolt-elements-textPrimary">
712
  {category}
713
  <span className="ml-2 text-xs text-bolt-elements-textSecondary">
app/lib/stores/logs.ts CHANGED
@@ -10,10 +10,12 @@ export interface LogEntry {
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
@@ -109,6 +111,8 @@ class LogStore {
109
  level: LogEntry['level'] = 'info',
110
  category: LogEntry['category'] = 'system',
111
  details?: Record<string, any>,
 
 
112
  ) {
113
  const id = this._generateId();
114
  const entry: LogEntry = {
@@ -118,6 +122,8 @@ class LogStore {
118
  message,
119
  details,
120
  category,
 
 
121
  };
122
 
123
  this._logs.setKey(id, entry);
@@ -262,6 +268,94 @@ class LogStore {
262
  this._readLogs.clear();
263
  this._saveReadLogs();
264
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
265
  }
266
 
267
  export const logStore = new LogStore();
 
10
  level: 'info' | 'warning' | 'error' | 'debug';
11
  message: string;
12
  details?: Record<string, any>;
13
+ category: 'system' | 'provider' | 'user' | 'error' | 'api' | 'auth' | 'database' | 'network' | 'performance';
14
  subCategory?: string;
15
  duration?: number;
16
  statusCode?: number;
17
+ source?: string;
18
+ stack?: string;
19
  }
20
 
21
  const MAX_LOGS = 1000; // Maximum number of logs to keep in memory
 
111
  level: LogEntry['level'] = 'info',
112
  category: LogEntry['category'] = 'system',
113
  details?: Record<string, any>,
114
+ statusCode?: number,
115
+ duration?: number,
116
  ) {
117
  const id = this._generateId();
118
  const entry: LogEntry = {
 
122
  message,
123
  details,
124
  category,
125
+ statusCode,
126
+ duration,
127
  };
128
 
129
  this._logs.setKey(id, entry);
 
268
  this._readLogs.clear();
269
  this._saveReadLogs();
270
  }
271
+
272
+ // Network request logging
273
+ logNetworkRequest(
274
+ method: string,
275
+ url: string,
276
+ statusCode: number,
277
+ duration: number,
278
+ requestData?: any,
279
+ responseData?: any,
280
+ ) {
281
+ this.addLog(
282
+ `${method} ${url}`,
283
+ statusCode >= 400 ? 'error' : 'info',
284
+ 'network',
285
+ {
286
+ method,
287
+ url,
288
+ statusCode,
289
+ duration,
290
+ request: requestData,
291
+ response: responseData,
292
+ },
293
+ statusCode,
294
+ duration,
295
+ );
296
+ }
297
+
298
+ // Authentication events
299
+ logAuthEvent(event: string, success: boolean, details?: Record<string, any>) {
300
+ this.addLog(`Auth ${event} ${success ? 'succeeded' : 'failed'}`, success ? 'info' : 'error', 'auth', details);
301
+ }
302
+
303
+ // API interactions
304
+ logApiCall(
305
+ endpoint: string,
306
+ method: string,
307
+ statusCode: number,
308
+ duration: number,
309
+ requestData?: any,
310
+ responseData?: any,
311
+ ) {
312
+ this.addLog(
313
+ `API ${method} ${endpoint}`,
314
+ statusCode >= 400 ? 'error' : 'info',
315
+ 'api',
316
+ {
317
+ endpoint,
318
+ method,
319
+ statusCode,
320
+ duration,
321
+ request: requestData,
322
+ response: responseData,
323
+ },
324
+ statusCode,
325
+ duration,
326
+ );
327
+ }
328
+
329
+ // Performance monitoring
330
+ logPerformance(operation: string, duration: number, details?: Record<string, any>) {
331
+ this.addLog(
332
+ `Performance: ${operation}`,
333
+ duration > 1000 ? 'warning' : 'info',
334
+ 'performance',
335
+ {
336
+ operation,
337
+ duration,
338
+ ...details,
339
+ },
340
+ undefined,
341
+ duration,
342
+ );
343
+ }
344
+
345
+ // Error logging with stack trace
346
+ logErrorWithStack(error: Error, category: LogEntry['category'] = 'error', details?: Record<string, any>) {
347
+ this.addLog(error.message, 'error', category, {
348
+ ...details,
349
+ name: error.name,
350
+ stack: error.stack,
351
+ });
352
+ }
353
+
354
+ // Refresh logs (useful for real-time updates)
355
+ refreshLogs() {
356
+ const currentLogs = this._logs.get();
357
+ this._logs.set({ ...currentLogs });
358
+ }
359
  }
360
 
361
  export const logStore = new LogStore();
app/routes/api.system.app-info.ts ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { ActionFunctionArgs } from '@remix-run/cloudflare';
2
+ import { json } from '@remix-run/cloudflare';
3
+
4
+ interface PackageJson {
5
+ name: string;
6
+ version: string;
7
+ description: string;
8
+ license: string;
9
+ dependencies: Record<string, string>;
10
+ devDependencies: Record<string, string>;
11
+ }
12
+
13
+ const packageJson = {
14
+ name: 'bolt.diy',
15
+ version: '0.1.0',
16
+ description: 'A DIY LLM interface',
17
+ license: 'MIT',
18
+ dependencies: {
19
+ '@remix-run/cloudflare': '^2.0.0',
20
+ react: '^18.0.0',
21
+ 'react-dom': '^18.0.0',
22
+ typescript: '^5.0.0',
23
+ },
24
+ devDependencies: {
25
+ '@types/react': '^18.0.0',
26
+ '@types/react-dom': '^18.0.0',
27
+ },
28
+ } as PackageJson;
29
+
30
+ export const action = async ({ request: _request }: ActionFunctionArgs) => {
31
+ try {
32
+ return json({
33
+ name: packageJson.name,
34
+ version: packageJson.version,
35
+ description: packageJson.description,
36
+ license: packageJson.license,
37
+ nodeVersion: process.version,
38
+ dependencies: packageJson.dependencies,
39
+ devDependencies: packageJson.devDependencies,
40
+ });
41
+ } catch (error) {
42
+ console.error('Failed to get webapp info:', error);
43
+ return json({ error: 'Failed to get webapp information' }, { status: 500 });
44
+ }
45
+ };
changelogUI.md DELETED
@@ -1,214 +0,0 @@
1
- # Bolt DIY UI Overhaul
2
-
3
- ## New User Interface Features
4
-
5
- ### 🎨 Redesigned Control Panel
6
-
7
- The Bolt DIY interface has been completely redesigned with a modern, intuitive layout featuring two main components:
8
-
9
- 1. **Users Window** - Main control panel for regular users
10
- 2. **Developer Window** - Advanced settings and debugging tools
11
-
12
- ### 💡 Core Features
13
-
14
- - **Drag & Drop Tab Management**: Customize tab order in both User and Developer windows
15
- - **Dynamic Status Updates**: Real-time status indicators for updates, notifications, and system health
16
- - **Responsive Design**: Beautiful transitions and animations using Framer Motion
17
- - **Dark/Light Mode Support**: Full theme support with consistent styling
18
- - **Improved Accessibility**: Using Radix UI primitives for better accessibility
19
- - **Enhanced Provider Management**: Split view for local and cloud providers
20
- - **Resource Monitoring**: New Task Manager for system performance tracking
21
-
22
- ### 🎯 Tab Overview
23
-
24
- #### User Window Tabs
25
-
26
- 1. **Profile**
27
-
28
- - Manage user profile and account settings
29
- - Avatar customization
30
- - Account preferences
31
-
32
- 2. **Settings**
33
-
34
- - Configure application preferences
35
- - Customize UI behavior
36
- - Manage general settings
37
-
38
- 3. **Notifications**
39
-
40
- - Real-time notification center
41
- - Unread notification tracking
42
- - Notification preferences
43
-
44
- 4. **Features**
45
-
46
- - Explore new and upcoming features
47
- - Feature preview toggles
48
- - Early access options
49
-
50
- 5. **Data**
51
-
52
- - Data management tools
53
- - Storage settings
54
- - Backup and restore options
55
-
56
- 6. **Cloud Providers**
57
-
58
- - Configure cloud-based AI providers
59
- - API key management
60
- - Cloud model selection
61
- - Provider-specific settings
62
- - Status monitoring for each provider
63
-
64
- 7. **Local Providers**
65
-
66
- - Manage local AI models
67
- - Ollama integration and model updates
68
- - LM Studio configuration
69
- - Local inference settings
70
- - Model download and updates
71
-
72
- 8. **Task Manager**
73
-
74
- - System resource monitoring
75
- - Process management
76
- - Performance metrics
77
- - Resource usage graphs
78
- - Alert configurations
79
-
80
- 9. **Connection**
81
-
82
- - Network status monitoring
83
- - Connection health metrics
84
- - Troubleshooting tools
85
- - Latency tracking
86
- - Auto-reconnect settings
87
-
88
- 10. **Debug**
89
-
90
- - System diagnostics
91
- - Performance monitoring
92
- - Error tracking
93
- - Provider status checks
94
- - System information
95
-
96
- 11. **Event Logs**
97
-
98
- - Comprehensive system logs
99
- - Filtered log views
100
- - Log management tools
101
- - Error tracking
102
- - Performance metrics
103
-
104
- 12. **Update**
105
- - Version management
106
- - Update notifications
107
- - Release notes
108
- - Auto-update configuration
109
-
110
- #### Developer Window Enhancements
111
-
112
- - **Advanced Tab Management**
113
-
114
- - Fine-grained control over tab visibility
115
- - Custom tab ordering
116
- - Tab permission management
117
- - Category-based organization
118
-
119
- - **Developer Tools**
120
- - Enhanced debugging capabilities
121
- - System metrics and monitoring
122
- - Performance optimization tools
123
- - Advanced logging features
124
-
125
- ### 🚀 UI Improvements
126
-
127
- 1. **Enhanced Navigation**
128
-
129
- - Intuitive back navigation
130
- - Breadcrumb-style header
131
- - Context-aware menu system
132
- - Improved tab organization
133
-
134
- 2. **Status Indicators**
135
-
136
- - Dynamic update badges
137
- - Real-time connection status
138
- - System health monitoring
139
- - Provider status tracking
140
-
141
- 3. **Profile Integration**
142
-
143
- - Quick access profile menu
144
- - Avatar support
145
- - Fast settings access
146
- - Personalization options
147
-
148
- 4. **Accessibility Features**
149
- - Keyboard navigation
150
- - Screen reader support
151
- - Focus management
152
- - ARIA attributes
153
-
154
- ### 🛠 Technical Enhancements
155
-
156
- - **State Management**
157
-
158
- - Nano Stores for efficient state handling
159
- - Persistent settings storage
160
- - Real-time state synchronization
161
- - Provider state management
162
-
163
- - **Performance Optimizations**
164
-
165
- - Lazy loading of tab contents
166
- - Efficient DOM updates
167
- - Optimized animations
168
- - Resource monitoring
169
-
170
- - **Developer Experience**
171
- - Improved error handling
172
- - Better debugging tools
173
- - Enhanced logging system
174
- - Performance profiling
175
-
176
- ### 🎯 Future Roadmap
177
-
178
- - [ ] Additional customization options
179
- - [ ] Enhanced theme support
180
- - [ ] More developer tools
181
- - [ ] Extended API integrations
182
- - [ ] Advanced monitoring capabilities
183
- - [ ] Custom provider plugins
184
- - [ ] Enhanced resource management
185
- - [ ] Advanced debugging features
186
-
187
- ## 🔧 Technical Details
188
-
189
- ### Dependencies
190
-
191
- - Radix UI for accessible components
192
- - Framer Motion for animations
193
- - React DnD for drag and drop
194
- - Nano Stores for state management
195
-
196
- ### Browser Support
197
-
198
- - Modern browsers (Chrome, Firefox, Safari, Edge)
199
- - Progressive enhancement for older browsers
200
-
201
- ### Performance
202
-
203
- - Optimized bundle size
204
- - Efficient state updates
205
- - Minimal re-renders
206
- - Resource-aware operations
207
-
208
- ## 📝 Contributing
209
-
210
- We welcome contributions! Please see our contributing guidelines for more information.
211
-
212
- ## 📄 License
213
-
214
- MIT License - see LICENSE for details