mirror of
https://github.com/coleam00/bolt.new-any-llm
synced 2024-12-27 06:12:07 +00:00
Settings UI enhancement
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
This commit is contained in:
parent
e716ca55f0
commit
e39f16e436
@ -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;
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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`);
|
||||
}
|
||||
|
||||
@ -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 {
|
||||
|
@ -10,6 +10,7 @@ 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 {
|
||||
@ -17,11 +18,10 @@ interface SettingsProps {
|
||||
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 }[] = [
|
||||
@ -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 (
|
||||
|
@ -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();
|
||||
@ -22,7 +23,10 @@ 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;
|
||||
}
|
||||
|
||||
@ -30,13 +34,12 @@ export default function ChatHistoryTab() {
|
||||
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 {
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
@ -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') || '');
|
||||
@ -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!');
|
||||
};
|
||||
|
||||
|
145
app/components/settings/event-logs/EventLogsTab.tsx
Normal file
145
app/components/settings/event-logs/EventLogsTab.tsx
Normal file
@ -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>
|
||||
);
|
||||
}
|
@ -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">
|
||||
@ -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">
|
||||
|
@ -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();
|
||||
@ -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 */}
|
||||
@ -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"
|
||||
/>
|
||||
|
@ -11,6 +11,7 @@ import { logger } from '~/utils/logger';
|
||||
import { HistoryItem } from './HistoryItem';
|
||||
import { binDates } from './date-binning';
|
||||
import { useSearchFilter } from '~/lib/hooks/useSearchFilter';
|
||||
import { ClockIcon } from '@heroicons/react/24/outline';
|
||||
|
||||
const menuVariants = {
|
||||
closed: {
|
||||
@ -35,6 +36,25 @@ const menuVariants = {
|
||||
|
||||
type DialogContent = { type: 'delete'; item: ChatHistoryItem } | null;
|
||||
|
||||
function CurrentDateTime() {
|
||||
const [dateTime, setDateTime] = useState(new Date());
|
||||
|
||||
useEffect(() => {
|
||||
const timer = setInterval(() => {
|
||||
setDateTime(new Date());
|
||||
}, 60000); // Update every minute
|
||||
|
||||
return () => clearInterval(timer);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<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">
|
||||
<ClockIcon className="h-4 w-4" />
|
||||
{dateTime.toLocaleDateString()} {dateTime.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export const Menu = () => {
|
||||
const { duplicateCurrentChat, exportChat } = useChatHistory();
|
||||
const menuRef = useRef<HTMLDivElement>(null);
|
||||
@ -126,18 +146,17 @@ export const Menu = () => {
|
||||
variants={menuVariants}
|
||||
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"
|
||||
>
|
||||
<div className="flex items-center h-[var(--header-height)]">{/* Placeholder */}</div>
|
||||
<div className="h-[60px]" /> {/* Spacer for top margin */}
|
||||
<CurrentDateTime />
|
||||
<div className="flex-1 flex flex-col h-full w-full overflow-hidden">
|
||||
<div className="p-4 select-none">
|
||||
<a
|
||||
href="/"
|
||||
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"
|
||||
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"
|
||||
>
|
||||
<span className="inline-block i-bolt:chat scale-110" />
|
||||
Start new chat
|
||||
</a>
|
||||
</div>
|
||||
<div className="pl-4 pr-4 my-2">
|
||||
<div className="relative w-full">
|
||||
<input
|
||||
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"
|
||||
|
@ -1,12 +1,20 @@
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { isDebugMode, isLocalModelsEnabled, LOCAL_PROVIDERS, providersStore } from '~/lib/stores/settings';
|
||||
import {
|
||||
isDebugMode,
|
||||
isEventLogsEnabled,
|
||||
isLocalModelsEnabled,
|
||||
LOCAL_PROVIDERS,
|
||||
providersStore,
|
||||
} from '~/lib/stores/settings';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import Cookies from 'js-cookie';
|
||||
import type { IProviderSetting, ProviderInfo } from '~/types/model';
|
||||
import { logStore } from '~/lib/stores/logs'; // assuming logStore is imported from this location
|
||||
|
||||
export function useSettings() {
|
||||
const providers = useStore(providersStore);
|
||||
const debug = useStore(isDebugMode);
|
||||
const eventLogs = useStore(isEventLogsEnabled);
|
||||
const isLocalModel = useStore(isLocalModelsEnabled);
|
||||
const [activeProviders, setActiveProviders] = useState<ProviderInfo[]>([]);
|
||||
|
||||
@ -39,6 +47,13 @@ export function useSettings() {
|
||||
isDebugMode.set(savedDebugMode === 'true');
|
||||
}
|
||||
|
||||
// load event logs from cookies
|
||||
const savedEventLogs = Cookies.get('isEventLogsEnabled');
|
||||
|
||||
if (savedEventLogs) {
|
||||
isEventLogsEnabled.set(savedEventLogs === 'true');
|
||||
}
|
||||
|
||||
// load local models from cookies
|
||||
const savedLocalModels = Cookies.get('isLocalModelsEnabled');
|
||||
|
||||
@ -80,11 +95,19 @@ export function useSettings() {
|
||||
|
||||
const enableDebugMode = useCallback((enabled: boolean) => {
|
||||
isDebugMode.set(enabled);
|
||||
logStore.logSystem(`Debug mode ${enabled ? 'enabled' : 'disabled'}`);
|
||||
Cookies.set('isDebugEnabled', String(enabled));
|
||||
}, []);
|
||||
|
||||
const enableEventLogs = useCallback((enabled: boolean) => {
|
||||
isEventLogsEnabled.set(enabled);
|
||||
logStore.logSystem(`Event logs ${enabled ? 'enabled' : 'disabled'}`);
|
||||
Cookies.set('isEventLogsEnabled', String(enabled));
|
||||
}, []);
|
||||
|
||||
const enableLocalModels = useCallback((enabled: boolean) => {
|
||||
isLocalModelsEnabled.set(enabled);
|
||||
logStore.logSystem(`Local models ${enabled ? 'enabled' : 'disabled'}`);
|
||||
Cookies.set('isLocalModelsEnabled', String(enabled));
|
||||
}, []);
|
||||
|
||||
@ -94,6 +117,8 @@ export function useSettings() {
|
||||
updateProviderSettings,
|
||||
debug,
|
||||
enableDebugMode,
|
||||
eventLogs,
|
||||
enableEventLogs,
|
||||
isLocalModel,
|
||||
enableLocalModels,
|
||||
};
|
||||
|
@ -4,6 +4,7 @@ import { atom } from 'nanostores';
|
||||
import type { Message } from 'ai';
|
||||
import { toast } from 'react-toastify';
|
||||
import { workbenchStore } from '~/lib/stores/workbench';
|
||||
import { logStore } from '~/lib/stores/logs'; // Import logStore
|
||||
import {
|
||||
getMessages,
|
||||
getNextId,
|
||||
@ -43,6 +44,8 @@ export function useChatHistory() {
|
||||
setReady(true);
|
||||
|
||||
if (persistenceEnabled) {
|
||||
const error = new Error('Chat persistence is unavailable');
|
||||
logStore.logError('Chat persistence initialization failed', error);
|
||||
toast.error('Chat persistence is unavailable');
|
||||
}
|
||||
|
||||
@ -69,6 +72,7 @@ export function useChatHistory() {
|
||||
setReady(true);
|
||||
})
|
||||
.catch((error) => {
|
||||
logStore.logError('Failed to load chat messages', error);
|
||||
toast.error(error.message);
|
||||
});
|
||||
}
|
||||
|
149
app/lib/stores/logs.ts
Normal file
149
app/lib/stores/logs.ts
Normal file
@ -0,0 +1,149 @@
|
||||
import { atom, map } from 'nanostores';
|
||||
import Cookies from 'js-cookie';
|
||||
import { createScopedLogger } from '~/utils/logger';
|
||||
|
||||
const logger = createScopedLogger('LogStore');
|
||||
|
||||
export interface LogEntry {
|
||||
id: string;
|
||||
timestamp: string;
|
||||
level: 'info' | 'warning' | 'error' | 'debug';
|
||||
message: string;
|
||||
details?: Record<string, any>;
|
||||
category: 'system' | 'provider' | 'user' | 'error';
|
||||
}
|
||||
|
||||
const MAX_LOGS = 1000; // Maximum number of logs to keep in memory
|
||||
|
||||
class LogStore {
|
||||
private _logs = map<Record<string, LogEntry>>({});
|
||||
showLogs = atom(false);
|
||||
|
||||
constructor() {
|
||||
// Load saved logs from cookies on initialization
|
||||
this._loadLogs();
|
||||
}
|
||||
|
||||
private _loadLogs() {
|
||||
const savedLogs = Cookies.get('eventLogs');
|
||||
|
||||
if (savedLogs) {
|
||||
try {
|
||||
const parsedLogs = JSON.parse(savedLogs);
|
||||
this._logs.set(parsedLogs);
|
||||
} catch (error) {
|
||||
logger.error('Failed to parse logs from cookies:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _saveLogs() {
|
||||
const currentLogs = this._logs.get();
|
||||
Cookies.set('eventLogs', JSON.stringify(currentLogs));
|
||||
}
|
||||
|
||||
private _generateId(): string {
|
||||
return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
||||
}
|
||||
|
||||
private _trimLogs() {
|
||||
const currentLogs = Object.entries(this._logs.get());
|
||||
|
||||
if (currentLogs.length > MAX_LOGS) {
|
||||
const sortedLogs = currentLogs.sort(
|
||||
([, a], [, b]) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime(),
|
||||
);
|
||||
const newLogs = Object.fromEntries(sortedLogs.slice(0, MAX_LOGS));
|
||||
this._logs.set(newLogs);
|
||||
}
|
||||
}
|
||||
|
||||
addLog(
|
||||
message: string,
|
||||
level: LogEntry['level'] = 'info',
|
||||
category: LogEntry['category'] = 'system',
|
||||
details?: Record<string, any>,
|
||||
) {
|
||||
const id = this._generateId();
|
||||
const entry: LogEntry = {
|
||||
id,
|
||||
timestamp: new Date().toISOString(),
|
||||
level,
|
||||
message,
|
||||
details,
|
||||
category,
|
||||
};
|
||||
|
||||
this._logs.setKey(id, entry);
|
||||
this._trimLogs();
|
||||
this._saveLogs();
|
||||
|
||||
return id;
|
||||
}
|
||||
|
||||
// System events
|
||||
logSystem(message: string, details?: Record<string, any>) {
|
||||
return this.addLog(message, 'info', 'system', details);
|
||||
}
|
||||
|
||||
// Provider events
|
||||
logProvider(message: string, details?: Record<string, any>) {
|
||||
return this.addLog(message, 'info', 'provider', details);
|
||||
}
|
||||
|
||||
// User actions
|
||||
logUserAction(message: string, details?: Record<string, any>) {
|
||||
return this.addLog(message, 'info', 'user', details);
|
||||
}
|
||||
|
||||
// Error events
|
||||
logError(message: string, error?: Error | unknown, details?: Record<string, any>) {
|
||||
const errorDetails = {
|
||||
...(details || {}),
|
||||
error:
|
||||
error instanceof Error
|
||||
? {
|
||||
message: error.message,
|
||||
stack: error.stack,
|
||||
}
|
||||
: error,
|
||||
};
|
||||
return this.addLog(message, 'error', 'error', errorDetails);
|
||||
}
|
||||
|
||||
// Warning events
|
||||
logWarning(message: string, details?: Record<string, any>) {
|
||||
return this.addLog(message, 'warning', 'system', details);
|
||||
}
|
||||
|
||||
// Debug events
|
||||
logDebug(message: string, details?: Record<string, any>) {
|
||||
return this.addLog(message, 'debug', 'system', details);
|
||||
}
|
||||
|
||||
clearLogs() {
|
||||
this._logs.set({});
|
||||
this._saveLogs();
|
||||
}
|
||||
|
||||
getLogs() {
|
||||
return Object.values(this._logs.get()).sort(
|
||||
(a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime(),
|
||||
);
|
||||
}
|
||||
|
||||
getFilteredLogs(level?: LogEntry['level'], category?: LogEntry['category'], searchQuery?: string) {
|
||||
return this.getLogs().filter((log) => {
|
||||
const matchesLevel = !level || level === 'debug' || log.level === level;
|
||||
const matchesCategory = !category || log.category === category;
|
||||
const matchesSearch =
|
||||
!searchQuery ||
|
||||
log.message.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
JSON.stringify(log.details).toLowerCase().includes(searchQuery.toLowerCase());
|
||||
|
||||
return matchesLevel && matchesCategory && matchesSearch;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export const logStore = new LogStore();
|
@ -43,4 +43,6 @@ export const providersStore = map<ProviderSetting>(initialProviderSettings);
|
||||
|
||||
export const isDebugMode = atom(false);
|
||||
|
||||
export const isEventLogsEnabled = atom(false);
|
||||
|
||||
export const isLocalModelsEnabled = atom(true);
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { atom } from 'nanostores';
|
||||
import { logStore } from './logs';
|
||||
|
||||
export type Theme = 'dark' | 'light';
|
||||
|
||||
@ -26,10 +27,8 @@ function initStore() {
|
||||
export function toggleTheme() {
|
||||
const currentTheme = themeStore.get();
|
||||
const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
|
||||
|
||||
themeStore.set(newTheme);
|
||||
|
||||
logStore.logSystem(`Theme changed to ${newTheme} mode`);
|
||||
localStorage.setItem(kTheme, newTheme);
|
||||
|
||||
document.querySelector('html')?.setAttribute('data-theme', newTheme);
|
||||
}
|
||||
|
19
app/root.tsx
19
app/root.tsx
@ -78,6 +78,23 @@ export function Layout({ children }: { children: React.ReactNode }) {
|
||||
);
|
||||
}
|
||||
|
||||
import { logStore } from './lib/stores/logs';
|
||||
|
||||
export default function App() {
|
||||
return <Outlet />;
|
||||
const theme = useStore(themeStore);
|
||||
|
||||
useEffect(() => {
|
||||
logStore.logSystem('Application initialized', {
|
||||
theme,
|
||||
platform: navigator.platform,
|
||||
userAgent: navigator.userAgent,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Layout>
|
||||
<Outlet />
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
|
@ -2,6 +2,7 @@ import Cookies from 'js-cookie';
|
||||
import type { ModelInfo, OllamaApiResponse, OllamaModel } from './types';
|
||||
import type { ProviderInfo, IProviderSetting } from '~/types/model';
|
||||
import { createScopedLogger } from './logger';
|
||||
import { logStore } from '~/lib/stores/logs';
|
||||
|
||||
export const WORK_DIR_NAME = 'project';
|
||||
export const WORK_DIR = `/home/${WORK_DIR_NAME}`;
|
||||
@ -373,12 +374,6 @@ const getOllamaBaseUrl = (settings?: IProviderSetting) => {
|
||||
};
|
||||
|
||||
async function getOllamaModels(apiKeys?: Record<string, string>, settings?: IProviderSetting): Promise<ModelInfo[]> {
|
||||
/*
|
||||
* if (typeof window === 'undefined') {
|
||||
* return [];
|
||||
* }
|
||||
*/
|
||||
|
||||
try {
|
||||
const baseUrl = getOllamaBaseUrl(settings);
|
||||
const response = await fetch(`${baseUrl}/api/tags`);
|
||||
@ -391,7 +386,9 @@ async function getOllamaModels(apiKeys?: Record<string, string>, settings?: IPro
|
||||
maxTokenAllowed: 8000,
|
||||
}));
|
||||
} catch (e: any) {
|
||||
logStore.logError('Failed to get Ollama models', e, { baseUrl: settings?.baseUrl });
|
||||
logger.warn('Failed to get Ollama models: ', e.message || '');
|
||||
|
||||
return [];
|
||||
}
|
||||
}
|
||||
@ -480,7 +477,9 @@ async function getLMStudioModels(_apiKeys?: Record<string, string>, settings?: I
|
||||
provider: 'LMStudio',
|
||||
}));
|
||||
} catch (e: any) {
|
||||
logStore.logError('Failed to get LMStudio models', e, { baseUrl: settings?.baseUrl });
|
||||
logger.warn('Failed to get LMStudio models: ', e.message || '');
|
||||
|
||||
return [];
|
||||
}
|
||||
}
|
||||
@ -499,6 +498,7 @@ async function initializeModelList(providerSettings?: Record<string, IProviderSe
|
||||
}
|
||||
}
|
||||
} catch (error: any) {
|
||||
logStore.logError('Failed to fetch API keys from cookies', error);
|
||||
logger.warn(`Failed to fetch apikeys from cookies: ${error?.message}`);
|
||||
}
|
||||
MODEL_LIST = [
|
||||
|
Loading…
Reference in New Issue
Block a user