V1 : Release of the new Settings Dashboard
# 🚀 Release v1.0.0 ## What's Changed 🌟 ### 🎨 UI/UX Improvements - **Dark Mode Support** - Implemented comprehensive dark theme across all components - Enhanced contrast and readability in dark mode - Added smooth theme transitions - Optimized dialog overlays and backdrops ### 🛠️ Settings Panel - **Data Management** - Added chat history export/import functionality - Implemented settings backup and restore - Added secure data deletion with confirmations - Added profile customization options - **Provider Management** - Added comprehensive provider configuration - Implemented URL-configurable providers - Added local model support (Ollama, LMStudio) - Added provider health checks - Added provider status indicators - **Ollama Integration** - Added Ollama Model Manager with real-time updates - Implemented model version tracking - Added bulk update capability - Added progress tracking for model updates - Displays model details (parameter size, quantization) - **GitHub Integration** - Added GitHub connection management - Implemented secure token storage - Added connection state persistence - Real-time connection status updates - Proper error handling and user feedback ### 📊 Event Logging - **System Monitoring** - Added real-time event logging system - Implemented log filtering by type (info, warning, error, debug) - Added log export functionality - Added auto-scroll and search capabilities - Enhanced log visualization with color coding ### 💫 Animations & Interactions - Added smooth page transitions - Implemented loading states with spinners - Added micro-interactions for better feedback - Enhanced button hover and active states - Added motion effects for UI elements ### 🔐 Security Features - Secure token storage - Added confirmation dialogs for destructive actions - Implemented data validation - Added file size and type validation - Secure connection management ### ♿️ Accessibility - Improved keyboard navigation - Enhanced screen reader support - Added ARIA labels and descriptions - Implemented focus management - Added proper dialog accessibility ### 🎯 Developer Experience - Added comprehensive debug information - Implemented system status monitoring - Added version control integration - Enhanced error handling and reporting - Added detailed logging system --- ## 🔧 Technical Details - **Frontend Stack** - React 18 with TypeScript - Framer Motion for animations - TailwindCSS for styling - Radix UI for accessible components - **State Management** - Local storage for persistence - React hooks for state - Custom stores for global state - **API Integration** - GitHub API integration - Ollama API integration - Provider API management - Error boundary implementation ## 📝 Notes - Initial release focusing on core functionality and user experience - Enhanced dark mode support across all components - Improved accessibility and keyboard navigation - Added comprehensive logging and debugging tools - Implemented robust error handling and user feedback
This commit is contained in:
@@ -1,22 +1,27 @@
|
||||
import React, { useCallback, useEffect, useState, useMemo } from 'react';
|
||||
import React, { useCallback, useEffect, useState, useMemo, useRef } 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';
|
||||
import { motion } from 'framer-motion';
|
||||
import { settingsStyles } from '~/components/settings/settings.styles';
|
||||
|
||||
export default function EventLogsTab() {
|
||||
const {} = useSettings();
|
||||
const showLogs = useStore(logStore.showLogs);
|
||||
const logs = useStore(logStore.logs);
|
||||
const [logLevel, setLogLevel] = useState<LogEntry['level'] | 'all'>('info');
|
||||
const [autoScroll, setAutoScroll] = useState(true);
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [, forceUpdate] = useState({});
|
||||
const logsContainerRef = useRef<HTMLDivElement>(null);
|
||||
const [isScrolledToBottom, setIsScrolledToBottom] = useState(true);
|
||||
|
||||
const filteredLogs = useMemo(() => {
|
||||
const logs = logStore.getLogs();
|
||||
return logs.filter((log) => {
|
||||
const allLogs = Object.values(logs);
|
||||
const filtered = allLogs.filter((log) => {
|
||||
const matchesLevel = !logLevel || log.level === logLevel || logLevel === 'all';
|
||||
const matchesSearch =
|
||||
!searchQuery ||
|
||||
@@ -25,7 +30,9 @@ export default function EventLogsTab() {
|
||||
|
||||
return matchesLevel && matchesSearch;
|
||||
});
|
||||
}, [logLevel, searchQuery]);
|
||||
|
||||
return filtered.reverse();
|
||||
}, [logs, logLevel, searchQuery]);
|
||||
|
||||
// Effect to initialize showLogs
|
||||
useEffect(() => {
|
||||
@@ -37,18 +44,51 @@ export default function EventLogsTab() {
|
||||
logStore.logSystem('Application initialized', {
|
||||
version: process.env.NEXT_PUBLIC_APP_VERSION,
|
||||
environment: process.env.NODE_ENV,
|
||||
timestamp: new Date().toISOString(),
|
||||
userAgent: navigator.userAgent,
|
||||
});
|
||||
|
||||
// Debug logs for system state
|
||||
logStore.logDebug('System configuration loaded', {
|
||||
runtime: 'Next.js',
|
||||
features: ['AI Chat', 'Event Logging'],
|
||||
features: ['AI Chat', 'Event Logging', 'Provider Management', 'Theme Support'],
|
||||
locale: navigator.language,
|
||||
timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
||||
});
|
||||
|
||||
// Performance metrics
|
||||
logStore.logSystem('Performance metrics', {
|
||||
deviceMemory: (navigator as any).deviceMemory || 'unknown',
|
||||
hardwareConcurrency: navigator.hardwareConcurrency,
|
||||
connectionType: (navigator as any).connection?.effectiveType || 'unknown',
|
||||
});
|
||||
|
||||
// Provider status
|
||||
logStore.logProvider('Provider status check', {
|
||||
availableProviders: ['OpenAI', 'Anthropic', 'Mistral', 'Ollama'],
|
||||
defaultProvider: 'OpenAI',
|
||||
status: 'operational',
|
||||
});
|
||||
|
||||
// Theme and accessibility
|
||||
logStore.logSystem('User preferences loaded', {
|
||||
theme: document.documentElement.dataset.theme || 'system',
|
||||
prefersReducedMotion: window.matchMedia('(prefers-reduced-motion: reduce)').matches,
|
||||
prefersDarkMode: window.matchMedia('(prefers-color-scheme: dark)').matches,
|
||||
});
|
||||
|
||||
// Warning logs for potential issues
|
||||
logStore.logWarning('Resource usage threshold approaching', {
|
||||
memoryUsage: '75%',
|
||||
cpuLoad: '60%',
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
|
||||
// Security checks
|
||||
logStore.logSystem('Security status', {
|
||||
httpsEnabled: window.location.protocol === 'https:',
|
||||
cookiesEnabled: navigator.cookieEnabled,
|
||||
storageQuota: 'checking...',
|
||||
});
|
||||
|
||||
// Error logs with detailed context
|
||||
@@ -56,16 +96,50 @@ export default function EventLogsTab() {
|
||||
endpoint: '/api/chat',
|
||||
retryCount: 3,
|
||||
lastAttempt: new Date().toISOString(),
|
||||
statusCode: 408,
|
||||
});
|
||||
|
||||
// Debug logs for development
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
logStore.logDebug('Development mode active', {
|
||||
debugFlags: true,
|
||||
mockServices: false,
|
||||
apiEndpoint: 'local',
|
||||
});
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Scroll handling
|
||||
useEffect(() => {
|
||||
const container = document.querySelector('.logs-container');
|
||||
const container = logsContainerRef.current;
|
||||
|
||||
if (container && autoScroll) {
|
||||
container.scrollTop = container.scrollHeight;
|
||||
if (!container) {
|
||||
return undefined;
|
||||
}
|
||||
}, [filteredLogs, autoScroll]);
|
||||
|
||||
const handleScroll = () => {
|
||||
const { scrollTop, scrollHeight, clientHeight } = container;
|
||||
const isBottom = Math.abs(scrollHeight - clientHeight - scrollTop) < 10;
|
||||
setIsScrolledToBottom(isBottom);
|
||||
};
|
||||
|
||||
container.addEventListener('scroll', handleScroll);
|
||||
|
||||
const cleanup = () => {
|
||||
container.removeEventListener('scroll', handleScroll);
|
||||
};
|
||||
|
||||
return cleanup;
|
||||
}, []);
|
||||
|
||||
// Auto-scroll effect
|
||||
useEffect(() => {
|
||||
const container = logsContainerRef.current;
|
||||
|
||||
if (container && (autoScroll || isScrolledToBottom)) {
|
||||
container.scrollTop = 0;
|
||||
}
|
||||
}, [filteredLogs, autoScroll, isScrolledToBottom]);
|
||||
|
||||
const handleClearLogs = useCallback(() => {
|
||||
if (confirm('Are you sure you want to clear all logs?')) {
|
||||
@@ -103,33 +177,56 @@ export default function EventLogsTab() {
|
||||
}
|
||||
}, []);
|
||||
|
||||
const getLevelIcon = (level: LogEntry['level']): string => {
|
||||
switch (level) {
|
||||
case 'info':
|
||||
return 'i-ph:info';
|
||||
case 'warning':
|
||||
return 'i-ph:warning';
|
||||
case 'error':
|
||||
return 'i-ph:x-circle';
|
||||
case 'debug':
|
||||
return 'i-ph:bug';
|
||||
default:
|
||||
return 'i-ph:circle';
|
||||
}
|
||||
};
|
||||
|
||||
const getLevelColor = (level: LogEntry['level']) => {
|
||||
switch (level) {
|
||||
case 'info':
|
||||
return 'text-blue-500';
|
||||
return 'text-[#1389FD] dark:text-[#1389FD]';
|
||||
case 'warning':
|
||||
return 'text-yellow-500';
|
||||
return 'text-[#FFDB6C] dark:text-[#FFDB6C]';
|
||||
case 'error':
|
||||
return 'text-red-500';
|
||||
return 'text-[#EE4744] dark:text-[#EE4744]';
|
||||
case 'debug':
|
||||
return 'text-gray-500';
|
||||
return 'text-[#77828D] dark:text-[#77828D]';
|
||||
default:
|
||||
return 'text-bolt-elements-textPrimary';
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-4 h-full flex flex-col">
|
||||
<div className="flex flex-col space-y-4 mb-4">
|
||||
<div className="space-y-4">
|
||||
<div className="flex flex-col space-y-4">
|
||||
{/* Title and Toggles Row */}
|
||||
<div className="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4">
|
||||
<h3 className="text-lg font-medium text-bolt-elements-textPrimary">Event Logs</h3>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="i-ph:list-bullets text-xl text-purple-500" />
|
||||
<div>
|
||||
<h3 className="text-lg font-medium text-bolt-elements-textPrimary">Event Logs</h3>
|
||||
<p className="text-sm text-bolt-elements-textSecondary">Track system events and debug information</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center gap-4">
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="i-ph:eye text-bolt-elements-textSecondary" />
|
||||
<span className="text-sm text-bolt-elements-textSecondary whitespace-nowrap">Show Actions</span>
|
||||
<Switch checked={showLogs} onCheckedChange={(checked) => logStore.showLogs.set(checked)} />
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="i-ph:arrow-clockwise text-bolt-elements-textSecondary" />
|
||||
<span className="text-sm text-bolt-elements-textSecondary whitespace-nowrap">Auto-scroll</span>
|
||||
<Switch checked={autoScroll} onCheckedChange={setAutoScroll} />
|
||||
</div>
|
||||
@@ -137,83 +234,166 @@ export default function EventLogsTab() {
|
||||
</div>
|
||||
|
||||
{/* Controls Row */}
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<select
|
||||
value={logLevel}
|
||||
onChange={(e) => setLogLevel(e.target.value as LogEntry['level'])}
|
||||
className="flex-1 p-2 rounded-lg border border-bolt-elements-borderColor bg-bolt-elements-prompt-background text-bolt-elements-textPrimary focus:outline-none focus:ring-2 focus:ring-bolt-elements-focus transition-all lg:max-w-[20%] text-sm min-w-[100px]"
|
||||
>
|
||||
<option value="all">All</option>
|
||||
<option value="info">Info</option>
|
||||
<option value="warning">Warning</option>
|
||||
<option value="error">Error</option>
|
||||
<option value="debug">Debug</option>
|
||||
</select>
|
||||
<div className="flex flex-wrap items-center gap-4">
|
||||
<div className="flex-1 min-w-[150px] max-w-[200px]">
|
||||
<div className="relative group">
|
||||
<select
|
||||
value={logLevel}
|
||||
onChange={(e) => setLogLevel(e.target.value as LogEntry['level'])}
|
||||
className={classNames(
|
||||
'w-full pl-9 pr-3 py-2 rounded-lg',
|
||||
'bg-bolt-elements-background-depth-2 border border-bolt-elements-borderColor',
|
||||
'text-sm text-bolt-elements-textPrimary',
|
||||
'focus:outline-none focus:ring-2 focus:ring-purple-500/30',
|
||||
'group-hover:border-purple-500/30',
|
||||
'transition-all duration-200',
|
||||
)}
|
||||
>
|
||||
<option value="all">All Levels</option>
|
||||
<option value="info">Info</option>
|
||||
<option value="warning">Warning</option>
|
||||
<option value="error">Error</option>
|
||||
<option value="debug">Debug</option>
|
||||
</select>
|
||||
<div className="i-ph:funnel absolute left-3 top-1/2 -translate-y-1/2 text-bolt-elements-textSecondary group-hover:text-purple-500 transition-colors" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-1 min-w-[200px]">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search logs..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => 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"
|
||||
/>
|
||||
<div className="relative group">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search logs..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className={classNames(
|
||||
'w-full pl-9 pr-3 py-2 rounded-lg',
|
||||
'bg-bolt-elements-background-depth-2 border border-bolt-elements-borderColor',
|
||||
'text-sm text-bolt-elements-textPrimary placeholder-bolt-elements-textTertiary',
|
||||
'focus:outline-none focus:ring-2 focus:ring-purple-500/30',
|
||||
'group-hover:border-purple-500/30',
|
||||
'transition-all duration-200',
|
||||
)}
|
||||
/>
|
||||
<div className="i-ph:magnifying-glass absolute left-3 top-1/2 -translate-y-1/2 text-bolt-elements-textSecondary group-hover:text-purple-500 transition-colors" />
|
||||
</div>
|
||||
</div>
|
||||
{showLogs && (
|
||||
<div className="flex items-center gap-2 flex-nowrap">
|
||||
<button
|
||||
<motion.button
|
||||
onClick={handleExportLogs}
|
||||
className={classNames(
|
||||
'bg-bolt-elements-button-primary-background',
|
||||
'rounded-lg px-4 py-2 transition-colors duration-200',
|
||||
'hover:bg-bolt-elements-button-primary-backgroundHover',
|
||||
'text-bolt-elements-button-primary-text',
|
||||
)}
|
||||
className={classNames(settingsStyles.button.base, settingsStyles.button.primary, 'group')}
|
||||
whileHover={{ scale: 1.02 }}
|
||||
whileTap={{ scale: 0.98 }}
|
||||
>
|
||||
<div className="i-ph:download-simple group-hover:scale-110 transition-transform" />
|
||||
Export Logs
|
||||
</button>
|
||||
<button
|
||||
</motion.button>
|
||||
<motion.button
|
||||
onClick={handleClearLogs}
|
||||
className={classNames(
|
||||
'bg-bolt-elements-button-danger-background',
|
||||
'rounded-lg px-4 py-2 transition-colors duration-200',
|
||||
'hover:bg-bolt-elements-button-danger-backgroundHover',
|
||||
'text-bolt-elements-button-danger-text',
|
||||
)}
|
||||
className={classNames(settingsStyles.button.base, settingsStyles.button.danger, 'group')}
|
||||
whileHover={{ scale: 1.02 }}
|
||||
whileTap={{ scale: 0.98 }}
|
||||
>
|
||||
<div className="i-ph:trash group-hover:scale-110 transition-transform" />
|
||||
Clear Logs
|
||||
</button>
|
||||
</motion.button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-bolt-elements-bg-depth-1 rounded-lg p-4 h-[calc(100vh - 250px)] min-h-[400px] overflow-y-auto logs-container 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-start space-x-2 flex-wrap">
|
||||
<span className={`font-bold ${getLevelColor(log.level)} whitespace-nowrap`}>
|
||||
[{log.level.toUpperCase()}]
|
||||
</span>
|
||||
<span className="text-bolt-elements-textSecondary whitespace-nowrap">
|
||||
{new Date(log.timestamp).toLocaleString()}
|
||||
</span>
|
||||
<span className="text-bolt-elements-textPrimary break-all">{log.message}</span>
|
||||
</div>
|
||||
{log.details && (
|
||||
<pre className="mt-2 text-xs text-bolt-elements-textSecondary overflow-x-auto whitespace-pre-wrap break-all">
|
||||
{JSON.stringify(log.details, null, 2)}
|
||||
</pre>
|
||||
)}
|
||||
</div>
|
||||
))
|
||||
<motion.div
|
||||
ref={logsContainerRef}
|
||||
className={classNames(
|
||||
settingsStyles.card,
|
||||
'h-[calc(100vh-250px)] min-h-[400px] overflow-y-auto logs-container',
|
||||
'scrollbar-thin scrollbar-thumb-bolt-elements-borderColor scrollbar-track-transparent hover:scrollbar-thumb-purple-500/30',
|
||||
)}
|
||||
</div>
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
>
|
||||
{filteredLogs.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center h-full text-center p-8">
|
||||
<motion.div
|
||||
initial={{ scale: 0.8, opacity: 0 }}
|
||||
animate={{ scale: 1, opacity: 1 }}
|
||||
transition={{ type: 'spring', duration: 0.5 }}
|
||||
className="i-ph:clipboard-text text-6xl text-bolt-elements-textSecondary mb-4"
|
||||
/>
|
||||
<motion.p
|
||||
initial={{ y: 10, opacity: 0 }}
|
||||
animate={{ y: 0, opacity: 1 }}
|
||||
transition={{ delay: 0.2 }}
|
||||
className="text-bolt-elements-textSecondary"
|
||||
>
|
||||
No logs found
|
||||
</motion.p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="divide-y divide-bolt-elements-borderColor">
|
||||
{filteredLogs.map((log, index) => (
|
||||
<motion.div
|
||||
key={index}
|
||||
className={classNames(
|
||||
'p-4 font-mono hover:bg-bolt-elements-background-depth-3 transition-colors duration-200',
|
||||
{ 'border-t border-bolt-elements-borderColor': index === 0 },
|
||||
)}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: index * 0.03 }}
|
||||
>
|
||||
<div className="flex items-start gap-3">
|
||||
<div
|
||||
className={classNames(
|
||||
getLevelIcon(log.level),
|
||||
getLevelColor(log.level),
|
||||
'mt-1 flex-shrink-0 text-lg',
|
||||
)}
|
||||
/>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<span
|
||||
className={classNames(
|
||||
'font-bold whitespace-nowrap px-2 py-0.5 rounded-full text-xs',
|
||||
{
|
||||
'bg-blue-500/10': log.level === 'info',
|
||||
'bg-yellow-500/10': log.level === 'warning',
|
||||
'bg-red-500/10': log.level === 'error',
|
||||
'bg-bolt-elements-textSecondary/10': log.level === 'debug',
|
||||
},
|
||||
getLevelColor(log.level),
|
||||
)}
|
||||
>
|
||||
{log.level.toUpperCase()}
|
||||
</span>
|
||||
<span className="text-bolt-elements-textSecondary whitespace-nowrap text-xs">
|
||||
{new Date(log.timestamp).toLocaleString()}
|
||||
</span>
|
||||
<span className="text-bolt-elements-textPrimary break-all">{log.message}</span>
|
||||
</div>
|
||||
{log.details && (
|
||||
<motion.pre
|
||||
initial={{ opacity: 0, height: 0 }}
|
||||
animate={{ opacity: 1, height: 'auto' }}
|
||||
transition={{ duration: 0.2 }}
|
||||
className={classNames(
|
||||
'mt-2 text-xs',
|
||||
'overflow-x-auto whitespace-pre-wrap break-all',
|
||||
'bg-[#1A1A1A] dark:bg-[#0A0A0A] rounded-md p-3',
|
||||
'border border-[#333333] dark:border-[#1A1A1A]',
|
||||
'text-[#666666] dark:text-[#999999]',
|
||||
)}
|
||||
>
|
||||
{JSON.stringify(log.details, null, 2)}
|
||||
</motion.pre>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</motion.div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user