diff --git a/.husky/pre-commit b/.husky/pre-commit index 1aab67d..b95e00d 100644 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -2,25 +2,42 @@ echo "🔍 Running pre-commit hook to check the code looks good... 🔍" +# Load NVM if available (useful for managing Node.js versions) export NVM_DIR="$HOME/.nvm" -[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" # Load nvm if you're using i +[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" -echo "Running typecheck..." -which pnpm - -if ! pnpm typecheck; then - echo "❌ Type checking failed! Please review TypeScript types." - echo "Once you're done, don't forget to add your changes to the commit! 🚀" - echo "Typecheck exit code: $?" - exit 1 -fi - -echo "Running lint..." -if ! pnpm lint; then - echo "❌ Linting failed! 'pnpm lint:fix' will help you fix the easy ones." - echo "Once you're done, don't forget to add your beautification to the commit! 🤩" - echo "lint exit code: $?" +# Ensure `pnpm` is available +echo "Checking if pnpm is available..." +if ! command -v pnpm >/dev/null 2>&1; then + echo "❌ pnpm not found! Please ensure pnpm is installed and available in PATH." exit 1 fi -echo "👍 All good! Committing changes..." +# Run typecheck +echo "Running typecheck..." +if ! pnpm typecheck; then + echo "❌ Type checking failed! Please review TypeScript types." + echo "Once you're done, don't forget to add your changes to the commit! 🚀" + exit 1 +fi + +# Run lint +echo "Running lint..." +if ! pnpm lint; then + echo "❌ Linting failed! Run 'pnpm lint:fix' to fix the easy issues." + echo "Once you're done, don't forget to add your beautification to the commit! 🤩" + exit 1 +fi + +# Update commit.json with the latest commit hash +echo "Updating commit.json with the latest commit hash..." +COMMIT_HASH=$(git rev-parse HEAD) +if [ $? -ne 0 ]; then + echo "❌ Failed to get commit hash. Ensure you are in a git repository." + exit 1 +fi + +echo "{ \"commit\": \"$COMMIT_HASH\" }" > app/commit.json +git add app/commit.json + +echo "👍 All checks passed! Committing changes..." diff --git a/app/commit.json b/app/commit.json index 814decb..850911f 100644 --- a/app/commit.json +++ b/app/commit.json @@ -1 +1,2 @@ { "commit": "eeafc12522b184dcbded28c5c6606e4a23e6849f" } + diff --git a/app/components/chat/ImportFolderButton.tsx b/app/components/chat/ImportFolderButton.tsx index 6cbfcac..c75200e 100644 --- a/app/components/chat/ImportFolderButton.tsx +++ b/app/components/chat/ImportFolderButton.tsx @@ -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 = ({ 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 = ({ 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 = ({ 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 = ({ 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 { diff --git a/app/components/settings/SettingsWindow.tsx b/app/components/settings/SettingsWindow.tsx index b7b368d..aae0a86 100644 --- a/app/components/settings/SettingsWindow.tsx +++ b/app/components/settings/SettingsWindow.tsx @@ -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('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: , + }, + ] + : []), ]; return ( diff --git a/app/components/settings/chat-history/ChatHistoryTab.tsx b/app/components/settings/chat-history/ChatHistoryTab.tsx index e96f0d8..76a0f21 100644 --- a/app/components/settings/chat-history/ChatHistoryTab.tsx +++ b/app/components/settings/chat-history/ChatHistoryTab.tsx @@ -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); } diff --git a/app/components/settings/connections/ConnectionsTab.tsx b/app/components/settings/connections/ConnectionsTab.tsx index 32d0fa0..8338395 100644 --- a/app/components/settings/connections/ConnectionsTab.tsx +++ b/app/components/settings/connections/ConnectionsTab.tsx @@ -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!'); }; diff --git a/app/components/settings/debug/DebugTab.tsx b/app/components/settings/debug/DebugTab.tsx index 7a84ec1..e18607d 100644 --- a/app/components/settings/debug/DebugTab.tsx +++ b/app/components/settings/debug/DebugTab.tsx @@ -2,68 +2,493 @@ import React, { useCallback, useEffect, useState } from 'react'; import { useSettings } from '~/lib/hooks/useSettings'; import commit from '~/commit.json'; -const versionHash = commit.commit; // Get the version hash from commit.json +interface ProviderStatus { + name: string; + enabled: boolean; + isLocal: boolean; + isRunning: boolean | null; + error?: string; + lastChecked: Date; + responseTime?: number; + url: string | null; +} + +interface SystemInfo { + os: string; + browser: string; + screen: string; + language: string; + timezone: string; + memory: string; + cores: number; +} + +interface IProviderConfig { + name: string; + settings: { + enabled: boolean; + }; +} + +const LOCAL_PROVIDERS = ['Ollama', 'LMStudio', 'OpenAILike']; +const versionHash = commit.commit; +const GITHUB_URLS = { + original: 'https://api.github.com/repos/stackblitz-labs/bolt.diy/commits/main', + fork: 'https://api.github.com/repos/Stijnus/bolt.new-any-llm/commits/main', +}; + +function getSystemInfo(): SystemInfo { + const formatBytes = (bytes: number): string => { + if (bytes === 0) { + return '0 Bytes'; + } + + const k = 1024; + const sizes = ['Bytes', 'KB', 'MB', 'GB']; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + + return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; + }; + + return { + os: navigator.platform, + browser: navigator.userAgent.split(' ').slice(-1)[0], + screen: `${window.screen.width}x${window.screen.height}`, + language: navigator.language, + timezone: Intl.DateTimeFormat().resolvedOptions().timeZone, + memory: formatBytes(performance?.memory?.jsHeapSizeLimit || 0), + cores: navigator.hardwareConcurrency || 0, + }; +} + +const checkProviderStatus = async (url: string | null, providerName: string): Promise => { + if (!url) { + console.log(`[Debug] No URL provided for ${providerName}`); + return { + name: providerName, + enabled: false, + isLocal: true, + isRunning: false, + error: 'No URL configured', + lastChecked: new Date(), + url: null, + }; + } + + console.log(`[Debug] Checking status for ${providerName} at ${url}`); + + const startTime = performance.now(); + + try { + if (providerName.toLowerCase() === 'ollama') { + // Special check for Ollama root endpoint + try { + console.log(`[Debug] Checking Ollama root endpoint: ${url}`); + + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), 5000); // 5 second timeout + + const response = await fetch(url, { + signal: controller.signal, + headers: { + Accept: 'text/plain,application/json', + }, + }); + clearTimeout(timeoutId); + + const text = await response.text(); + console.log(`[Debug] Ollama root response:`, text); + + if (text.includes('Ollama is running')) { + console.log(`[Debug] Ollama running confirmed via root endpoint`); + return { + name: providerName, + enabled: false, + isLocal: true, + isRunning: true, + lastChecked: new Date(), + responseTime: performance.now() - startTime, + url, + }; + } + } catch (error) { + console.log(`[Debug] Ollama root check failed:`, error); + + const errorMessage = error instanceof Error ? error.message : 'Unknown error'; + + if (errorMessage.includes('aborted')) { + return { + name: providerName, + enabled: false, + isLocal: true, + isRunning: false, + error: 'Connection timeout', + lastChecked: new Date(), + responseTime: performance.now() - startTime, + url, + }; + } + } + } + + // Try different endpoints based on provider + const checkUrls = [`${url}/api/health`, `${url}/v1/models`]; + console.log(`[Debug] Checking additional endpoints:`, checkUrls); + + const results = await Promise.all( + checkUrls.map(async (checkUrl) => { + try { + console.log(`[Debug] Trying endpoint: ${checkUrl}`); + + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), 5000); + + const response = await fetch(checkUrl, { + signal: controller.signal, + headers: { + Accept: 'application/json', + }, + }); + clearTimeout(timeoutId); + + const ok = response.ok; + console.log(`[Debug] Endpoint ${checkUrl} response:`, ok); + + if (ok) { + try { + const data = await response.json(); + console.log(`[Debug] Endpoint ${checkUrl} data:`, data); + } catch { + console.log(`[Debug] Could not parse JSON from ${checkUrl}`); + } + } + + return ok; + } catch (error) { + console.log(`[Debug] Endpoint ${checkUrl} failed:`, error); + return false; + } + }), + ); + + const isRunning = results.some((result) => result); + console.log(`[Debug] Final status for ${providerName}:`, isRunning); + + return { + name: providerName, + enabled: false, + isLocal: true, + isRunning, + lastChecked: new Date(), + responseTime: performance.now() - startTime, + url, + }; + } catch (error) { + console.log(`[Debug] Provider check failed for ${providerName}:`, error); + return { + name: providerName, + enabled: false, + isLocal: true, + isRunning: false, + error: error instanceof Error ? error.message : 'Unknown error', + lastChecked: new Date(), + responseTime: performance.now() - startTime, + url, + }; + } +}; export default function DebugTab() { const { providers } = useSettings(); - const [activeProviders, setActiveProviders] = useState([]); + const [activeProviders, setActiveProviders] = useState([]); + const [updateMessage, setUpdateMessage] = useState(''); + const [systemInfo] = useState(getSystemInfo()); + const [isCheckingUpdate, setIsCheckingUpdate] = useState(false); + + const updateProviderStatuses = async () => { + if (!providers) { + return; + } + + try { + const entries = Object.entries(providers) as [string, IProviderConfig][]; + const statuses = entries + .filter(([, provider]) => LOCAL_PROVIDERS.includes(provider.name)) + .map(async ([, provider]) => { + const envVarName = + provider.name.toLowerCase() === 'ollama' + ? 'OLLAMA_API_BASE_URL' + : provider.name.toLowerCase() === 'lmstudio' + ? 'LMSTUDIO_API_BASE_URL' + : `REACT_APP_${provider.name.toUpperCase()}_URL`; + + // Access environment variables through import.meta.env + const url = import.meta.env[envVarName] || null; + console.log(`[Debug] Using URL for ${provider.name}:`, url, `(from ${envVarName})`); + + const status = await checkProviderStatus(url, provider.name); + + return { + ...status, + enabled: provider.settings.enabled ?? false, + }; + }); + + Promise.all(statuses).then(setActiveProviders); + } catch (error) { + console.error('[Debug] Failed to update provider statuses:', error); + } + }; + useEffect(() => { - setActiveProviders( - Object.entries(providers) - .filter(([_key, provider]) => provider.settings.enabled) - .map(([_key, provider]) => provider.name), - ); + updateProviderStatuses(); + + const interval = setInterval(updateProviderStatuses, 30000); + + return () => clearInterval(interval); }, [providers]); + const handleCheckForUpdate = useCallback(async () => { + if (isCheckingUpdate) { + return; + } + + try { + setIsCheckingUpdate(true); + setUpdateMessage('Checking for updates...'); + + const [originalResponse, forkResponse] = await Promise.all([ + fetch(GITHUB_URLS.original), + fetch(GITHUB_URLS.fork), + ]); + + if (!originalResponse.ok || !forkResponse.ok) { + throw new Error('Failed to fetch repository information'); + } + + const [originalData, forkData] = await Promise.all([ + originalResponse.json() as Promise<{ sha: string }>, + forkResponse.json() as Promise<{ sha: string }>, + ]); + + const originalCommitHash = originalData.sha; + const forkCommitHash = forkData.sha; + const isForked = versionHash === forkCommitHash && forkCommitHash !== originalCommitHash; + + if (originalCommitHash !== versionHash) { + setUpdateMessage( + `Update available from original repository!\n` + + `Current: ${versionHash.slice(0, 7)}${isForked ? ' (forked)' : ''}\n` + + `Latest: ${originalCommitHash.slice(0, 7)}`, + ); + } else { + setUpdateMessage('You are on the latest version from the original repository'); + } + } catch (error) { + setUpdateMessage('Failed to check for updates'); + console.error('[Debug] Failed to check for updates:', error); + } finally { + setIsCheckingUpdate(false); + } + }, [isCheckingUpdate]); + const handleCopyToClipboard = useCallback(() => { const debugInfo = { - OS: navigator.platform, - Browser: navigator.userAgent, - ActiveFeatures: activeProviders, - BaseURLs: { - Ollama: process.env.REACT_APP_OLLAMA_URL, - OpenAI: process.env.REACT_APP_OPENAI_URL, - LMStudio: process.env.REACT_APP_LM_STUDIO_URL, - }, + System: systemInfo, + Providers: activeProviders.map((provider) => ({ + name: provider.name, + enabled: provider.enabled, + isLocal: provider.isLocal, + running: provider.isRunning, + error: provider.error, + lastChecked: provider.lastChecked, + responseTime: provider.responseTime, + url: provider.url, + })), Version: versionHash, + Timestamp: new Date().toISOString(), }; navigator.clipboard.writeText(JSON.stringify(debugInfo, null, 2)).then(() => { alert('Debug information copied to clipboard!'); }); - }, [providers]); + }, [activeProviders, systemInfo]); return ( -
-

