Stijnus
commited on
Commit
·
e39f16e
1
Parent(s):
e716ca5
Settings UI enhancement
Browse filesDate & 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 +23 -0
- app/components/settings/SettingsWindow.tsx +13 -3
- app/components/settings/chat-history/ChatHistoryTab.tsx +11 -3
- app/components/settings/connections/ConnectionsTab.tsx +5 -0
- app/components/settings/event-logs/EventLogsTab.tsx +145 -0
- app/components/settings/features/FeaturesTab.tsx +5 -1
- app/components/settings/providers/ProvidersTab.tsx +18 -4
- app/components/sidebar/Menu.client.tsx +23 -4
- app/lib/hooks/useSettings.tsx +26 -1
- app/lib/persistence/useChatHistory.ts +4 -0
- app/lib/stores/logs.ts +149 -0
- app/lib/stores/settings.ts +2 -0
- app/lib/stores/theme.ts +2 -3
- app/root.tsx +18 -1
- app/utils/constants.ts +6 -6
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) =>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
-
|
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="
|
|
|
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 {
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 = [
|