Stijnus commited on
Commit
e39f16e
·
1 Parent(s): e716ca5

Settings UI enhancement

Browse files

Date & Time Display
Added a real-time clock component in the sidebar

Event Logs System
Implemented an EventLogsTab component for system monitoring
Provides a structured way to:
Track user interactions
Monitor system events
Display activity history

app/components/chat/ImportFolderButton.tsx CHANGED
@@ -3,6 +3,7 @@ import type { Message } from 'ai';
3
  import { toast } from 'react-toastify';
4
  import { MAX_FILES, isBinaryFile, shouldIncludeFile } from '~/utils/fileUtils';
5
  import { createChatFromFolder } from '~/utils/folderImport';
 
6
 
7
  interface ImportFolderButtonProps {
8
  className?: string;
@@ -16,9 +17,15 @@ export const ImportFolderButton: React.FC<ImportFolderButtonProps> = ({ classNam
16
  const allFiles = Array.from(e.target.files || []);
17
 
18
  if (allFiles.length > MAX_FILES) {
 
 
 
 
 
19
  toast.error(
20
  `This folder contains ${allFiles.length.toLocaleString()} files. This product is not yet optimized for very large projects. Please select a folder with fewer than ${MAX_FILES.toLocaleString()} files.`,
21
  );
 
22
  return;
23
  }
24
 
@@ -31,7 +38,10 @@ export const ImportFolderButton: React.FC<ImportFolderButtonProps> = ({ classNam
31
  const filteredFiles = allFiles.filter((file) => shouldIncludeFile(file.webkitRelativePath));
32
 
33
  if (filteredFiles.length === 0) {
 
 
34
  toast.error('No files found in the selected folder');
 
35
  return;
36
  }
37
 
@@ -48,11 +58,18 @@ export const ImportFolderButton: React.FC<ImportFolderButtonProps> = ({ classNam
48
  .map((f) => f.file.webkitRelativePath.split('/').slice(1).join('/'));
49
 
50
  if (textFiles.length === 0) {
 
 
51
  toast.error('No text files found in the selected folder');
 
52
  return;
53
  }
54
 
55
  if (binaryFilePaths.length > 0) {
 
 
 
 
56
  toast.info(`Skipping ${binaryFilePaths.length} binary files`);
57
  }
58
 
@@ -62,8 +79,14 @@ export const ImportFolderButton: React.FC<ImportFolderButtonProps> = ({ classNam
62
  await importChat(folderName, [...messages]);
63
  }
64
 
 
 
 
 
 
65
  toast.success('Folder imported successfully');
66
  } catch (error) {
 
67
  console.error('Failed to import folder:', error);
68
  toast.error('Failed to import folder');
69
  } finally {
 
3
  import { toast } from 'react-toastify';
4
  import { MAX_FILES, isBinaryFile, shouldIncludeFile } from '~/utils/fileUtils';
5
  import { createChatFromFolder } from '~/utils/folderImport';
6
+ import { logStore } from '~/lib/stores/logs'; // Assuming logStore is imported from this location
7
 
8
  interface ImportFolderButtonProps {
9
  className?: string;
 
17
  const allFiles = Array.from(e.target.files || []);
18
 
19
  if (allFiles.length > MAX_FILES) {
20
+ const error = new Error(`Too many files: ${allFiles.length}`);
21
+ logStore.logError('File import failed - too many files', error, {
22
+ fileCount: allFiles.length,
23
+ maxFiles: MAX_FILES,
24
+ });
25
  toast.error(
26
  `This folder contains ${allFiles.length.toLocaleString()} files. This product is not yet optimized for very large projects. Please select a folder with fewer than ${MAX_FILES.toLocaleString()} files.`,
27
  );
28
+
29
  return;
30
  }
31
 
 
38
  const filteredFiles = allFiles.filter((file) => shouldIncludeFile(file.webkitRelativePath));
39
 
40
  if (filteredFiles.length === 0) {
41
+ const error = new Error('No valid files found');
42
+ logStore.logError('File import failed - no valid files', error, { folderName });
43
  toast.error('No files found in the selected folder');
44
+
45
  return;
46
  }
47
 
 
58
  .map((f) => f.file.webkitRelativePath.split('/').slice(1).join('/'));
59
 
60
  if (textFiles.length === 0) {
61
+ const error = new Error('No text files found');
62
+ logStore.logError('File import failed - no text files', error, { folderName });
63
  toast.error('No text files found in the selected folder');
64
+
65
  return;
66
  }
67
 
68
  if (binaryFilePaths.length > 0) {
69
+ logStore.logWarning(`Skipping binary files during import`, {
70
+ folderName,
71
+ binaryCount: binaryFilePaths.length,
72
+ });
73
  toast.info(`Skipping ${binaryFilePaths.length} binary files`);
74
  }
75
 
 
79
  await importChat(folderName, [...messages]);
80
  }
81
 
82
+ logStore.logSystem('Folder imported successfully', {
83
+ folderName,
84
+ textFileCount: textFiles.length,
85
+ binaryFileCount: binaryFilePaths.length,
86
+ });
87
  toast.success('Folder imported successfully');
88
  } catch (error) {
89
+ logStore.logError('Failed to import folder', error, { folderName });
90
  console.error('Failed to import folder:', error);
91
  toast.error('Failed to import folder');
92
  } finally {
app/components/settings/SettingsWindow.tsx CHANGED
@@ -10,6 +10,7 @@ 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 ConnectionsTab from './connections/ConnectionsTab';
14
 
15
  interface SettingsProps {
@@ -17,11 +18,10 @@ interface SettingsProps {
17
  onClose: () => void;
18
  }
19
 
20
- type TabType = 'chat-history' | 'providers' | 'features' | 'debug' | 'connection';
21
 
22
- // Providers that support base URL configuration
23
  export const SettingsWindow = ({ open, onClose }: SettingsProps) => {
24
- const { debug } = useSettings();
25
  const [activeTab, setActiveTab] = useState<TabType>('chat-history');
26
 
27
  const tabs: { id: TabType; label: string; icon: string; component?: ReactElement }[] = [
@@ -39,6 +39,16 @@ export const SettingsWindow = ({ open, onClose }: SettingsProps) => {
39
  },
40
  ]
41
  : []),
 
 
 
 
 
 
 
 
 
 
42
  ];
43
 
44
  return (
 
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
 
16
  interface SettingsProps {
 
18
  onClose: () => void;
19
  }
20
 
21
+ type TabType = 'chat-history' | 'providers' | 'features' | 'debug' | 'event-logs' | 'connection';
22
 
 
23
  export const SettingsWindow = ({ open, onClose }: SettingsProps) => {
24
+ const { debug, eventLogs } = useSettings();
25
  const [activeTab, setActiveTab] = useState<TabType>('chat-history');
26
 
27
  const tabs: { id: TabType; label: string; icon: string; component?: ReactElement }[] = [
 
39
  },
40
  ]
41
  : []),
42
+ ...(eventLogs
43
+ ? [
44
+ {
45
+ id: 'event-logs' as TabType,
46
+ label: 'Event Logs',
47
+ icon: 'i-ph:list-bullets',
48
+ component: <EventLogsTab />,
49
+ },
50
+ ]
51
+ : []),
52
  ];
53
 
54
  return (
app/components/settings/chat-history/ChatHistoryTab.tsx CHANGED
@@ -4,6 +4,7 @@ import { toast } from 'react-toastify';
4
  import { db, deleteById, getAll } from '~/lib/persistence';
5
  import { classNames } from '~/utils/classNames';
6
  import styles from '~/components/settings/Settings.module.scss';
 
7
 
8
  export default function ChatHistoryTab() {
9
  const navigate = useNavigate();
@@ -22,7 +23,10 @@ export default function ChatHistoryTab() {
22
 
23
  const handleDeleteAllChats = async () => {
24
  if (!db) {
 
 
25
  toast.error('Database is not available');
 
26
  return;
27
  }
28
 
@@ -30,13 +34,12 @@ export default function ChatHistoryTab() {
30
  setIsDeleting(true);
31
 
32
  const allChats = await getAll(db);
33
-
34
- // Delete all chats one by one
35
  await Promise.all(allChats.map((chat) => deleteById(db!, chat.id)));
36
-
37
  toast.success('All chats deleted successfully');
38
  navigate('/', { replace: true });
39
  } catch (error) {
 
40
  toast.error('Failed to delete chats');
41
  console.error(error);
42
  } finally {
@@ -46,7 +49,10 @@ export default function ChatHistoryTab() {
46
 
47
  const handleExportAllChats = async () => {
48
  if (!db) {
 
 
49
  toast.error('Database is not available');
 
50
  return;
51
  }
52
 
@@ -58,8 +64,10 @@ export default function ChatHistoryTab() {
58
  };
59
 
60
  downloadAsJson(exportData, `all-chats-${new Date().toISOString()}.json`);
 
61
  toast.success('Chats exported successfully');
62
  } catch (error) {
 
63
  toast.error('Failed to export chats');
64
  console.error(error);
65
  }
 
4
  import { db, deleteById, getAll } from '~/lib/persistence';
5
  import { classNames } from '~/utils/classNames';
6
  import styles from '~/components/settings/Settings.module.scss';
7
+ import { logStore } from '~/lib/stores/logs'; // Import logStore for event logging
8
 
9
  export default function ChatHistoryTab() {
10
  const navigate = useNavigate();
 
23
 
24
  const handleDeleteAllChats = async () => {
25
  if (!db) {
26
+ const error = new Error('Database is not available');
27
+ logStore.logError('Failed to delete chats - DB unavailable', error);
28
  toast.error('Database is not available');
29
+
30
  return;
31
  }
32
 
 
34
  setIsDeleting(true);
35
 
36
  const allChats = await getAll(db);
 
 
37
  await Promise.all(allChats.map((chat) => deleteById(db!, chat.id)));
38
+ logStore.logSystem('All chats deleted successfully', { count: allChats.length });
39
  toast.success('All chats deleted successfully');
40
  navigate('/', { replace: true });
41
  } catch (error) {
42
+ logStore.logError('Failed to delete chats', error);
43
  toast.error('Failed to delete chats');
44
  console.error(error);
45
  } finally {
 
49
 
50
  const handleExportAllChats = async () => {
51
  if (!db) {
52
+ const error = new Error('Database is not available');
53
+ logStore.logError('Failed to export chats - DB unavailable', error);
54
  toast.error('Database is not available');
55
+
56
  return;
57
  }
58
 
 
64
  };
65
 
66
  downloadAsJson(exportData, `all-chats-${new Date().toISOString()}.json`);
67
+ logStore.logSystem('Chats exported successfully', { count: allChats.length });
68
  toast.success('Chats exported successfully');
69
  } catch (error) {
70
+ logStore.logError('Failed to export chats', error);
71
  toast.error('Failed to export chats');
72
  console.error(error);
73
  }
app/components/settings/connections/ConnectionsTab.tsx CHANGED
@@ -1,6 +1,7 @@
1
  import React, { useState } from 'react';
2
  import { toast } from 'react-toastify';
3
  import Cookies from 'js-cookie';
 
4
 
5
  export default function ConnectionsTab() {
6
  const [githubUsername, setGithubUsername] = useState(Cookies.get('githubUsername') || '');
@@ -9,6 +10,10 @@ export default function ConnectionsTab() {
9
  const handleSaveConnection = () => {
10
  Cookies.set('githubUsername', githubUsername);
11
  Cookies.set('githubToken', githubToken);
 
 
 
 
12
  toast.success('GitHub credentials saved successfully!');
13
  };
14
 
 
1
  import React, { useState } from 'react';
2
  import { toast } from 'react-toastify';
3
  import Cookies from 'js-cookie';
4
+ import { logStore } from '~/lib/stores/logs';
5
 
6
  export default function ConnectionsTab() {
7
  const [githubUsername, setGithubUsername] = useState(Cookies.get('githubUsername') || '');
 
10
  const handleSaveConnection = () => {
11
  Cookies.set('githubUsername', githubUsername);
12
  Cookies.set('githubToken', githubToken);
13
+ logStore.logSystem('GitHub connection settings updated', {
14
+ username: githubUsername,
15
+ hasToken: !!githubToken,
16
+ });
17
  toast.success('GitHub credentials saved successfully!');
18
  };
19
 
app/components/settings/event-logs/EventLogsTab.tsx ADDED
@@ -0,0 +1,145 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useCallback, useEffect, useState, useMemo } 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
+
7
+ export default function EventLogsTab() {
8
+ const {} = useSettings();
9
+ const [logLevel, setLogLevel] = useState<LogEntry['level']>('info');
10
+ const [autoScroll, setAutoScroll] = useState(true);
11
+ const [searchQuery, setSearchQuery] = useState('');
12
+ const [, forceUpdate] = useState({});
13
+
14
+ useEffect(() => {
15
+ // Add some initial logs for testing
16
+ logStore.logSystem('System started', { version: '1.0.0' });
17
+ logStore.logWarning('High memory usage detected', { memoryUsage: '85%' });
18
+ logStore.logError('Failed to connect to provider', new Error('Connection timeout'), { provider: 'OpenAI' });
19
+ }, []);
20
+
21
+ const handleClearLogs = useCallback(() => {
22
+ if (confirm('Are you sure you want to clear all logs?')) {
23
+ logStore.clearLogs();
24
+ toast.success('Logs cleared successfully');
25
+ forceUpdate({}); // Force a re-render after clearing logs
26
+ }
27
+ }, []);
28
+
29
+ const handleExportLogs = useCallback(() => {
30
+ try {
31
+ const logText = logStore
32
+ .getLogs()
33
+ .map(
34
+ (log) =>
35
+ `[${log.level.toUpperCase()}] ${log.timestamp} - ${log.message}${
36
+ log.details ? '\nDetails: ' + JSON.stringify(log.details, null, 2) : ''
37
+ }`,
38
+ )
39
+ .join('\n\n');
40
+
41
+ const blob = new Blob([logText], { type: 'text/plain' });
42
+ const url = URL.createObjectURL(blob);
43
+ const a = document.createElement('a');
44
+ a.href = url;
45
+ a.download = `event-logs-${new Date().toISOString()}.txt`;
46
+ document.body.appendChild(a);
47
+ a.click();
48
+ document.body.removeChild(a);
49
+ URL.revokeObjectURL(url);
50
+ toast.success('Logs exported successfully');
51
+ } catch (error) {
52
+ toast.error('Failed to export logs');
53
+ console.error('Export error:', error);
54
+ }
55
+ }, []);
56
+
57
+ const filteredLogs = useMemo(() => {
58
+ return logStore.getFilteredLogs(logLevel, undefined, searchQuery);
59
+ }, [logLevel, searchQuery]);
60
+
61
+ const getLevelColor = (level: LogEntry['level']) => {
62
+ switch (level) {
63
+ case 'info':
64
+ return 'text-blue-500';
65
+ case 'warning':
66
+ return 'text-yellow-500';
67
+ case 'error':
68
+ return 'text-red-500';
69
+ case 'debug':
70
+ return 'text-gray-500';
71
+ default:
72
+ return 'text-bolt-elements-textPrimary';
73
+ }
74
+ };
75
+
76
+ return (
77
+ <div className="p-4">
78
+ <div className="flex flex-col space-y-4 mb-4">
79
+ <div className="flex justify-between items-center">
80
+ <h3 className="text-lg font-medium text-bolt-elements-textPrimary">Event Logs</h3>
81
+ <div className="flex items-center space-x-2">
82
+ <span className="text-sm text-bolt-elements-textSecondary">Auto-scroll</span>
83
+ <Switch checked={autoScroll} onCheckedChange={setAutoScroll} />
84
+ </div>
85
+ </div>
86
+
87
+ <div className="flex items-center space-x-2">
88
+ <select
89
+ value={logLevel}
90
+ onChange={(e) => setLogLevel(e.target.value as LogEntry['level'])}
91
+ className="bg-bolt-elements-bg-depth-2 text-bolt-elements-textPrimary rounded-lg px-3 py-1.5 text-sm min-w-[100px]"
92
+ >
93
+ <option value="info">Info</option>
94
+ <option value="warning">Warning</option>
95
+ <option value="error">Error</option>
96
+ <option value="debug">Debug</option>
97
+ </select>
98
+ <input
99
+ type="text"
100
+ placeholder="Search logs..."
101
+ value={searchQuery}
102
+ onChange={(e) => setSearchQuery(e.target.value)}
103
+ className="bg-bolt-elements-bg-depth-2 text-bolt-elements-textPrimary rounded-lg px-3 py-1.5 text-sm flex-1"
104
+ />
105
+ <button
106
+ onClick={handleExportLogs}
107
+ className="bg-blue-500 text-white rounded-lg px-3 py-1.5 hover:bg-blue-600 transition-colors duration-200 text-sm whitespace-nowrap"
108
+ >
109
+ Export Logs
110
+ </button>
111
+ <button
112
+ onClick={handleClearLogs}
113
+ className="bg-red-500 text-white rounded-lg px-3 py-1.5 hover:bg-red-600 transition-colors duration-200 text-sm whitespace-nowrap"
114
+ >
115
+ Clear Logs
116
+ </button>
117
+ </div>
118
+ </div>
119
+
120
+ <div className="bg-bolt-elements-bg-depth-1 rounded-lg p-4 h-[500px] overflow-y-auto">
121
+ {filteredLogs.length === 0 ? (
122
+ <div className="text-center text-bolt-elements-textSecondary py-8">No logs found</div>
123
+ ) : (
124
+ filteredLogs.map((log, index) => (
125
+ <div
126
+ key={index}
127
+ className="text-sm mb-3 font-mono border-b border-bolt-elements-borderColor pb-2 last:border-0"
128
+ >
129
+ <div className="flex items-center space-x-2 flex-wrap">
130
+ <span className={`font-bold ${getLevelColor(log.level)}`}>[{log.level.toUpperCase()}]</span>
131
+ <span className="text-bolt-elements-textSecondary">{new Date(log.timestamp).toLocaleString()}</span>
132
+ <span className="text-bolt-elements-textPrimary">{log.message}</span>
133
+ </div>
134
+ {log.details && (
135
+ <pre className="mt-2 text-xs text-bolt-elements-textSecondary overflow-x-auto">
136
+ {JSON.stringify(log.details, null, 2)}
137
+ </pre>
138
+ )}
139
+ </div>
140
+ ))
141
+ )}
142
+ </div>
143
+ </div>
144
+ );
145
+ }
app/components/settings/features/FeaturesTab.tsx CHANGED
@@ -3,7 +3,7 @@ import { Switch } from '~/components/ui/Switch';
3
  import { useSettings } from '~/lib/hooks/useSettings';
4
 
5
  export default function FeaturesTab() {
6
- const { debug, enableDebugMode, isLocalModel, enableLocalModels } = useSettings();
7
  return (
8
  <div className="p-4 bg-bolt-elements-bg-depth-2 border border-bolt-elements-borderColor rounded-lg mb-4">
9
  <div className="mb-6">
@@ -12,6 +12,10 @@ export default function FeaturesTab() {
12
  <span className="text-bolt-elements-textPrimary">Debug Info</span>
13
  <Switch className="ml-auto" checked={debug} onCheckedChange={enableDebugMode} />
14
  </div>
 
 
 
 
15
  </div>
16
 
17
  <div className="mb-6 border-t border-bolt-elements-borderColor pt-4">
 
3
  import { useSettings } from '~/lib/hooks/useSettings';
4
 
5
  export default function FeaturesTab() {
6
+ const { debug, enableDebugMode, isLocalModel, enableLocalModels, eventLogs, enableEventLogs } = useSettings();
7
  return (
8
  <div className="p-4 bg-bolt-elements-bg-depth-2 border border-bolt-elements-borderColor rounded-lg mb-4">
9
  <div className="mb-6">
 
12
  <span className="text-bolt-elements-textPrimary">Debug Info</span>
13
  <Switch className="ml-auto" checked={debug} onCheckedChange={enableDebugMode} />
14
  </div>
15
+ <div className="flex items-center justify-between mb-2">
16
+ <span className="text-bolt-elements-textPrimary">Event Logs</span>
17
+ <Switch className="ml-auto" checked={eventLogs} onCheckedChange={enableEventLogs} />
18
+ </div>
19
  </div>
20
 
21
  <div className="mb-6 border-t border-bolt-elements-borderColor pt-4">
app/components/settings/providers/ProvidersTab.tsx CHANGED
@@ -3,6 +3,7 @@ import { Switch } from '~/components/ui/Switch';
3
  import { useSettings } from '~/lib/hooks/useSettings';
4
  import { LOCAL_PROVIDERS, URL_CONFIGURABLE_PROVIDERS } from '~/lib/stores/settings';
5
  import type { IProviderConfig } from '~/types/model';
 
6
 
7
  export default function ProvidersTab() {
8
  const { providers, updateProviderSettings, isLocalModel } = useSettings();
@@ -60,7 +61,15 @@ export default function ProvidersTab() {
60
  <Switch
61
  className="ml-auto"
62
  checked={provider.settings.enabled}
63
- onCheckedChange={(enabled) => updateProviderSettings(provider.name, { ...provider.settings, enabled })}
 
 
 
 
 
 
 
 
64
  />
65
  </div>
66
  {/* Base URL input for configurable providers */}
@@ -70,9 +79,14 @@ export default function ProvidersTab() {
70
  <input
71
  type="text"
72
  value={provider.settings.baseUrl || ''}
73
- onChange={(e) =>
74
- updateProviderSettings(provider.name, { ...provider.settings, baseUrl: e.target.value })
75
- }
 
 
 
 
 
76
  placeholder={`Enter ${provider.name} base URL`}
77
  className="w-full bg-white dark:bg-bolt-elements-background-depth-4 relative px-2 py-1.5 rounded-md focus:outline-none placeholder-bolt-elements-textTertiary text-bolt-elements-textPrimary dark:text-bolt-elements-textPrimary border border-bolt-elements-borderColor"
78
  />
 
3
  import { useSettings } from '~/lib/hooks/useSettings';
4
  import { LOCAL_PROVIDERS, URL_CONFIGURABLE_PROVIDERS } from '~/lib/stores/settings';
5
  import type { IProviderConfig } from '~/types/model';
6
+ import { logStore } from '~/lib/stores/logs';
7
 
8
  export default function ProvidersTab() {
9
  const { providers, updateProviderSettings, isLocalModel } = useSettings();
 
61
  <Switch
62
  className="ml-auto"
63
  checked={provider.settings.enabled}
64
+ onCheckedChange={(enabled) => {
65
+ updateProviderSettings(provider.name, { ...provider.settings, enabled });
66
+
67
+ if (enabled) {
68
+ logStore.logProvider(`Provider ${provider.name} enabled`, { provider: provider.name });
69
+ } else {
70
+ logStore.logProvider(`Provider ${provider.name} disabled`, { provider: provider.name });
71
+ }
72
+ }}
73
  />
74
  </div>
75
  {/* Base URL input for configurable providers */}
 
79
  <input
80
  type="text"
81
  value={provider.settings.baseUrl || ''}
82
+ onChange={(e) => {
83
+ const newBaseUrl = e.target.value;
84
+ updateProviderSettings(provider.name, { ...provider.settings, baseUrl: newBaseUrl });
85
+ logStore.logProvider(`Base URL updated for ${provider.name}`, {
86
+ provider: provider.name,
87
+ baseUrl: newBaseUrl,
88
+ });
89
+ }}
90
  placeholder={`Enter ${provider.name} base URL`}
91
  className="w-full bg-white dark:bg-bolt-elements-background-depth-4 relative px-2 py-1.5 rounded-md focus:outline-none placeholder-bolt-elements-textTertiary text-bolt-elements-textPrimary dark:text-bolt-elements-textPrimary border border-bolt-elements-borderColor"
92
  />
app/components/sidebar/Menu.client.tsx CHANGED
@@ -11,6 +11,7 @@ import { logger } from '~/utils/logger';
11
  import { HistoryItem } from './HistoryItem';
12
  import { binDates } from './date-binning';
13
  import { useSearchFilter } from '~/lib/hooks/useSearchFilter';
 
14
 
15
  const menuVariants = {
16
  closed: {
@@ -35,6 +36,25 @@ const menuVariants = {
35
 
36
  type DialogContent = { type: 'delete'; item: ChatHistoryItem } | null;
37
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
38
  export const Menu = () => {
39
  const { duplicateCurrentChat, exportChat } = useChatHistory();
40
  const menuRef = useRef<HTMLDivElement>(null);
@@ -126,18 +146,17 @@ export const Menu = () => {
126
  variants={menuVariants}
127
  className="flex selection-accent flex-col side-menu fixed top-0 w-[350px] h-full bg-bolt-elements-background-depth-2 border-r rounded-r-3xl border-bolt-elements-borderColor z-sidebar shadow-xl shadow-bolt-elements-sidebar-dropdownShadow text-sm"
128
  >
129
- <div className="flex items-center h-[var(--header-height)]">{/* Placeholder */}</div>
 
130
  <div className="flex-1 flex flex-col h-full w-full overflow-hidden">
131
  <div className="p-4 select-none">
132
  <a
133
  href="/"
134
- className="flex gap-2 items-center bg-bolt-elements-sidebar-buttonBackgroundDefault text-bolt-elements-sidebar-buttonText hover:bg-bolt-elements-sidebar-buttonBackgroundHover rounded-md p-2 transition-theme"
135
  >
136
  <span className="inline-block i-bolt:chat scale-110" />
137
  Start new chat
138
  </a>
139
- </div>
140
- <div className="pl-4 pr-4 my-2">
141
  <div className="relative w-full">
142
  <input
143
  className="w-full bg-white dark:bg-bolt-elements-background-depth-4 relative px-2 py-1.5 rounded-md focus:outline-none placeholder-bolt-elements-textTertiary text-bolt-elements-textPrimary dark:text-bolt-elements-textPrimary border border-bolt-elements-borderColor"
 
11
  import { HistoryItem } from './HistoryItem';
12
  import { binDates } from './date-binning';
13
  import { useSearchFilter } from '~/lib/hooks/useSearchFilter';
14
+ import { ClockIcon } from '@heroicons/react/24/outline';
15
 
16
  const menuVariants = {
17
  closed: {
 
36
 
37
  type DialogContent = { type: 'delete'; item: ChatHistoryItem } | null;
38
 
39
+ function CurrentDateTime() {
40
+ const [dateTime, setDateTime] = useState(new Date());
41
+
42
+ useEffect(() => {
43
+ const timer = setInterval(() => {
44
+ setDateTime(new Date());
45
+ }, 60000); // Update every minute
46
+
47
+ return () => clearInterval(timer);
48
+ }, []);
49
+
50
+ return (
51
+ <div className="flex items-center gap-2 px-4 py-3 font-bold text-gray-700 dark:text-gray-300 border-b border-bolt-elements-borderColor">
52
+ <ClockIcon className="h-4 w-4" />
53
+ {dateTime.toLocaleDateString()} {dateTime.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
54
+ </div>
55
+ );
56
+ }
57
+
58
  export const Menu = () => {
59
  const { duplicateCurrentChat, exportChat } = useChatHistory();
60
  const menuRef = useRef<HTMLDivElement>(null);
 
146
  variants={menuVariants}
147
  className="flex selection-accent flex-col side-menu fixed top-0 w-[350px] h-full bg-bolt-elements-background-depth-2 border-r rounded-r-3xl border-bolt-elements-borderColor z-sidebar shadow-xl shadow-bolt-elements-sidebar-dropdownShadow text-sm"
148
  >
149
+ <div className="h-[60px]" /> {/* Spacer for top margin */}
150
+ <CurrentDateTime />
151
  <div className="flex-1 flex flex-col h-full w-full overflow-hidden">
152
  <div className="p-4 select-none">
153
  <a
154
  href="/"
155
+ className="flex gap-2 items-center bg-bolt-elements-sidebar-buttonBackgroundDefault text-bolt-elements-sidebar-buttonText hover:bg-bolt-elements-sidebar-buttonBackgroundHover rounded-md p-2 transition-theme mb-4"
156
  >
157
  <span className="inline-block i-bolt:chat scale-110" />
158
  Start new chat
159
  </a>
 
 
160
  <div className="relative w-full">
161
  <input
162
  className="w-full bg-white dark:bg-bolt-elements-background-depth-4 relative px-2 py-1.5 rounded-md focus:outline-none placeholder-bolt-elements-textTertiary text-bolt-elements-textPrimary dark:text-bolt-elements-textPrimary border border-bolt-elements-borderColor"
app/lib/hooks/useSettings.tsx CHANGED
@@ -1,12 +1,20 @@
1
  import { useStore } from '@nanostores/react';
2
- import { isDebugMode, isLocalModelsEnabled, LOCAL_PROVIDERS, providersStore } from '~/lib/stores/settings';
 
 
 
 
 
 
3
  import { useCallback, useEffect, useState } from 'react';
4
  import Cookies from 'js-cookie';
5
  import type { IProviderSetting, ProviderInfo } from '~/types/model';
 
6
 
7
  export function useSettings() {
8
  const providers = useStore(providersStore);
9
  const debug = useStore(isDebugMode);
 
10
  const isLocalModel = useStore(isLocalModelsEnabled);
11
  const [activeProviders, setActiveProviders] = useState<ProviderInfo[]>([]);
12
 
@@ -39,6 +47,13 @@ export function useSettings() {
39
  isDebugMode.set(savedDebugMode === 'true');
40
  }
41
 
 
 
 
 
 
 
 
42
  // load local models from cookies
43
  const savedLocalModels = Cookies.get('isLocalModelsEnabled');
44
 
@@ -80,11 +95,19 @@ export function useSettings() {
80
 
81
  const enableDebugMode = useCallback((enabled: boolean) => {
82
  isDebugMode.set(enabled);
 
83
  Cookies.set('isDebugEnabled', String(enabled));
84
  }, []);
85
 
 
 
 
 
 
 
86
  const enableLocalModels = useCallback((enabled: boolean) => {
87
  isLocalModelsEnabled.set(enabled);
 
88
  Cookies.set('isLocalModelsEnabled', String(enabled));
89
  }, []);
90
 
@@ -94,6 +117,8 @@ export function useSettings() {
94
  updateProviderSettings,
95
  debug,
96
  enableDebugMode,
 
 
97
  isLocalModel,
98
  enableLocalModels,
99
  };
 
1
  import { useStore } from '@nanostores/react';
2
+ import {
3
+ isDebugMode,
4
+ isEventLogsEnabled,
5
+ isLocalModelsEnabled,
6
+ LOCAL_PROVIDERS,
7
+ providersStore,
8
+ } from '~/lib/stores/settings';
9
  import { useCallback, useEffect, useState } from 'react';
10
  import Cookies from 'js-cookie';
11
  import type { IProviderSetting, ProviderInfo } from '~/types/model';
12
+ import { logStore } from '~/lib/stores/logs'; // assuming logStore is imported from this location
13
 
14
  export function useSettings() {
15
  const providers = useStore(providersStore);
16
  const debug = useStore(isDebugMode);
17
+ const eventLogs = useStore(isEventLogsEnabled);
18
  const isLocalModel = useStore(isLocalModelsEnabled);
19
  const [activeProviders, setActiveProviders] = useState<ProviderInfo[]>([]);
20
 
 
47
  isDebugMode.set(savedDebugMode === 'true');
48
  }
49
 
50
+ // load event logs from cookies
51
+ const savedEventLogs = Cookies.get('isEventLogsEnabled');
52
+
53
+ if (savedEventLogs) {
54
+ isEventLogsEnabled.set(savedEventLogs === 'true');
55
+ }
56
+
57
  // load local models from cookies
58
  const savedLocalModels = Cookies.get('isLocalModelsEnabled');
59
 
 
95
 
96
  const enableDebugMode = useCallback((enabled: boolean) => {
97
  isDebugMode.set(enabled);
98
+ logStore.logSystem(`Debug mode ${enabled ? 'enabled' : 'disabled'}`);
99
  Cookies.set('isDebugEnabled', String(enabled));
100
  }, []);
101
 
102
+ const enableEventLogs = useCallback((enabled: boolean) => {
103
+ isEventLogsEnabled.set(enabled);
104
+ logStore.logSystem(`Event logs ${enabled ? 'enabled' : 'disabled'}`);
105
+ Cookies.set('isEventLogsEnabled', String(enabled));
106
+ }, []);
107
+
108
  const enableLocalModels = useCallback((enabled: boolean) => {
109
  isLocalModelsEnabled.set(enabled);
110
+ logStore.logSystem(`Local models ${enabled ? 'enabled' : 'disabled'}`);
111
  Cookies.set('isLocalModelsEnabled', String(enabled));
112
  }, []);
113
 
 
117
  updateProviderSettings,
118
  debug,
119
  enableDebugMode,
120
+ eventLogs,
121
+ enableEventLogs,
122
  isLocalModel,
123
  enableLocalModels,
124
  };
app/lib/persistence/useChatHistory.ts CHANGED
@@ -4,6 +4,7 @@ import { atom } from 'nanostores';
4
  import type { Message } from 'ai';
5
  import { toast } from 'react-toastify';
6
  import { workbenchStore } from '~/lib/stores/workbench';
 
7
  import {
8
  getMessages,
9
  getNextId,
@@ -43,6 +44,8 @@ export function useChatHistory() {
43
  setReady(true);
44
 
45
  if (persistenceEnabled) {
 
 
46
  toast.error('Chat persistence is unavailable');
47
  }
48
 
@@ -69,6 +72,7 @@ export function useChatHistory() {
69
  setReady(true);
70
  })
71
  .catch((error) => {
 
72
  toast.error(error.message);
73
  });
74
  }
 
4
  import type { Message } from 'ai';
5
  import { toast } from 'react-toastify';
6
  import { workbenchStore } from '~/lib/stores/workbench';
7
+ import { logStore } from '~/lib/stores/logs'; // Import logStore
8
  import {
9
  getMessages,
10
  getNextId,
 
44
  setReady(true);
45
 
46
  if (persistenceEnabled) {
47
+ const error = new Error('Chat persistence is unavailable');
48
+ logStore.logError('Chat persistence initialization failed', error);
49
  toast.error('Chat persistence is unavailable');
50
  }
51
 
 
72
  setReady(true);
73
  })
74
  .catch((error) => {
75
+ logStore.logError('Failed to load chat messages', error);
76
  toast.error(error.message);
77
  });
78
  }
app/lib/stores/logs.ts ADDED
@@ -0,0 +1,149 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { atom, map } from 'nanostores';
2
+ import Cookies from 'js-cookie';
3
+ import { createScopedLogger } from '~/utils/logger';
4
+
5
+ const logger = createScopedLogger('LogStore');
6
+
7
+ export interface LogEntry {
8
+ id: string;
9
+ timestamp: string;
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
17
+
18
+ class LogStore {
19
+ private _logs = map<Record<string, LogEntry>>({});
20
+ showLogs = atom(false);
21
+
22
+ constructor() {
23
+ // Load saved logs from cookies on initialization
24
+ this._loadLogs();
25
+ }
26
+
27
+ private _loadLogs() {
28
+ const savedLogs = Cookies.get('eventLogs');
29
+
30
+ if (savedLogs) {
31
+ try {
32
+ const parsedLogs = JSON.parse(savedLogs);
33
+ this._logs.set(parsedLogs);
34
+ } catch (error) {
35
+ logger.error('Failed to parse logs from cookies:', error);
36
+ }
37
+ }
38
+ }
39
+
40
+ private _saveLogs() {
41
+ const currentLogs = this._logs.get();
42
+ Cookies.set('eventLogs', JSON.stringify(currentLogs));
43
+ }
44
+
45
+ private _generateId(): string {
46
+ return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
47
+ }
48
+
49
+ private _trimLogs() {
50
+ const currentLogs = Object.entries(this._logs.get());
51
+
52
+ if (currentLogs.length > MAX_LOGS) {
53
+ const sortedLogs = currentLogs.sort(
54
+ ([, a], [, b]) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime(),
55
+ );
56
+ const newLogs = Object.fromEntries(sortedLogs.slice(0, MAX_LOGS));
57
+ this._logs.set(newLogs);
58
+ }
59
+ }
60
+
61
+ addLog(
62
+ message: string,
63
+ level: LogEntry['level'] = 'info',
64
+ category: LogEntry['category'] = 'system',
65
+ details?: Record<string, any>,
66
+ ) {
67
+ const id = this._generateId();
68
+ const entry: LogEntry = {
69
+ id,
70
+ timestamp: new Date().toISOString(),
71
+ level,
72
+ message,
73
+ details,
74
+ category,
75
+ };
76
+
77
+ this._logs.setKey(id, entry);
78
+ this._trimLogs();
79
+ this._saveLogs();
80
+
81
+ return id;
82
+ }
83
+
84
+ // System events
85
+ logSystem(message: string, details?: Record<string, any>) {
86
+ return this.addLog(message, 'info', 'system', details);
87
+ }
88
+
89
+ // Provider events
90
+ logProvider(message: string, details?: Record<string, any>) {
91
+ return this.addLog(message, 'info', 'provider', details);
92
+ }
93
+
94
+ // User actions
95
+ logUserAction(message: string, details?: Record<string, any>) {
96
+ return this.addLog(message, 'info', 'user', details);
97
+ }
98
+
99
+ // Error events
100
+ logError(message: string, error?: Error | unknown, details?: Record<string, any>) {
101
+ const errorDetails = {
102
+ ...(details || {}),
103
+ error:
104
+ error instanceof Error
105
+ ? {
106
+ message: error.message,
107
+ stack: error.stack,
108
+ }
109
+ : error,
110
+ };
111
+ return this.addLog(message, 'error', 'error', errorDetails);
112
+ }
113
+
114
+ // Warning events
115
+ logWarning(message: string, details?: Record<string, any>) {
116
+ return this.addLog(message, 'warning', 'system', details);
117
+ }
118
+
119
+ // Debug events
120
+ logDebug(message: string, details?: Record<string, any>) {
121
+ return this.addLog(message, 'debug', 'system', details);
122
+ }
123
+
124
+ clearLogs() {
125
+ this._logs.set({});
126
+ this._saveLogs();
127
+ }
128
+
129
+ getLogs() {
130
+ return Object.values(this._logs.get()).sort(
131
+ (a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime(),
132
+ );
133
+ }
134
+
135
+ getFilteredLogs(level?: LogEntry['level'], category?: LogEntry['category'], searchQuery?: string) {
136
+ return this.getLogs().filter((log) => {
137
+ const matchesLevel = !level || level === 'debug' || log.level === level;
138
+ const matchesCategory = !category || log.category === category;
139
+ const matchesSearch =
140
+ !searchQuery ||
141
+ log.message.toLowerCase().includes(searchQuery.toLowerCase()) ||
142
+ JSON.stringify(log.details).toLowerCase().includes(searchQuery.toLowerCase());
143
+
144
+ return matchesLevel && matchesCategory && matchesSearch;
145
+ });
146
+ }
147
+ }
148
+
149
+ export const logStore = new LogStore();
app/lib/stores/settings.ts CHANGED
@@ -43,4 +43,6 @@ export const providersStore = map<ProviderSetting>(initialProviderSettings);
43
 
44
  export const isDebugMode = atom(false);
45
 
 
 
46
  export const isLocalModelsEnabled = atom(true);
 
43
 
44
  export const isDebugMode = atom(false);
45
 
46
+ export const isEventLogsEnabled = atom(false);
47
+
48
  export const isLocalModelsEnabled = atom(true);
app/lib/stores/theme.ts CHANGED
@@ -1,4 +1,5 @@
1
  import { atom } from 'nanostores';
 
2
 
3
  export type Theme = 'dark' | 'light';
4
 
@@ -26,10 +27,8 @@ function initStore() {
26
  export function toggleTheme() {
27
  const currentTheme = themeStore.get();
28
  const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
29
-
30
  themeStore.set(newTheme);
31
-
32
  localStorage.setItem(kTheme, newTheme);
33
-
34
  document.querySelector('html')?.setAttribute('data-theme', newTheme);
35
  }
 
1
  import { atom } from 'nanostores';
2
+ import { logStore } from './logs';
3
 
4
  export type Theme = 'dark' | 'light';
5
 
 
27
  export function toggleTheme() {
28
  const currentTheme = themeStore.get();
29
  const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
 
30
  themeStore.set(newTheme);
31
+ logStore.logSystem(`Theme changed to ${newTheme} mode`);
32
  localStorage.setItem(kTheme, newTheme);
 
33
  document.querySelector('html')?.setAttribute('data-theme', newTheme);
34
  }
app/root.tsx CHANGED
@@ -78,6 +78,23 @@ export function Layout({ children }: { children: React.ReactNode }) {
78
  );
79
  }
80
 
 
 
81
  export default function App() {
82
- return <Outlet />;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
83
  }
 
78
  );
79
  }
80
 
81
+ import { logStore } from './lib/stores/logs';
82
+
83
  export default function App() {
84
+ const theme = useStore(themeStore);
85
+
86
+ useEffect(() => {
87
+ logStore.logSystem('Application initialized', {
88
+ theme,
89
+ platform: navigator.platform,
90
+ userAgent: navigator.userAgent,
91
+ timestamp: new Date().toISOString(),
92
+ });
93
+ }, []);
94
+
95
+ return (
96
+ <Layout>
97
+ <Outlet />
98
+ </Layout>
99
+ );
100
  }
app/utils/constants.ts CHANGED
@@ -2,6 +2,7 @@ import Cookies from 'js-cookie';
2
  import type { ModelInfo, OllamaApiResponse, OllamaModel } from './types';
3
  import type { ProviderInfo, IProviderSetting } from '~/types/model';
4
  import { createScopedLogger } from './logger';
 
5
 
6
  export const WORK_DIR_NAME = 'project';
7
  export const WORK_DIR = `/home/${WORK_DIR_NAME}`;
@@ -373,12 +374,6 @@ const getOllamaBaseUrl = (settings?: IProviderSetting) => {
373
  };
374
 
375
  async function getOllamaModels(apiKeys?: Record<string, string>, settings?: IProviderSetting): Promise<ModelInfo[]> {
376
- /*
377
- * if (typeof window === 'undefined') {
378
- * return [];
379
- * }
380
- */
381
-
382
  try {
383
  const baseUrl = getOllamaBaseUrl(settings);
384
  const response = await fetch(`${baseUrl}/api/tags`);
@@ -391,7 +386,9 @@ async function getOllamaModels(apiKeys?: Record<string, string>, settings?: IPro
391
  maxTokenAllowed: 8000,
392
  }));
393
  } catch (e: any) {
 
394
  logger.warn('Failed to get Ollama models: ', e.message || '');
 
395
  return [];
396
  }
397
  }
@@ -480,7 +477,9 @@ async function getLMStudioModels(_apiKeys?: Record<string, string>, settings?: I
480
  provider: 'LMStudio',
481
  }));
482
  } catch (e: any) {
 
483
  logger.warn('Failed to get LMStudio models: ', e.message || '');
 
484
  return [];
485
  }
486
  }
@@ -499,6 +498,7 @@ async function initializeModelList(providerSettings?: Record<string, IProviderSe
499
  }
500
  }
501
  } catch (error: any) {
 
502
  logger.warn(`Failed to fetch apikeys from cookies: ${error?.message}`);
503
  }
504
  MODEL_LIST = [
 
2
  import type { ModelInfo, OllamaApiResponse, OllamaModel } from './types';
3
  import type { ProviderInfo, IProviderSetting } from '~/types/model';
4
  import { createScopedLogger } from './logger';
5
+ import { logStore } from '~/lib/stores/logs';
6
 
7
  export const WORK_DIR_NAME = 'project';
8
  export const WORK_DIR = `/home/${WORK_DIR_NAME}`;
 
374
  };
375
 
376
  async function getOllamaModels(apiKeys?: Record<string, string>, settings?: IProviderSetting): Promise<ModelInfo[]> {
 
 
 
 
 
 
377
  try {
378
  const baseUrl = getOllamaBaseUrl(settings);
379
  const response = await fetch(`${baseUrl}/api/tags`);
 
386
  maxTokenAllowed: 8000,
387
  }));
388
  } catch (e: any) {
389
+ logStore.logError('Failed to get Ollama models', e, { baseUrl: settings?.baseUrl });
390
  logger.warn('Failed to get Ollama models: ', e.message || '');
391
+
392
  return [];
393
  }
394
  }
 
477
  provider: 'LMStudio',
478
  }));
479
  } catch (e: any) {
480
+ logStore.logError('Failed to get LMStudio models', e, { baseUrl: settings?.baseUrl });
481
  logger.warn('Failed to get LMStudio models: ', e.message || '');
482
+
483
  return [];
484
  }
485
  }
 
498
  }
499
  }
500
  } catch (error: any) {
501
+ logStore.logError('Failed to fetch API keys from cookies', error);
502
  logger.warn(`Failed to fetch apikeys from cookies: ${error?.message}`);
503
  }
504
  MODEL_LIST = [