Debug Tab

- +
+
+

Debug Information

+
+ + +
+
-

System Information

-

OS: {navigator.platform}

-

Browser: {navigator.userAgent}

+ {updateMessage && ( +
+

{updateMessage}

+ {updateMessage.includes('Update available') && ( +
+

To update:

+
    +
  1. + Pull the latest changes:{' '} + git pull upstream main +
  2. +
  3. + Install any new dependencies:{' '} + pnpm install +
  4. +
  5. Restart the application
  6. +
+
+ )} +
+ )} -

Active Features

-
    - {activeProviders.map((name) => ( -
  • - {name} -
  • - ))} -
+
+
+

System Information

+
+
+
+

Operating System

+

{systemInfo.os}

+
+
+

Browser

+

{systemInfo.browser}

+
+
+

Screen Resolution

+

{systemInfo.screen}

+
+
+

Language

+

{systemInfo.language}

+
+
+

Timezone

+

{systemInfo.timezone}

+
+
+

CPU Cores

+

{systemInfo.cores}

+
+
+
+

Version

+

+ {versionHash.slice(0, 7)} + + ({new Date().toLocaleDateString()}) + +

+
+
+
-

Base URLs

-
    -
  • Ollama: {process.env.REACT_APP_OLLAMA_URL}
  • -
  • OpenAI: {process.env.REACT_APP_OPENAI_URL}
  • -
  • LM Studio: {process.env.REACT_APP_LM_STUDIO_URL}
  • -
