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,10 +1,11 @@
|
||||
import * as RadixDialog from '@radix-ui/react-dialog';
|
||||
import { motion } from 'framer-motion';
|
||||
import { useState, type ReactElement } from 'react';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import { useState } from 'react';
|
||||
import { classNames } from '~/utils/classNames';
|
||||
import { DialogTitle, dialogVariants, dialogBackdropVariants } from '~/components/ui/Dialog';
|
||||
import { IconButton } from '~/components/ui/IconButton';
|
||||
import styles from './Settings.module.scss';
|
||||
import { DialogTitle } from '~/components/ui/Dialog';
|
||||
import type { SettingCategory, TabType } from './settings.types';
|
||||
import { categoryLabels, categoryIcons } from './settings.types';
|
||||
import ProfileTab from './profile/ProfileTab';
|
||||
import ProvidersTab from './providers/ProvidersTab';
|
||||
import { useSettings } from '~/lib/hooks/useSettings';
|
||||
import FeaturesTab from './features/FeaturesTab';
|
||||
@@ -18,110 +19,281 @@ interface SettingsProps {
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
type TabType = 'data' | 'providers' | 'features' | 'debug' | 'event-logs' | 'connection';
|
||||
|
||||
export const SettingsWindow = ({ open, onClose }: SettingsProps) => {
|
||||
const { debug, eventLogs } = useSettings();
|
||||
const [activeTab, setActiveTab] = useState<TabType>('data');
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [activeTab, setActiveTab] = useState<TabType | null>(null);
|
||||
|
||||
const tabs: { id: TabType; label: string; icon: string; component?: ReactElement }[] = [
|
||||
{ id: 'data', label: 'Data', icon: 'i-ph:database', component: <DataTab /> },
|
||||
{ id: 'providers', label: 'Providers', icon: 'i-ph:key', component: <ProvidersTab /> },
|
||||
{ id: 'connection', label: 'Connection', icon: 'i-ph:link', component: <ConnectionsTab /> },
|
||||
{ id: 'features', label: 'Features', icon: 'i-ph:star', component: <FeaturesTab /> },
|
||||
...(debug
|
||||
? [
|
||||
{
|
||||
id: 'debug' as TabType,
|
||||
label: 'Debug Tab',
|
||||
icon: 'i-ph:bug',
|
||||
component: <DebugTab />,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(eventLogs
|
||||
? [
|
||||
{
|
||||
id: 'event-logs' as TabType,
|
||||
label: 'Event Logs',
|
||||
icon: 'i-ph:list-bullets',
|
||||
component: <EventLogsTab />,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
];
|
||||
const settingItems = [
|
||||
{
|
||||
id: 'profile' as const,
|
||||
label: 'Profile Settings',
|
||||
icon: 'i-ph:user-circle',
|
||||
category: 'profile' as const,
|
||||
description: 'Manage your personal information and preferences',
|
||||
component: () => <ProfileTab />,
|
||||
keywords: ['profile', 'account', 'avatar', 'email', 'name', 'theme', 'notifications'],
|
||||
},
|
||||
|
||||
{
|
||||
id: 'data' as const,
|
||||
label: 'Data Management',
|
||||
icon: 'i-ph:database',
|
||||
category: 'file_sharing' as const,
|
||||
description: 'Manage your chat history and application data',
|
||||
component: () => <DataTab />,
|
||||
keywords: ['data', 'export', 'import', 'backup', 'delete'],
|
||||
},
|
||||
|
||||
{
|
||||
id: 'providers' as const,
|
||||
label: 'Providers',
|
||||
icon: 'i-ph:key',
|
||||
category: 'file_sharing' as const,
|
||||
description: 'Configure AI providers and API keys',
|
||||
component: () => <ProvidersTab />,
|
||||
keywords: ['api', 'keys', 'providers', 'configuration'],
|
||||
},
|
||||
|
||||
{
|
||||
id: 'connection' as const,
|
||||
label: 'Connection',
|
||||
icon: 'i-ph:link',
|
||||
category: 'connectivity' as const,
|
||||
description: 'Manage network and connection settings',
|
||||
component: () => <ConnectionsTab />,
|
||||
keywords: ['network', 'connection', 'proxy', 'ssl'],
|
||||
},
|
||||
|
||||
{
|
||||
id: 'features' as const,
|
||||
label: 'Features',
|
||||
icon: 'i-ph:star',
|
||||
category: 'system' as const,
|
||||
description: 'Configure application features and preferences',
|
||||
component: () => <FeaturesTab />,
|
||||
keywords: ['features', 'settings', 'options'],
|
||||
},
|
||||
] as const;
|
||||
|
||||
const debugItems = debug
|
||||
? [
|
||||
{
|
||||
id: 'debug' as const,
|
||||
label: 'Debug',
|
||||
icon: 'i-ph:bug',
|
||||
category: 'system' as const,
|
||||
description: 'Advanced debugging tools and options',
|
||||
component: () => <DebugTab />,
|
||||
keywords: ['debug', 'logs', 'developer'],
|
||||
},
|
||||
]
|
||||
: [];
|
||||
|
||||
const eventLogItems = eventLogs
|
||||
? [
|
||||
{
|
||||
id: 'event-logs' as const,
|
||||
label: 'Event Logs',
|
||||
icon: 'i-ph:list-bullets',
|
||||
category: 'system' as const,
|
||||
description: 'View system events and application logs',
|
||||
component: () => <EventLogsTab />,
|
||||
keywords: ['logs', 'events', 'history'],
|
||||
},
|
||||
]
|
||||
: [];
|
||||
|
||||
const allSettingItems = [...settingItems, ...debugItems, ...eventLogItems];
|
||||
|
||||
const filteredItems = allSettingItems.filter(
|
||||
(item) =>
|
||||
item.label.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
item.description?.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
item.keywords?.some((keyword) => keyword.toLowerCase().includes(searchQuery.toLowerCase())),
|
||||
);
|
||||
|
||||
const groupedItems = filteredItems.reduce(
|
||||
(acc, item) => {
|
||||
if (!acc[item.category]) {
|
||||
acc[item.category] = allSettingItems.filter((i) => i.category === item.category);
|
||||
}
|
||||
|
||||
return acc;
|
||||
},
|
||||
{} as Record<SettingCategory, typeof allSettingItems>,
|
||||
);
|
||||
|
||||
const handleBackToDashboard = () => {
|
||||
setActiveTab(null);
|
||||
onClose();
|
||||
};
|
||||
|
||||
const activeTabItem = allSettingItems.find((item) => item.id === activeTab);
|
||||
|
||||
return (
|
||||
<RadixDialog.Root open={open}>
|
||||
<RadixDialog.Portal>
|
||||
<RadixDialog.Overlay asChild onClick={onClose}>
|
||||
<motion.div
|
||||
className="bg-black/50 fixed inset-0 z-max backdrop-blur-sm"
|
||||
initial="closed"
|
||||
animate="open"
|
||||
exit="closed"
|
||||
variants={dialogBackdropVariants}
|
||||
/>
|
||||
</RadixDialog.Overlay>
|
||||
<RadixDialog.Content aria-describedby={undefined} asChild>
|
||||
<motion.div
|
||||
className="fixed top-[50%] left-[50%] z-max h-[85vh] w-[90vw] max-w-[900px] translate-x-[-50%] translate-y-[-50%] border border-bolt-elements-borderColor rounded-lg shadow-lg focus:outline-none overflow-hidden"
|
||||
initial="closed"
|
||||
animate="open"
|
||||
exit="closed"
|
||||
variants={dialogVariants}
|
||||
>
|
||||
<div className="flex h-full">
|
||||
<div
|
||||
className={classNames(
|
||||
'w-48 border-r border-bolt-elements-borderColor bg-bolt-elements-background-depth-1 p-4 flex flex-col justify-between',
|
||||
styles['settings-tabs'],
|
||||
)}
|
||||
>
|
||||
<DialogTitle className="flex-shrink-0 text-lg font-semibold text-bolt-elements-textPrimary mb-2">
|
||||
Settings
|
||||
</DialogTitle>
|
||||
{tabs.map((tab) => (
|
||||
<button
|
||||
key={tab.id}
|
||||
onClick={() => setActiveTab(tab.id)}
|
||||
className={classNames(activeTab === tab.id ? styles.active : '')}
|
||||
<div className="fixed inset-0 flex items-center justify-center z-[9999]">
|
||||
<RadixDialog.Overlay asChild>
|
||||
<motion.div
|
||||
className="absolute inset-0 bg-black/50 backdrop-blur-sm"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
/>
|
||||
</RadixDialog.Overlay>
|
||||
<RadixDialog.Content aria-describedby={undefined} asChild>
|
||||
<motion.div
|
||||
className={classNames(
|
||||
'relative',
|
||||
'w-[1000px] max-h-[90vh] min-h-[700px]',
|
||||
'bg-[#FAFAFA] dark:bg-[#0A0A0A]',
|
||||
'rounded-2xl overflow-hidden shadow-2xl',
|
||||
'border border-[#E5E5E5] dark:border-[#1A1A1A]',
|
||||
'overflow-y-auto scrollbar-thin scrollbar-thumb-gray-300 dark:scrollbar-thumb-gray-700 scrollbar-track-transparent',
|
||||
)}
|
||||
initial={{ opacity: 0, scale: 0.95, y: 20 }}
|
||||
animate={{ opacity: 1, scale: 1, y: 0 }}
|
||||
exit={{ opacity: 0, scale: 0.95, y: 20 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
>
|
||||
<AnimatePresence mode="wait">
|
||||
{activeTab ? (
|
||||
<motion.div
|
||||
className="flex flex-col h-full"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -20 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
>
|
||||
<div className={tab.icon} />
|
||||
{tab.label}
|
||||
</button>
|
||||
))}
|
||||
<div className="mt-auto flex flex-col gap-2">
|
||||
<a
|
||||
href="https://github.com/stackblitz-labs/bolt.diy"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className={classNames(styles['settings-button'], 'flex items-center gap-2')}
|
||||
>
|
||||
<div className="i-ph:github-logo" />
|
||||
GitHub
|
||||
</a>
|
||||
<a
|
||||
href="https://stackblitz-labs.github.io/bolt.diy/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className={classNames(styles['settings-button'], 'flex items-center gap-2')}
|
||||
>
|
||||
<div className="i-ph:book" />
|
||||
Docs
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center justify-between p-6 border-b border-[#E5E5E5] dark:border-[#1A1A1A] sticky top-0 bg-[#FAFAFA] dark:bg-[#0A0A0A] z-10">
|
||||
<div className="flex items-center">
|
||||
<button
|
||||
onClick={() => setActiveTab(null)}
|
||||
className="flex items-center gap-2 px-4 py-2 rounded-lg text-sm bg-[#F5F5F5] dark:bg-[#1A1A1A] text-[#666666] dark:text-[#999999] hover:text-[#333333] dark:hover:text-white"
|
||||
>
|
||||
<div className="i-ph:arrow-left w-4 h-4" />
|
||||
Back to Settings
|
||||
</button>
|
||||
|
||||
<div className="flex-1 flex flex-col p-8 pt-10 bg-bolt-elements-background-depth-2">
|
||||
<div className="flex-1 overflow-y-auto">{tabs.find((tab) => tab.id === activeTab)?.component}</div>
|
||||
</div>
|
||||
</div>
|
||||
<RadixDialog.Close asChild onClick={onClose}>
|
||||
<IconButton icon="i-ph:x" className="absolute top-[10px] right-[10px]" />
|
||||
</RadixDialog.Close>
|
||||
</motion.div>
|
||||
</RadixDialog.Content>
|
||||
<div className="text-bolt-elements-textTertiary mx-6 select-none">|</div>
|
||||
|
||||
{activeTabItem && (
|
||||
<div className="flex items-center gap-4">
|
||||
<div className={classNames(activeTabItem.icon, 'w-6 h-6 text-purple-500')} />
|
||||
<div>
|
||||
<h2 className="text-lg font-medium text-bolt-elements-textPrimary">
|
||||
{activeTabItem.label}
|
||||
</h2>
|
||||
<p className="text-sm text-bolt-elements-textSecondary">{activeTabItem.description}</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={handleBackToDashboard}
|
||||
className="flex items-center gap-2 px-4 py-2 rounded-lg text-sm bg-[#F5F5F5] dark:bg-[#1A1A1A] text-[#666666] dark:text-[#999999] hover:text-[#333333] dark:hover:text-white"
|
||||
>
|
||||
<div className="i-ph:house w-4 h-4" />
|
||||
Back to Bolt DIY
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex-1 p-6 overflow-y-auto scrollbar-thin scrollbar-thumb-gray-300 dark:scrollbar-thumb-gray-700 scrollbar-track-transparent">
|
||||
{allSettingItems.find((item) => item.id === activeTab)?.component()}
|
||||
</div>
|
||||
</motion.div>
|
||||
) : (
|
||||
<motion.div
|
||||
className="flex flex-col h-full"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -20 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
>
|
||||
<div className="flex items-center justify-between p-6 border-b border-[#E5E5E5] dark:border-[#1A1A1A] sticky top-0 bg-[#FAFAFA] dark:bg-[#0A0A0A] z-10">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="i-ph:lightning-fill w-5 h-5 text-purple-500" />
|
||||
<DialogTitle className="text-lg font-medium text-bolt-elements-textPrimary">
|
||||
Bolt Control Panel
|
||||
</DialogTitle>
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="relative w-[320px]">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search settings..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className={classNames(
|
||||
'w-full h-10 pl-10 pr-4 rounded-lg text-sm',
|
||||
'bg-[#F8F8F8] dark:bg-[#1A1A1A]',
|
||||
'border border-[#E5E5E5] dark:border-[#333333]',
|
||||
'text-bolt-elements-textPrimary placeholder-bolt-elements-textTertiary',
|
||||
'focus:outline-none focus:ring-1 focus:ring-purple-500 transition-all',
|
||||
)}
|
||||
/>
|
||||
<div className="absolute left-3.5 top-1/2 -translate-y-1/2">
|
||||
<div className="i-ph:magnifying-glass w-4 h-4 text-bolt-elements-textTertiary" />
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleBackToDashboard}
|
||||
className="flex items-center gap-2 px-4 py-2 rounded-lg text-sm bg-[#F5F5F5] dark:bg-[#1A1A1A] text-[#666666] dark:text-[#999999] hover:text-[#333333] dark:hover:text-white"
|
||||
>
|
||||
<div className="i-ph:house w-4 h-4" />
|
||||
Back to Bolt DIY
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 p-6 overflow-y-auto scrollbar-thin scrollbar-thumb-gray-300 dark:scrollbar-thumb-gray-700 scrollbar-track-transparent">
|
||||
<div className="space-y-8">
|
||||
{(Object.keys(groupedItems) as SettingCategory[]).map((category) => (
|
||||
<div key={category} className="space-y-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className={classNames(categoryIcons[category], 'w-5 h-5 text-purple-500')} />
|
||||
<h2 className="text-base font-medium text-bolt-elements-textPrimary">
|
||||
{categoryLabels[category]}
|
||||
</h2>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{groupedItems[category].map((item) => (
|
||||
<button
|
||||
key={item.id}
|
||||
onClick={() => setActiveTab(item.id)}
|
||||
className={classNames(
|
||||
'flex flex-col gap-2 p-4 rounded-lg text-left',
|
||||
'bg-white dark:bg-[#0A0A0A]',
|
||||
'border border-[#E5E5E5] dark:border-[#1A1A1A]',
|
||||
'hover:bg-[#F8F8F8] dark:hover:bg-[#1A1A1A]',
|
||||
'transition-all duration-200',
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className={classNames(item.icon, 'w-5 h-5 text-purple-500')} />
|
||||
<span className="text-sm font-medium text-bolt-elements-textPrimary">
|
||||
{item.label}
|
||||
</span>
|
||||
</div>
|
||||
{item.description && (
|
||||
<p className="text-sm text-bolt-elements-textSecondary">{item.description}</p>
|
||||
)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</motion.div>
|
||||
</RadixDialog.Content>
|
||||
</div>
|
||||
</RadixDialog.Portal>
|
||||
</RadixDialog.Root>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user