Skip to content

Commit

Permalink
Settings UI enhancement
Browse files Browse the repository at this point in the history
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
  • Loading branch information
Stijnus committed Dec 13, 2024
1 parent e716ca5 commit e39f16e
Show file tree
Hide file tree
Showing 15 changed files with 450 additions and 26 deletions.
23 changes: 23 additions & 0 deletions app/components/chat/ImportFolderButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type { Message } from 'ai';
import { toast } from 'react-toastify';
import { MAX_FILES, isBinaryFile, shouldIncludeFile } from '~/utils/fileUtils';
import { createChatFromFolder } from '~/utils/folderImport';
import { logStore } from '~/lib/stores/logs'; // Assuming logStore is imported from this location

interface ImportFolderButtonProps {
className?: string;
Expand All @@ -16,9 +17,15 @@ export const ImportFolderButton: React.FC<ImportFolderButtonProps> = ({ classNam
const allFiles = Array.from(e.target.files || []);

if (allFiles.length > MAX_FILES) {
const error = new Error(`Too many files: ${allFiles.length}`);
logStore.logError('File import failed - too many files', error, {
fileCount: allFiles.length,
maxFiles: MAX_FILES,
});
toast.error(
`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.`,
);

return;
}

Expand All @@ -31,7 +38,10 @@ export const ImportFolderButton: React.FC<ImportFolderButtonProps> = ({ classNam
const filteredFiles = allFiles.filter((file) => shouldIncludeFile(file.webkitRelativePath));

if (filteredFiles.length === 0) {
const error = new Error('No valid files found');
logStore.logError('File import failed - no valid files', error, { folderName });
toast.error('No files found in the selected folder');

return;
}

Expand All @@ -48,11 +58,18 @@ export const ImportFolderButton: React.FC<ImportFolderButtonProps> = ({ classNam
.map((f) => f.file.webkitRelativePath.split('/').slice(1).join('/'));

if (textFiles.length === 0) {
const error = new Error('No text files found');
logStore.logError('File import failed - no text files', error, { folderName });
toast.error('No text files found in the selected folder');

return;
}

if (binaryFilePaths.length > 0) {
logStore.logWarning(`Skipping binary files during import`, {
folderName,
binaryCount: binaryFilePaths.length,
});
toast.info(`Skipping ${binaryFilePaths.length} binary files`);
}

Expand All @@ -62,8 +79,14 @@ export const ImportFolderButton: React.FC<ImportFolderButtonProps> = ({ classNam
await importChat(folderName, [...messages]);
}

logStore.logSystem('Folder imported successfully', {
folderName,
textFileCount: textFiles.length,
binaryFileCount: binaryFilePaths.length,
});
toast.success('Folder imported successfully');
} catch (error) {
logStore.logError('Failed to import folder', error, { folderName });
console.error('Failed to import folder:', error);
toast.error('Failed to import folder');
} finally {
Expand Down
16 changes: 13 additions & 3 deletions app/components/settings/SettingsWindow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,18 @@ import ProvidersTab from './providers/ProvidersTab';
import { useSettings } from '~/lib/hooks/useSettings';
import FeaturesTab from './features/FeaturesTab';
import DebugTab from './debug/DebugTab';
import EventLogsTab from './event-logs/EventLogsTab';
import ConnectionsTab from './connections/ConnectionsTab';

interface SettingsProps {
open: boolean;
onClose: () => void;
}

type TabType = 'chat-history' | 'providers' | 'features' | 'debug' | 'connection';
type TabType = 'chat-history' | 'providers' | 'features' | 'debug' | 'event-logs' | 'connection';

// Providers that support base URL configuration
export const SettingsWindow = ({ open, onClose }: SettingsProps) => {
const { debug } = useSettings();
const { debug, eventLogs } = useSettings();
const [activeTab, setActiveTab] = useState<TabType>('chat-history');

const tabs: { id: TabType; label: string; icon: string; component?: ReactElement }[] = [
Expand All @@ -39,6 +39,16 @@ export const SettingsWindow = ({ open, onClose }: SettingsProps) => {
},
]
: []),
...(eventLogs
? [
{
id: 'event-logs' as TabType,
label: 'Event Logs',
icon: 'i-ph:list-bullets',
component: <EventLogsTab />,
},
]
: []),
];

return (
Expand Down
14 changes: 11 additions & 3 deletions app/components/settings/chat-history/ChatHistoryTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { toast } from 'react-toastify';
import { db, deleteById, getAll } from '~/lib/persistence';
import { classNames } from '~/utils/classNames';
import styles from '~/components/settings/Settings.module.scss';
import { logStore } from '~/lib/stores/logs'; // Import logStore for event logging

export default function ChatHistoryTab() {
const navigate = useNavigate();
Expand All @@ -22,21 +23,23 @@ export default function ChatHistoryTab() {

const handleDeleteAllChats = async () => {
if (!db) {
const error = new Error('Database is not available');
logStore.logError('Failed to delete chats - DB unavailable', error);
toast.error('Database is not available');

return;
}

try {
setIsDeleting(true);

const allChats = await getAll(db);

// Delete all chats one by one
await Promise.all(allChats.map((chat) => deleteById(db!, chat.id)));

logStore.logSystem('All chats deleted successfully', { count: allChats.length });
toast.success('All chats deleted successfully');
navigate('/', { replace: true });
} catch (error) {
logStore.logError('Failed to delete chats', error);
toast.error('Failed to delete chats');
console.error(error);
} finally {
Expand All @@ -46,7 +49,10 @@ export default function ChatHistoryTab() {

const handleExportAllChats = async () => {
if (!db) {
const error = new Error('Database is not available');
logStore.logError('Failed to export chats - DB unavailable', error);
toast.error('Database is not available');

return;
}

Expand All @@ -58,8 +64,10 @@ export default function ChatHistoryTab() {
};

downloadAsJson(exportData, `all-chats-${new Date().toISOString()}.json`);
logStore.logSystem('Chats exported successfully', { count: allChats.length });
toast.success('Chats exported successfully');
} catch (error) {
logStore.logError('Failed to export chats', error);
toast.error('Failed to export chats');
console.error(error);
}
Expand Down
5 changes: 5 additions & 0 deletions app/components/settings/connections/ConnectionsTab.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React, { useState } from 'react';
import { toast } from 'react-toastify';
import Cookies from 'js-cookie';
import { logStore } from '~/lib/stores/logs';

export default function ConnectionsTab() {
const [githubUsername, setGithubUsername] = useState(Cookies.get('githubUsername') || '');
Expand All @@ -9,6 +10,10 @@ export default function ConnectionsTab() {
const handleSaveConnection = () => {
Cookies.set('githubUsername', githubUsername);
Cookies.set('githubToken', githubToken);
logStore.logSystem('GitHub connection settings updated', {
username: githubUsername,
hasToken: !!githubToken,
});
toast.success('GitHub credentials saved successfully!');
};

Expand Down
145 changes: 145 additions & 0 deletions app/components/settings/event-logs/EventLogsTab.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
import React, { useCallback, useEffect, useState, useMemo } from 'react';
import { useSettings } from '~/lib/hooks/useSettings';
import { toast } from 'react-toastify';
import { Switch } from '~/components/ui/Switch';
import { logStore, type LogEntry } from '~/lib/stores/logs';

export default function EventLogsTab() {
const {} = useSettings();
const [logLevel, setLogLevel] = useState<LogEntry['level']>('info');
const [autoScroll, setAutoScroll] = useState(true);
const [searchQuery, setSearchQuery] = useState('');
const [, forceUpdate] = useState({});

useEffect(() => {
// Add some initial logs for testing
logStore.logSystem('System started', { version: '1.0.0' });
logStore.logWarning('High memory usage detected', { memoryUsage: '85%' });
logStore.logError('Failed to connect to provider', new Error('Connection timeout'), { provider: 'OpenAI' });
}, []);

const handleClearLogs = useCallback(() => {
if (confirm('Are you sure you want to clear all logs?')) {
logStore.clearLogs();
toast.success('Logs cleared successfully');
forceUpdate({}); // Force a re-render after clearing logs
}
}, []);

const handleExportLogs = useCallback(() => {
try {
const logText = logStore
.getLogs()
.map(
(log) =>
`[${log.level.toUpperCase()}] ${log.timestamp} - ${log.message}${
log.details ? '\nDetails: ' + JSON.stringify(log.details, null, 2) : ''
}`,
)
.join('\n\n');

const blob = new Blob([logText], { type: 'text/plain' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `event-logs-${new Date().toISOString()}.txt`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
toast.success('Logs exported successfully');
} catch (error) {
toast.error('Failed to export logs');
console.error('Export error:', error);
}
}, []);

const filteredLogs = useMemo(() => {
return logStore.getFilteredLogs(logLevel, undefined, searchQuery);
}, [logLevel, searchQuery]);

const getLevelColor = (level: LogEntry['level']) => {
switch (level) {
case 'info':
return 'text-blue-500';
case 'warning':
return 'text-yellow-500';
case 'error':
return 'text-red-500';
case 'debug':
return 'text-gray-500';
default:
return 'text-bolt-elements-textPrimary';
}
};

return (
<div className="p-4">
<div className="flex flex-col space-y-4 mb-4">
<div className="flex justify-between items-center">
<h3 className="text-lg font-medium text-bolt-elements-textPrimary">Event Logs</h3>
<div className="flex items-center space-x-2">
<span className="text-sm text-bolt-elements-textSecondary">Auto-scroll</span>
<Switch checked={autoScroll} onCheckedChange={setAutoScroll} />
</div>
</div>

<div className="flex items-center space-x-2">
<select
value={logLevel}
onChange={(e) => setLogLevel(e.target.value as LogEntry['level'])}
className="bg-bolt-elements-bg-depth-2 text-bolt-elements-textPrimary rounded-lg px-3 py-1.5 text-sm min-w-[100px]"
>
<option value="info">Info</option>
<option value="warning">Warning</option>
<option value="error">Error</option>
<option value="debug">Debug</option>
</select>
<input
type="text"
placeholder="Search logs..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="bg-bolt-elements-bg-depth-2 text-bolt-elements-textPrimary rounded-lg px-3 py-1.5 text-sm flex-1"
/>
<button
onClick={handleExportLogs}
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"
>
Export Logs
</button>
<button
onClick={handleClearLogs}
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"
>
Clear Logs
</button>
</div>
</div>

<div className="bg-bolt-elements-bg-depth-1 rounded-lg p-4 h-[500px] overflow-y-auto">
{filteredLogs.length === 0 ? (
<div className="text-center text-bolt-elements-textSecondary py-8">No logs found</div>
) : (
filteredLogs.map((log, index) => (
<div
key={index}
className="text-sm mb-3 font-mono border-b border-bolt-elements-borderColor pb-2 last:border-0"
>
<div className="flex items-center space-x-2 flex-wrap">
<span className={`font-bold ${getLevelColor(log.level)}`}>[{log.level.toUpperCase()}]</span>
<span className="text-bolt-elements-textSecondary">{new Date(log.timestamp).toLocaleString()}</span>
<span className="text-bolt-elements-textPrimary">{log.message}</span>
</div>
{log.details && (
<pre className="mt-2 text-xs text-bolt-elements-textSecondary overflow-x-auto">
{JSON.stringify(log.details, null, 2)}
</pre>
)}
</div>
))
)}
</div>
</div>
);
}
6 changes: 5 additions & 1 deletion app/components/settings/features/FeaturesTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { Switch } from '~/components/ui/Switch';
import { useSettings } from '~/lib/hooks/useSettings';

export default function FeaturesTab() {
const { debug, enableDebugMode, isLocalModel, enableLocalModels } = useSettings();
const { debug, enableDebugMode, isLocalModel, enableLocalModels, eventLogs, enableEventLogs } = useSettings();
return (
<div className="p-4 bg-bolt-elements-bg-depth-2 border border-bolt-elements-borderColor rounded-lg mb-4">
<div className="mb-6">
Expand All @@ -12,6 +12,10 @@ export default function FeaturesTab() {
<span className="text-bolt-elements-textPrimary">Debug Info</span>
<Switch className="ml-auto" checked={debug} onCheckedChange={enableDebugMode} />
</div>
<div className="flex items-center justify-between mb-2">
<span className="text-bolt-elements-textPrimary">Event Logs</span>
<Switch className="ml-auto" checked={eventLogs} onCheckedChange={enableEventLogs} />
</div>
</div>

<div className="mb-6 border-t border-bolt-elements-borderColor pt-4">
Expand Down
22 changes: 18 additions & 4 deletions app/components/settings/providers/ProvidersTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Switch } from '~/components/ui/Switch';
import { useSettings } from '~/lib/hooks/useSettings';
import { LOCAL_PROVIDERS, URL_CONFIGURABLE_PROVIDERS } from '~/lib/stores/settings';
import type { IProviderConfig } from '~/types/model';
import { logStore } from '~/lib/stores/logs';

export default function ProvidersTab() {
const { providers, updateProviderSettings, isLocalModel } = useSettings();
Expand Down Expand Up @@ -60,7 +61,15 @@ export default function ProvidersTab() {
<Switch
className="ml-auto"
checked={provider.settings.enabled}
onCheckedChange={(enabled) => updateProviderSettings(provider.name, { ...provider.settings, enabled })}
onCheckedChange={(enabled) => {
updateProviderSettings(provider.name, { ...provider.settings, enabled });

if (enabled) {
logStore.logProvider(`Provider ${provider.name} enabled`, { provider: provider.name });
} else {
logStore.logProvider(`Provider ${provider.name} disabled`, { provider: provider.name });
}
}}
/>
</div>
{/* Base URL input for configurable providers */}
Expand All @@ -70,9 +79,14 @@ export default function ProvidersTab() {
<input
type="text"
value={provider.settings.baseUrl || ''}
onChange={(e) =>
updateProviderSettings(provider.name, { ...provider.settings, baseUrl: e.target.value })
}
onChange={(e) => {
const newBaseUrl = e.target.value;
updateProviderSettings(provider.name, { ...provider.settings, baseUrl: newBaseUrl });
logStore.logProvider(`Base URL updated for ${provider.name}`, {
provider: provider.name,
baseUrl: newBaseUrl,
});
}}
placeholder={`Enter ${provider.name} base URL`}
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"
/>
Expand Down
Loading

0 comments on commit e39f16e

Please sign in to comment.