+
+

Local LLM Status

+
+
+ {activeProviders.map((provider) => ( +
+
+
+
+
+
+
+

{provider.name}

+ {provider.url && ( +

+ {provider.url} +

+ )} +
+
+
+ + {provider.enabled ? 'Enabled' : 'Disabled'} + + {provider.enabled && ( + + {provider.isRunning ? 'Running' : 'Not Running'} + + )} +
+
-

Version Information

-

Version Hash: {versionHash}

+
+ {/* Status Details */} +
+ + Last checked: {new Date(provider.lastChecked).toLocaleTimeString()} + + {provider.responseTime && ( + + Response time: {Math.round(provider.responseTime)}ms + + )} +
+ + {/* Error Message */} + {provider.error && ( +
+ Error: {provider.error} +
+ )} + + {/* Connection Info */} + {provider.url && ( +
+ Endpoints checked: +
    +
  • {provider.url} (root)
  • +
  • {provider.url}/api/health
  • +
  • {provider.url}/v1/models
  • +
+
+ )} +
+
+ ))} + {activeProviders.length === 0 && ( +
No local LLMs configured
+ )} +
+
+
+
); } diff --git a/app/components/settings/event-logs/EventLogsTab.tsx b/app/components/settings/event-logs/EventLogsTab.tsx new file mode 100644 index 0000000..5c1ed44 --- /dev/null +++ b/app/components/settings/event-logs/EventLogsTab.tsx @@ -0,0 +1,219 @@ +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'; +import { useStore } from '@nanostores/react'; +import { classNames } from '~/utils/classNames'; + +export default function EventLogsTab() { + const {} = useSettings(); + const showLogs = useStore(logStore.showLogs); + const [logLevel, setLogLevel] = useState('info'); + const [autoScroll, setAutoScroll] = useState(true); + const [searchQuery, setSearchQuery] = useState(''); + const [, forceUpdate] = useState({}); + + const filteredLogs = useMemo(() => { + const logs = logStore.getLogs(); + return logs.filter((log) => { + const matchesLevel = !logLevel || log.level === logLevel || logLevel === 'all'; + const matchesSearch = + !searchQuery || + log.message?.toLowerCase().includes(searchQuery.toLowerCase()) || + JSON.stringify(log.details)?.toLowerCase()?.includes(searchQuery?.toLowerCase()); + + return matchesLevel && matchesSearch; + }); + }, [logLevel, searchQuery]); + + // Effect to initialize showLogs + useEffect(() => { + logStore.showLogs.set(true); + }, []); + + useEffect(() => { + // System info logs + logStore.logSystem('Application initialized', { + version: process.env.NEXT_PUBLIC_APP_VERSION, + environment: process.env.NODE_ENV, + }); + + // Debug logs for system state + logStore.logDebug('System configuration loaded', { + runtime: 'Next.js', + features: ['AI Chat', 'Event Logging'], + }); + + // Warning logs for potential issues + logStore.logWarning('Resource usage threshold approaching', { + memoryUsage: '75%', + cpuLoad: '60%', + }); + + // Error logs with detailed context + logStore.logError('API connection failed', new Error('Connection timeout'), { + endpoint: '/api/chat', + retryCount: 3, + lastAttempt: new Date().toISOString(), + }); + }, []); + + useEffect(() => { + const container = document.querySelector('.logs-container'); + + if (container && autoScroll) { + container.scrollTop = container.scrollHeight; + } + }, [filteredLogs, autoScroll]); + + 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 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 ( +
+
+ {/* Title and Toggles Row */} +
+

Event Logs

+
+
+ Show Actions + logStore.showLogs.set(checked)} /> +
+
+ Auto-scroll + +
+
+
+ + {/* Controls Row */} +
+ +
+ setSearchQuery(e.target.value)} + 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" + /> +
+ {showLogs && ( +
+ + +
+ )} +
+
+ +
+ {filteredLogs.length === 0 ? ( +
No logs found
+ ) : ( + filteredLogs.map((log, index) => ( +
+
+ + [{log.level.toUpperCase()}] + + + {new Date(log.timestamp).toLocaleString()} + + {log.message} +
+ {log.details && ( +
+                  {JSON.stringify(log.details, null, 2)}
+                
+ )} +
+ )) + )} +
+
+ ); +} diff --git a/app/components/settings/features/FeaturesTab.tsx b/app/components/settings/features/FeaturesTab.tsx index 0b4fa75..9c9e4a0 100644 --- a/app/components/settings/features/FeaturesTab.tsx +++ b/app/components/settings/features/FeaturesTab.tsx @@ -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 (
@@ -12,6 +12,10 @@ export default function FeaturesTab() { Debug Info
+
+ Event Logs + +
diff --git a/app/components/settings/providers/ProvidersTab.tsx b/app/components/settings/providers/ProvidersTab.tsx index 309afb8..0efbc66 100644 --- a/app/components/settings/providers/ProvidersTab.tsx +++ b/app/components/settings/providers/ProvidersTab.tsx @@ -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(); @@ -49,11 +50,22 @@ export default function ProvidersTab() { className="flex flex-col mb-2 provider-item hover:bg-bolt-elements-bg-depth-3 p-4 rounded-lg border border-bolt-elements-borderColor " >
- {provider.name} +
+ {`${provider.name} + {provider.name} +
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 }); + } + }} />
{/* Base URL input for configurable providers */} @@ -63,9 +75,14 @@ export default function ProvidersTab() { - 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" /> diff --git a/app/components/sidebar/Menu.client.tsx b/app/components/sidebar/Menu.client.tsx index 38ffb54..78d5137 100644 --- a/app/components/sidebar/Menu.client.tsx +++ b/app/components/sidebar/Menu.client.tsx @@ -35,6 +35,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 ( +
+
+ {dateTime.toLocaleDateString()} {dateTime.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })} +
+ ); +} + export const Menu = () => { const { duplicateCurrentChat, exportChat } = useChatHistory(); const menuRef = useRef(null); @@ -126,18 +145,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" > -
{/* Placeholder */}
+
{/* Spacer for top margin */} +
-
([]); @@ -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, }; diff --git a/app/lib/persistence/useChatHistory.ts b/app/lib/persistence/useChatHistory.ts index ca44b4b..0a8eeb5 100644 --- a/app/lib/persistence/useChatHistory.ts +++ b/app/lib/persistence/useChatHistory.ts @@ -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); }); } diff --git a/app/lib/stores/logs.ts b/app/lib/stores/logs.ts new file mode 100644 index 0000000..4b306a5 --- /dev/null +++ b/app/lib/stores/logs.ts @@ -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; + category: 'system' | 'provider' | 'user' | 'error'; +} + +const MAX_LOGS = 1000; // Maximum number of logs to keep in memory + +class LogStore { + private _logs = map>({}); + showLogs = atom(true); + + 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, + ) { + 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) { + return this.addLog(message, 'info', 'system', details); + } + + // Provider events + logProvider(message: string, details?: Record) { + return this.addLog(message, 'info', 'provider', details); + } + + // User actions + logUserAction(message: string, details?: Record) { + return this.addLog(message, 'info', 'user', details); + } + + // Error events + logError(message: string, error?: Error | unknown, details?: Record) { + 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) { + return this.addLog(message, 'warning', 'system', details); + } + + // Debug events + logDebug(message: string, details?: Record) { + 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(); diff --git a/app/lib/stores/settings.ts b/app/lib/stores/settings.ts index e86b3be..abbb825 100644 --- a/app/lib/stores/settings.ts +++ b/app/lib/stores/settings.ts @@ -43,4 +43,6 @@ export const providersStore = map(initialProviderSettings); export const isDebugMode = atom(false); +export const isEventLogsEnabled = atom(false); + export const isLocalModelsEnabled = atom(true); diff --git a/app/lib/stores/theme.ts b/app/lib/stores/theme.ts index 4f3e47b..e70cb15 100644 --- a/app/lib/stores/theme.ts +++ b/app/lib/stores/theme.ts @@ -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); } diff --git a/app/root.tsx b/app/root.tsx index 31eb387..23978f6 100644 --- a/app/root.tsx +++ b/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 ; + const theme = useStore(themeStore); + + useEffect(() => { + logStore.logSystem('Application initialized', { + theme, + platform: navigator.platform, + userAgent: navigator.userAgent, + timestamp: new Date().toISOString(), + }); + }, []); + + return ( + + + + ); } diff --git a/app/types/global.d.ts b/app/types/global.d.ts index 6a03036..e0ad664 100644 --- a/app/types/global.d.ts +++ b/app/types/global.d.ts @@ -3,3 +3,11 @@ interface Window { webkitSpeechRecognition: typeof SpeechRecognition; SpeechRecognition: typeof SpeechRecognition; } + +interface Performance { + memory?: { + jsHeapSizeLimit: number; + totalJSHeapSize: number; + usedJSHeapSize: number; + }; +} diff --git a/app/utils/constants.ts b/app/utils/constants.ts index 240b4b9..61e709b 100644 --- a/app/utils/constants.ts +++ b/app/utils/constants.ts @@ -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, settings?: IProviderSetting): Promise { - /* - * 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, 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, 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 + + + diff --git a/public/icons/Cohere.svg b/public/icons/Cohere.svg new file mode 100644 index 0000000..c7531c4 --- /dev/null +++ b/public/icons/Cohere.svg @@ -0,0 +1,4 @@ + + + + diff --git a/public/icons/Deepseek.svg b/public/icons/Deepseek.svg new file mode 100644 index 0000000..c3a4049 --- /dev/null +++ b/public/icons/Deepseek.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/public/icons/Google.svg b/public/icons/Google.svg new file mode 100644 index 0000000..79b262a --- /dev/null +++ b/public/icons/Google.svg @@ -0,0 +1,4 @@ + + + + diff --git a/public/icons/Groq.svg b/public/icons/Groq.svg new file mode 100644 index 0000000..b198fb4 --- /dev/null +++ b/public/icons/Groq.svg @@ -0,0 +1,4 @@ + + + + diff --git a/public/icons/HuggingFace.svg b/public/icons/HuggingFace.svg new file mode 100644 index 0000000..6a70cd8 --- /dev/null +++ b/public/icons/HuggingFace.svg @@ -0,0 +1,4 @@ + + + + diff --git a/public/icons/LMStudio.svg b/public/icons/LMStudio.svg new file mode 100644 index 0000000..d8d9887 --- /dev/null +++ b/public/icons/LMStudio.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/public/icons/Mistral.svg b/public/icons/Mistral.svg new file mode 100644 index 0000000..7526259 --- /dev/null +++ b/public/icons/Mistral.svg @@ -0,0 +1,4 @@ + + + + diff --git a/public/icons/Ollama.svg b/public/icons/Ollama.svg new file mode 100644 index 0000000..dd63997 --- /dev/null +++ b/public/icons/Ollama.svg @@ -0,0 +1,4 @@ + + + + diff --git a/public/icons/OpenAI.svg b/public/icons/OpenAI.svg new file mode 100644 index 0000000..5af0871 --- /dev/null +++ b/public/icons/OpenAI.svg @@ -0,0 +1,4 @@ + + + + diff --git a/public/icons/OpenAILike.svg b/public/icons/OpenAILike.svg new file mode 100644 index 0000000..20e3813 --- /dev/null +++ b/public/icons/OpenAILike.svg @@ -0,0 +1,4 @@ + + + + diff --git a/public/icons/OpenRouter.svg b/public/icons/OpenRouter.svg new file mode 100644 index 0000000..7cfe36b --- /dev/null +++ b/public/icons/OpenRouter.svg @@ -0,0 +1,4 @@ + + + + diff --git a/public/icons/Together.svg b/public/icons/Together.svg new file mode 100644 index 0000000..a2975f1 --- /dev/null +++ b/public/icons/Together.svg @@ -0,0 +1,4 @@ + + + + diff --git a/public/icons/xAI.svg b/public/icons/xAI.svg new file mode 100644 index 0000000..5a57ea5 --- /dev/null +++ b/public/icons/xAI.svg @@ -0,0 +1,5 @@ + + + + +